#ifndef UFBX_UFBX_C_INCLUDED
#define UFBX_UFBX_C_INCLUDED

#if defined(UFBX_HEADER_PATH)
	#include UFBX_HEADER_PATH
#else
	#include "ufbx.h"
#endif

// -- User configuration

// User configuration:
//   UFBX_REGRESSION           Enable regression mode for development
//   UFBX_UBSAN                Explicitly enable undefined behavior sanitizer workarounds
//   UFBX_NO_UNALIGNED_LOADS   Do not use unaligned loads even when they are supported
//   UFBX_USE_UNALIGNED_LOADS  Forcibly use unaligned loads on unknown platforms
//   UFBX_USE_SSE              Explicitly enable SSE2 support (for x86)
//   UFBX_HAS_FTELLO           Allow ufbx to use `ftello()` to measure file size
//   UFBX_WASM_32BIT           Optimize WASM for 32-bit architectures
//   UFBX_TRACE                Log calls of `ufbxi_check()` for tracing execution
//   UFBX_LITTLE_ENDIAN=0/1    Explicitly define little/big endian architecture
//   UFBX_PATH_SEPARATOR=''    Specify default platform path separator
//   UFBX_NO_SSE               Do not try to include SSE

// Dependencies:
//   UFBX_NO_MALLOC              Disable default malloc/realloc/free
//   UFBX_NO_STDIO               Disable stdio FILE API
//   UFBX_EXTERNAL_MALLOC        Link to external ufbx_malloc() interface
//   UFBX_EXTERNAL_STDIO         Link to external ufbx_stdio_() interface
//   UFBX_EXTERNAL_MATH          Link to external <math.h> interface
//   UFBX_EXTERNAL_STRING        Link to external <string.h> interface

// Freestanding:
//   UFBX_MATH_PREFIX='ufbx_'    Prefix for external <math.h> functions used
//   UFBX_STRING_PREFIX='ufbx_'  Prefix for external <string.h> functions used
//   UFBX_NO_LIBC                Do not include libc (implies UFBX_EXTERNAL_MATH/STRING/MALLOC/STDIO by default)
//   UFBX_NO_LIBC_TYPES          Do not include any libc headers, you must define all types in <stdint.h/stddef.h/etc>

// Mostly internal for debugging:
//   UFBX_STATIC_ANALYSIS      Enable static analysis augmentation
//   UFBX_DEBUG_BINARY_SEARCH  Force using binary search for debugging
//   UFBX_EXTENSIVE_THREADING  Use threads for small inputs

#if defined(UFBX_CONFIG_SOURCE)
	#include UFBX_CONFIG_SOURCE
#endif

// -- Configuration

#define UFBXI_MAX_NON_ARRAY_VALUES 8
#define UFBXI_MAX_NODE_DEPTH 32
#define UFBXI_MAX_XML_DEPTH 32
#define UFBXI_MAX_SKIP_SIZE 0x40000000
#define UFBXI_MAP_MAX_SCAN 32
#define UFBXI_KD_FAST_DEPTH 6
#define UFBXI_HUGE_MAX_SCAN 16
#define UFBXI_MIN_FILE_FORMAT_LOOKAHEAD 32
#define UFBXI_FACE_GROUP_HASH_BITS 8
#define UFBXI_MIN_THREADED_DEFLATE_BYTES 256
#define UFBXI_MIN_THREADED_ASCII_VALUES 64
#define UFBXI_GEOMETRY_CACHE_BUFFER_SIZE 512

#ifndef UFBXI_MAX_NURBS_ORDER
#define UFBXI_MAX_NURBS_ORDER 128
#endif

// By default enough to have squares be non-denormal
#ifndef UFBX_EPSILON
#define UFBX_EPSILON (sizeof(ufbx_real) == sizeof(float) ? \
	(ufbx_real)1.0842021795674597e-19f : (ufbx_real)1.4916681462400413e-154)
#endif

// -- Feature exclusion

#if !defined(UFBX_MINIMAL)
	#if !defined(UFBX_NO_SUBDIVISION)
		#define UFBXI_FEATURE_SUBDIVISION 1
	#endif
	#if !defined(UFBX_NO_TESSELLATION)
		#define UFBXI_FEATURE_TESSELLATION 1
	#endif
	#if !defined(UFBX_NO_GEOMETRY_CACHE)
		#define UFBXI_FEATURE_GEOMETRY_CACHE 1
	#endif
	#if !defined(UFBX_NO_SCENE_EVALUATION)
		#define UFBXI_FEATURE_SCENE_EVALUATION 1
	#endif
	#if !defined(UFBX_NO_SKINNING_EVALUATION)
		#define UFBXI_FEATURE_SKINNING_EVALUATION 1
	#endif
	#if !defined(UFBX_NO_ANIMATION_BAKING)
		#define UFBXI_FEATURE_ANIMATION_BAKING 1
	#endif
	#if !defined(UFBX_NO_TRIANGULATION)
		#define UFBXI_FEATURE_TRIANGULATION 1
	#endif
	#if !defined(UFBX_NO_INDEX_GENERATION)
		#define UFBXI_FEATURE_INDEX_GENERATION 1
	#endif
	#if !defined(UFBX_NO_FORMAT_OBJ)
		#define UFBXI_FEATURE_FORMAT_OBJ 1
	#endif
#endif

#if defined(UFBX_DEV)
	#if !defined(UFBX_NO_ERROR_STACK)
		#define UFBXI_FEATURE_ERROR_STACK 1
	#endif
#endif

#if !defined(UFBXI_FEATURE_SUBDIVISION) && defined(UFBX_ENABLE_SUBDIVISION)
	#define UFBXI_FEATURE_SUBDIVISION 1
#endif
#if !defined(UFBXI_FEATURE_TESSELLATION) && defined(UFBX_ENABLE_TESSELLATION)
	#define UFBXI_FEATURE_TESSELLATION 1
#endif
#if !defined(UFBXI_FEATURE_GEOMETRY_CACHE) && defined(UFBX_ENABLE_GEOMETRY_CACHE)
	#define UFBXI_FEATURE_GEOMETRY_CACHE 1
#endif
#if !defined(UFBXI_FEATURE_SCENE_EVALUATION) && defined(UFBX_ENABLE_SCENE_EVALUATION)
	#define UFBXI_FEATURE_SCENE_EVALUATION 1
#endif
#if !defined(UFBXI_FEATURE_SKINNING_EVALUATION) && defined(UFBX_ENABLE_SKINNING_EVALUATION)
	#define UFBXI_FEATURE_SKINNING_EVALUATION 1
#endif
#if !defined(UFBXI_FEATURE_ANIMATION_BAKING) && defined(UFBX_ENABLE_ANIMATION_BAKING)
	#define UFBXI_FEATURE_ANIMATION_BAKING 1
#endif
#if !defined(UFBXI_FEATURE_TRIANGULATION) && defined(UFBX_ENABLE_TRIANGULATION)
	#define UFBXI_FEATURE_TRIANGULATION 1
#endif
#if !defined(UFBXI_FEATURE_INDEX_GENERATION) && defined(UFBX_ENABLE_INDEX_GENERATION)
	#define UFBXI_FEATURE_INDEX_GENERATION 1
#endif
#if !defined(UFBXI_FEATURE_FORMAT_OBJ) && defined(UFBX_ENABLE_FORMAT_OBJ)
	#define UFBXI_FEATURE_FORMAT_OBJ 1
#endif
#if !defined(UFBXI_FEATURE_ERROR_STACK) && defined(UFBX_ENABLE_ERROR_STACK)
	#define UFBXI_FEATURE_ERROR_STACK 1
#endif

#if !defined(UFBXI_FEATURE_SUBDIVISION)
	#define UFBXI_FEATURE_SUBDIVISION 0
#endif
#if !defined(UFBXI_FEATURE_TESSELLATION)
	#define UFBXI_FEATURE_TESSELLATION 0
#endif
#if !defined(UFBXI_FEATURE_GEOMETRY_CACHE)
	#define UFBXI_FEATURE_GEOMETRY_CACHE 0
#endif
#if !defined(UFBXI_FEATURE_SCENE_EVALUATION)
	#define UFBXI_FEATURE_SCENE_EVALUATION 0
#endif
#if !defined(UFBXI_FEATURE_SKINNING_EVALUATION)
	#define UFBXI_FEATURE_SKINNING_EVALUATION 0
#endif
#if !defined(UFBXI_FEATURE_ANIMATION_BAKING)
	#define UFBXI_FEATURE_ANIMATION_BAKING 0
#endif
#if !defined(UFBXI_FEATURE_TRIANGULATION)
	#define UFBXI_FEATURE_TRIANGULATION 0
#endif
#if !defined(UFBXI_FEATURE_INDEX_GENERATION)
	#define UFBXI_FEATURE_INDEX_GENERATION 0
#endif
#if !defined(UFBXI_FEATURE_FORMAT_OBJ)
	#define UFBXI_FEATURE_FORMAT_OBJ 0
#endif
#if !defined(UFBXI_FEATURE_ERROR_STACK)
	#define UFBXI_FEATURE_ERROR_STACK 0
#endif

// Derived features

#if UFBXI_FEATURE_GEOMETRY_CACHE
	#define UFBXI_FEATURE_XML 1
#else
	#define UFBXI_FEATURE_XML 0
#endif

#if UFBXI_FEATURE_TRIANGULATION
	#define UFBXI_FEATURE_KD 1
#else
	#define UFBXI_FEATURE_KD 0
#endif

#if !UFBXI_FEATURE_SUBDIVISION || !UFBXI_FEATURE_TESSELLATION || !UFBXI_FEATURE_GEOMETRY_CACHE || !UFBXI_FEATURE_SCENE_EVALUATION || !UFBXI_FEATURE_SKINNING_EVALUATION || !UFBXI_FEATURE_ANIMATION_BAKING || !UFBXI_FEATURE_TRIANGULATION || !UFBXI_FEATURE_INDEX_GENERATION || !UFBXI_FEATURE_XML || !UFBXI_FEATURE_KD || !UFBXI_FEATURE_FORMAT_OBJ
	#define UFBXI_PARTIAL_FEATURES 1
#endif

// -- Headers

// Legacy mapping
#if !defined(UFBX_EXTERNAL_MATH) && defined(UFBX_NO_MATH_H)
	#define UFBX_EXTERNAL_MATH
#endif

#if !defined(UFBX_NO_LIBC_TYPES)
	#include <stdarg.h>
#endif

#if !defined(UFBX_NO_LIBC)
	#if !defined(UFBX_NO_FLOAT_H)
		#include <float.h>
	#endif
	#if !defined(UFBX_EXTERNAL_MATH)
		#include <math.h>
	#endif
	#if !defined(UFBX_EXTERNAL_STRING)
		#include <string.h>
	#endif
	#if !defined(UFBX_NO_STDIO) && !defined(UFBX_EXTERNAL_STDIO)
		#include <stdio.h>
	#endif
	#if !defined(UFBX_NO_MALLOC) && !defined(UFBX_EXTERNAL_MALLOC)
		#include <stdlib.h>
	#endif
#else
	#if !defined(UFBX_EXTERNAL_MATH) && !defined(UFBX_NO_EXTERNAL_MATH)
		#define UFBX_EXTERNAL_MATH
	#endif
	#if !defined(UFBX_EXTERNAL_STRING) && !defined(UFBX_NO_EXTERNAL_STRING)
		#define UFBX_EXTERNAL_STRING
	#endif
	#if !defined(UFBX_EXTERNAL_MALLOC) && !defined(UFBX_NO_EXTERNAL_MALLOC) && !defined(UFBX_NO_MALLOC)
		#define UFBX_EXTERNAL_MALLOC
	#endif
	#if !defined(UFBX_EXTERNAL_STDIO) && !defined(UFBX_NO_EXTERNAL_STDIO) && !defined(UFBX_NO_STDIO)
		#define UFBX_EXTERNAL_STDIO
	#endif
#endif

#if defined(UFBX_EXTERNAL_STRING) && !defined(UFBX_STRING_PREFIX)
	#define UFBX_STRING_PREFIX ufbx_
#endif

#if !defined(UFBX_EXTERNAL_MATH)
	#if !defined(UFBX_MATH_PREFIX)
		#define UFBX_MATH_PREFIX
	#endif
#endif

#define ufbxi_pre_cat2(a, b) a##b
#define ufbxi_pre_cat(a, b) ufbxi_pre_cat2(a, b)

// -- External functions

#ifndef ufbx_extern_abi
	#if defined(UFBX_STATIC)
		#define ufbx_extern_abi static
	#else
		#define ufbx_extern_abi
	#endif
#endif

#if defined(UFBX_MATH_PREFIX)
	#define ufbxi_math_fn(name) ufbxi_pre_cat(UFBX_MATH_PREFIX, name)
	#define ufbx_sqrt ufbxi_math_fn(sqrt)
	#define ufbx_fabs ufbxi_math_fn(fabs)
	#define ufbx_pow ufbxi_math_fn(pow)
	#define ufbx_sin ufbxi_math_fn(sin)
	#define ufbx_cos ufbxi_math_fn(cos)
	#define ufbx_tan ufbxi_math_fn(tan)
	#define ufbx_asin ufbxi_math_fn(asin)
	#define ufbx_acos ufbxi_math_fn(acos)
	#define ufbx_atan ufbxi_math_fn(atan)
	#define ufbx_atan2 ufbxi_math_fn(atan2)
	#define ufbx_copysign ufbxi_math_fn(copysign)
	#define ufbx_fmin ufbxi_math_fn(fmin)
	#define ufbx_fmax ufbxi_math_fn(fmax)
	#define ufbx_nextafter ufbxi_math_fn(nextafter)
	#define ufbx_rint ufbxi_math_fn(rint)
	#define ufbx_ceil ufbxi_math_fn(ceil)
	#define ufbx_isnan ufbxi_math_fn(isnan)
#endif

#if !defined(UFBX_NO_EXTERNAL_DEFINES)

#if defined(__cplusplus)
extern "C" {
#endif

#if defined(UFBX_EXTERNAL_MATH)
	ufbx_extern_abi double ufbx_sqrt(double x);
	ufbx_extern_abi double ufbx_sin(double x);
	ufbx_extern_abi double ufbx_cos(double x);
	ufbx_extern_abi double ufbx_tan(double x);
	ufbx_extern_abi double ufbx_asin(double x);
	ufbx_extern_abi double ufbx_acos(double x);
	ufbx_extern_abi double ufbx_atan(double x);
	ufbx_extern_abi double ufbx_atan2(double y, double x);
	ufbx_extern_abi double ufbx_pow(double x, double y);
	ufbx_extern_abi double ufbx_fmin(double a, double b);
	ufbx_extern_abi double ufbx_fmax(double a, double b);
	ufbx_extern_abi double ufbx_fabs(double x);
	ufbx_extern_abi double ufbx_copysign(double x, double y);
	ufbx_extern_abi double ufbx_nextafter(double x, double y);
	ufbx_extern_abi double ufbx_rint(double x);
	ufbx_extern_abi double ufbx_ceil(double x);
	ufbx_extern_abi int ufbx_isnan(double x);
#endif

#if defined(UFBX_EXTERNAL_STRING)
	ufbx_extern_abi size_t ufbx_strlen(const char *str);
	ufbx_extern_abi void *ufbx_memcpy(void *dst, const void *src, size_t count);
	ufbx_extern_abi void *ufbx_memmove(void *dst, const void *src, size_t count);
	ufbx_extern_abi void *ufbx_memset(void *dst, int ch, size_t count);
	ufbx_extern_abi const void *ufbx_memchr(const void *ptr, int value, size_t count);
	ufbx_extern_abi int ufbx_memcmp(const void *a, const void *b, size_t count);
	ufbx_extern_abi int ufbx_strcmp(const char *a, const char *b);
	ufbx_extern_abi int ufbx_strncmp(const char *a, const char *b, size_t count);
#endif

#if defined(UFBX_EXTERNAL_MALLOC)
	ufbx_extern_abi void *ufbx_malloc(size_t size);
	ufbx_extern_abi void *ufbx_realloc(void *ptr, size_t old_size, size_t new_size);
	ufbx_extern_abi void ufbx_free(void *ptr, size_t old_size);
#endif

#if defined(UFBX_EXTERNAL_STDIO)
	ufbx_extern_abi void *ufbx_stdio_open(const char *path, size_t path_len);
	ufbx_extern_abi size_t ufbx_stdio_read(void *file, void *data, size_t size);
	ufbx_extern_abi bool ufbx_stdio_skip(void *file, size_t size);
	ufbx_extern_abi uint64_t ufbx_stdio_size(void *file);
	ufbx_extern_abi void ufbx_stdio_close(void *file);
#endif

#if defined(__cplusplus)
}
#endif

#endif

#if !defined(UFBX_INFINITY)
	#if defined(INFINITY)
		#define UFBX_INFINITY INFINITY
	#else
		#define UFBX_INFINITY (1e+300 * 1e+300)
	#endif
#endif
#if !defined(UFBX_NAN)
	#if defined(NAN)
		#define UFBX_NAN NAN
	#else
		#define UFBX_NAN (UFBX_INFINITY * 0.0f)
	#endif
#endif
#if !defined(UFBX_FLT_EPSILON)
	#if defined(FLT_EPSILON)
		#define UFBX_FLT_EPSILON FLT_EPSILON
	#else
		#define UFBX_FLT_EPSILON 1.192092896e-07f
	#endif
#endif
#if !defined(UFBX_FLT_EVAL_METHOD)
	#if defined(FLT_EVAL_METHOD)
		#define UFBX_FLT_EVAL_METHOD FLT_EVAL_METHOD
	#elif defined(__FLT_EVAL_METHOD__)
		#define UFBX_FLT_EVAL_METHOD __FLT_EVAL_METHOD__
	#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64))
		#define UFBX_FLT_EVAL_METHOD 0
	#else
		#define UFBX_FLT_EVAL_METHOD -1
	#endif
#endif

#if defined(ufbx_malloc) || defined(ufbx_realloc) || defined(ufbx_free)
	// User provided allocators
	#if !defined(ufbx_malloc) || !defined(ufbx_realloc) || !defined(ufbx_free)
		#error Inconsistent custom global allocator
	#endif
#elif defined(UFBX_NO_MALLOC)
	#define ufbx_malloc(size) ((void)(size), (void*)NULL)
	#define ufbx_realloc(ptr, old_size, new_size) ((void)(ptr), (void)(old_size), (void)(new_size), (void*)NULL)
	#define ufbx_free(ptr, old_size) ((void)(ptr), (void*)(old_size))
#elif defined(UFBX_EXTERNAL_MALLOC)
	// Nop
#else
	#define ufbx_malloc(size) malloc((size))
	#define ufbx_realloc(ptr, old_size, new_size) realloc((ptr), (new_size))
	#define ufbx_free(ptr, old_size) free((ptr))
#endif

#if !defined(ufbx_panic_handler)
	static void ufbxi_panic_handler(const char *message)
	{
		(void)message;
		#if !defined(UFBX_NO_STDIO) && !defined(UFBX_EXTERNAL_STDIO)
			fprintf(stderr, "ufbx panic: %s\n", message);
		#endif
		ufbx_assert(false && "ufbx panic: See stderr for more information");
	}
	#define ufbx_panic_handler ufbxi_panic_handler
#endif

// -- Platform

#if defined(_MSC_VER)
	#define UFBXI_MSC_VER _MSC_VER
#else
	#define UFBXI_MSC_VER 0
#endif

#if defined(__GNUC__)
	#define UFBXI_GNUC __GNUC__
#else
	#define UFBXI_GNUC 0
#endif

#if !defined(UFBX_STANDARD_C) && defined(_MSC_VER)
	#define ufbxi_noinline __declspec(noinline)
	#define ufbxi_forceinline __forceinline
	#define ufbxi_restrict __restrict
	#if defined(_Check_return_)
		#define ufbxi_nodiscard _Check_return_
	#else
		#define ufbxi_nodiscard
	#endif
	#define ufbxi_unused
	#define ufbxi_unlikely(cond) (cond)
#elif !defined(UFBX_STANDARD_C) && (defined(__GNUC__) || defined(__clang__))
	#define ufbxi_noinline __attribute__((noinline))
	#define ufbxi_forceinline inline __attribute__((always_inline))
	#define ufbxi_restrict __restrict
	#define ufbxi_nodiscard __attribute__((warn_unused_result))
	#define ufbxi_unused __attribute__((unused))
	#define ufbxi_unlikely(cond) __builtin_expect((cond), 0)
#else
	#define ufbxi_noinline
	#define ufbxi_forceinline
	#define ufbxi_nodiscard
	#define ufbxi_restrict
	#define ufbxi_unused
	#define ufbxi_unlikely(cond) (cond)
#endif

#if !defined(UFBX_STANDARD_C) && defined(__clang__)
	#define ufbxi_nounroll _Pragma("clang loop unroll(disable)") _Pragma("clang loop vectorize(disable)")
#elif !defined(UFBX_STANDARD_C) && UFBXI_GNUC >= 8
	#define ufbxi_nounroll _Pragma("GCC unroll 0")
#elif !defined(UFBX_STANDARD_C) && defined(_MSC_VER)
	#define ufbxi_nounroll __pragma(loop(no_vector))
#else
	#define ufbxi_nounroll
#endif

#if defined(__GNUC__) && !defined(__clang__)
	#define ufbxi_ignore(cond) (void)!(cond)
#else
	#define ufbxi_ignore(cond) (void)(cond)
#endif

#if defined(_MSC_VER)
	#pragma warning(push)
	#pragma warning(disable: 4061) // enumerator 'ENUM' in switch of enum 'enum' is not explicitly handled by a case label
	#pragma warning(disable: 4200) // nonstandard extension used: zero-sized array in struct/union
	#pragma warning(disable: 4201) // nonstandard extension used: nameless struct/union
	#pragma warning(disable: 4210) // nonstandard extension used: function given file scope
	#pragma warning(disable: 4127) // conditional expression is constant
	#pragma warning(disable: 4706) // assignment within conditional expression
	#pragma warning(disable: 4789) // buffer 'type_and_name' of size 8 bytes will be overrun; 16 bytes will be written starting at offset 0
	#pragma warning(disable: 4820) // type': 'N' bytes padding added after data member 'member'
	#if defined(UFBX_STANDARD_C)
		#pragma warning(disable: 4996) // 'fopen': This function or variable may be unsafe. Consider using fopen_s instead.
	#endif
	#if defined(UFBXI_PARTIAL_FEATURES)
		#pragma warning(disable: 4100) // 'name': unreferenced formal parameter
		#pragma warning(disable: 4505) // 'func': unreferenced function with internal linkage has been removed
	#endif
#endif

#if defined(__clang__)
	#pragma clang diagnostic push
	#pragma clang diagnostic ignored "-Wmissing-field-initializers"
	#pragma clang diagnostic ignored "-Wmissing-braces"
	#pragma clang diagnostic ignored "-Wdouble-promotion"
	#pragma clang diagnostic ignored "-Wpedantic"
	#pragma clang diagnostic ignored "-Wcast-qual"
	#pragma clang diagnostic ignored "-Wcast-align"
	#pragma clang diagnostic ignored "-Wcovered-switch-default"
	#pragma clang diagnostic ignored "-Wpadded"
	#pragma clang diagnostic ignored "-Wswitch-enum"
	#pragma clang diagnostic ignored "-Wfloat-equal"
	#pragma clang diagnostic ignored "-Wformat-nonliteral"
	#if __has_warning("-Watomic-implicit-seq-cst")
		#pragma clang diagnostic ignored "-Watomic-implicit-seq-cst"
	#endif
	#if defined(UFBX_STANDARD_C)
		#pragma clang diagnostic ignored "-Wunused-function"
	#endif
	#if defined(UFBXI_PARTIAL_FEATURES)
		#pragma clang diagnostic ignored "-Wunused-function"
		#pragma clang diagnostic ignored "-Wunused-parameter"
	#endif
	#if defined(__cplusplus)
		#pragma clang diagnostic ignored "-Wold-style-cast"
		#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant"
	#else
		#pragma clang diagnostic ignored "-Wdeclaration-after-statement"
		#pragma clang diagnostic ignored "-Wbad-function-cast"
	#endif
#endif

#if defined(__GNUC__)
	#pragma GCC diagnostic push
	#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
	#pragma GCC diagnostic ignored "-Wmissing-braces"
	#pragma GCC diagnostic ignored "-Wdouble-promotion"
	#pragma GCC diagnostic ignored "-Wpedantic"
	#pragma GCC diagnostic ignored "-Wcast-qual"
	#pragma GCC diagnostic ignored "-Wcast-align"
	#pragma GCC diagnostic ignored "-Wpadded"
	#pragma GCC diagnostic ignored "-Wswitch-enum"
	#pragma GCC diagnostic ignored "-Wfloat-equal"
	#pragma GCC diagnostic ignored "-Wformat-nonliteral"
	#pragma GCC diagnostic ignored "-Wlong-long"
	#if defined(UFBX_STANDARD_C)
		#pragma GCC diagnostic ignored "-Wunused-function"
	#endif
	#if defined(UFBXI_PARTIAL_FEATURES)
		#pragma GCC diagnostic ignored "-Wunused-function"
		#pragma GCC diagnostic ignored "-Wunused-parameter"
	#endif
	#if defined(__cplusplus)
		#pragma GCC diagnostic ignored "-Wold-style-cast"
		#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant"
	#else
		#pragma GCC diagnostic ignored "-Wdeclaration-after-statement"
		#pragma GCC diagnostic ignored "-Wbad-function-cast"
		#if __GNUC__ >= 5
			#pragma GCC diagnostic ignored "-Wc90-c99-compat"
			#pragma GCC diagnostic ignored "-Wc99-c11-compat"
		#endif
	#endif
#endif

#if !defined(ufbx_static_assert)
	#if defined(__cplusplus) && __cplusplus >= 201103
		#define ufbx_static_assert(desc, cond) static_assert(cond, #desc ": " #cond)
	#else
		#define ufbx_static_assert(desc, cond) typedef char ufbxi_static_assert_##desc[(cond)?1:-1]
	#endif
#endif

#if defined(__has_feature)
	#if __has_feature(undefined_behavior_sanitizer) && !defined(UFBX_UBSAN)
		#define UFBX_UBSAN 1
	#endif
#endif

#if defined(__SANITIZE_UNDEFINED__)  && !defined(UFBX_UBSAN)
	#define UFBX_UBSAN 1
#endif

// Don't use unaligned loads with UB-sanitizer
#if defined(UFBX_UBSAN) && !defined(UFBX_NO_UNALIGNED_LOADS)
	#define UFBX_NO_UNALIGNED_LOADS
#endif

#if defined(__clang_analyzer__) && !defined(UFBX_STATIC_ANALYSIS)
	#define UFBX_STATIC_ANALYSIS 1
#endif

#if defined(UFBX_STATIC_ANALYSIS)
	bool ufbxi_analysis_opaque;
	#define ufbxi_maybe_null(ptr) (ufbxi_analysis_opaque ? (ptr) : NULL)
	#define ufbxi_analysis_assert(cond) ufbx_assert(cond)
#else
	#define ufbxi_maybe_null(ptr) (ptr)
	#define ufbxi_analysis_assert(cond) (void)0
#endif

#if defined(UFBX_STATIC_ANALYSIS) || defined(UFBX_UBSAN)
	#define ufbxi_maybe_uninit(cond, value, def) ((cond) ? (value) : (def))
#else
	#define ufbxi_maybe_uninit(cond, value, def) (value)
#endif

#if !defined(ufbxi_trace)
	#if defined(UFBX_TRACE)
		#define ufbxi_trace(desc) (fprintf(stderr, "ufbx trace: %s:%d: %s\n", __FILE__, __LINE__, #desc), fflush(stderr), desc)
	#else
		#define ufbxi_trace(desc) (desc)
	#endif
#endif

#ifndef UFBX_PATH_SEPARATOR
	#if defined(_WIN32)
		#define UFBX_PATH_SEPARATOR '\\'
	#else
		#define UFBX_PATH_SEPARATOR '/'
	#endif
#endif

#if !defined(UFBX_STANDARD_C) && defined(_POSIX_C_SOURCE)
	#if _POSIX_C_SOURCE >= 200112l
		#ifndef UFBX_HAS_FTELLO
			#define UFBX_HAS_FTELLO
		#endif
	#endif
#endif

#if !defined(UFBX_STANDARD_C) && !defined(UFBX_NO_SSE) && (defined(_MSC_VER) && defined(_M_X64) && !defined(_M_ARM64EC)) || ((defined(__GNUC__) || defined(__clang__)) && defined(__x86_64__)) || defined(UFBX_USE_SSE)
	#define UFBXI_HAS_SSE 1
	#include <xmmintrin.h>
	#include <emmintrin.h>
#else
	#define UFBXI_HAS_SSE 0
#endif

// -- Atomic counter

#define UFBXI_THREAD_SAFE 1

#if defined(__cplusplus)
	#define ufbxi_extern_c extern "C"
#else
	#define ufbxi_extern_c
#endif

#if !defined(UFBX_STANDARD_C) && (defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER))
	typedef size_t ufbxi_atomic_counter;
	#define ufbxi_atomic_counter_init(ptr) (*(ptr) = 0)
	#define ufbxi_atomic_counter_free(ptr) (*(ptr) = 0)
	#define ufbxi_atomic_counter_inc(ptr) __sync_fetch_and_add((ptr), 1)
	#define ufbxi_atomic_counter_dec(ptr) __sync_fetch_and_sub((ptr), 1)
	#define ufbxi_atomic_counter_load(ptr) __sync_fetch_and_add((ptr), 0) // TODO: Proper atomic load
#elif !defined(UFBX_STANDARD_C) && defined(_MSC_VER)
	#if defined(_M_X64) || defined(_M_ARM64)
		ufbxi_extern_c __int64 _InterlockedIncrement64(__int64 volatile * lpAddend);
		ufbxi_extern_c __int64 _InterlockedDecrement64(__int64 volatile * lpAddend);
		ufbxi_extern_c __int64 _InterlockedExchangeAdd64(__int64 volatile * lpAddend, __int64 Value);
		typedef volatile __int64 ufbxi_atomic_counter;
		#define ufbxi_atomic_counter_init(ptr) (*(ptr) = 0)
		#define ufbxi_atomic_counter_free(ptr) (*(ptr) = 0)
		#define ufbxi_atomic_counter_inc(ptr) ((size_t)_InterlockedIncrement64(ptr) - 1)
		#define ufbxi_atomic_counter_dec(ptr) ((size_t)_InterlockedDecrement64(ptr) + 1)
		#define ufbxi_atomic_counter_load(ptr) ((size_t)_InterlockedExchangeAdd64((ptr), 0))
	#else
		ufbxi_extern_c long __cdecl _InterlockedIncrement(long volatile * lpAddend);
		ufbxi_extern_c long __cdecl _InterlockedDecrement(long volatile * lpAddend);
		ufbxi_extern_c long __cdecl _InterlockedExchangeAdd(long volatile * lpAddend, long Value);
		typedef volatile long ufbxi_atomic_counter;
		#define ufbxi_atomic_counter_init(ptr) (*(ptr) = 0)
		#define ufbxi_atomic_counter_free(ptr) (*(ptr) = 0)
		#define ufbxi_atomic_counter_inc(ptr) ((size_t)_InterlockedIncrement(ptr) - 1)
		#define ufbxi_atomic_counter_dec(ptr) ((size_t)_InterlockedDecrement(ptr) + 1)
		#define ufbxi_atomic_counter_load(ptr) ((size_t)_InterlockedExchangeAdd((ptr), 0))
	#endif
#elif !defined(UFBX_STANDARD_C) && defined(__TINYC__)
	#if defined(__x86_64__) || defined(_AMD64_)
		static size_t ufbxi_tcc_atomic_add(volatile size_t *dst, size_t value) {
			__asm__ __volatile__("lock; xaddq %0, %1;" : "+r" (value), "=m" (*dst) : "m" (dst));
			return value;
		}
	#elif defined(__i386__) || defined(_X86_)
		static size_t ufbxi_tcc_atomic_add(volatile size_t *dst, size_t value) {
			__asm__ __volatile__("lock; xaddl %0, %1;" : "+r" (value), "=m" (*dst) : "m" (dst));
			return value;
		}
	#else
		#error Unexpected TCC architecture
	#endif
	typedef volatile size_t ufbxi_atomic_counter;
	#define ufbxi_atomic_counter_init(ptr) (*(ptr) = 0)
	#define ufbxi_atomic_counter_free(ptr) (*(ptr) = 0)
	#define ufbxi_atomic_counter_inc(ptr) ufbxi_tcc_atomic_add((ptr), 1)
	#define ufbxi_atomic_counter_dec(ptr) ufbxi_tcc_atomic_add((ptr), SIZE_MAX)
	#define ufbxi_atomic_counter_load(ptr) ufbxi_tcc_atomic_add((ptr), 0)
#elif defined(__cplusplus) && (__cplusplus >= 201103L)
	#include <new>
	#include <atomic>
	typedef struct { alignas(std::atomic_size_t) char data[sizeof(std::atomic_size_t)]; } ufbxi_atomic_counter;
	#define ufbxi_atomic_counter_init(ptr) (new (&(ptr)->data) std::atomic_size_t(0))
	#define ufbxi_atomic_counter_free(ptr) (((std::atomic_size_t*)(ptr)->data)->~atomic())
	#define ufbxi_atomic_counter_inc(ptr) ((std::atomic_size_t*)(ptr)->data)->fetch_add(1)
	#define ufbxi_atomic_counter_dec(ptr) ((std::atomic_size_t*)(ptr)->data)->fetch_sub(1)
	#define ufbxi_atomic_counter_load(ptr) ((std::atomic_size_t*)(ptr)->data)->load(std::memory_order_acquire)
#elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && !defined(__STDC_NO_ATOMICS__)
	#include <stdatomic.h>
	typedef volatile atomic_size_t ufbxi_atomic_counter;
	#define ufbxi_atomic_counter_init(ptr) atomic_init(ptr, 0)
	#define ufbxi_atomic_counter_free(ptr) (void)(ptr)
	#define ufbxi_atomic_counter_inc(ptr) atomic_fetch_add((ptr), 1)
	#define ufbxi_atomic_counter_dec(ptr) atomic_fetch_sub((ptr), 1)
	#define ufbxi_atomic_counter_load(ptr) atomic_load_explicit((ptr), memory_order_acquire)
#else
	typedef volatile size_t ufbxi_atomic_counter;
	#define ufbxi_atomic_counter_init(ptr) (*(ptr) = 0)
	#define ufbxi_atomic_counter_free(ptr) (*(ptr) = 0)
	#define ufbxi_atomic_counter_inc(ptr) ((*(ptr))++)
	#define ufbxi_atomic_counter_dec(ptr) ((*(ptr))--)
	#define ufbxi_atomic_counter_load(ptr) (*(ptr))
	#undef UFBXI_THREAD_SAFE
	#define UFBXI_THREAD_SAFE 0
#endif

// ^^ No references to <string.h> before this point ^^
// vv No more includes past this point vv

#if defined(UFBX_STRING_PREFIX)
	#define ufbxi_string_fn(name) ufbxi_pre_cat(UFBX_STRING_PREFIX, name)
	#define strlen ufbxi_string_fn(strlen)
	#define memcpy ufbxi_string_fn(memcpy)
	#define memmove ufbxi_string_fn(memmove)
	#define memset ufbxi_string_fn(memset)
	#define memchr ufbxi_string_fn(memchr)
	#define memcmp ufbxi_string_fn(memcmp)
	#define strcmp ufbxi_string_fn(strcmp)
	#define strncmp ufbxi_string_fn(strncmp)
#endif

#if !defined(UFBX_LITTLE_ENDIAN)
	#if !defined(UFBX_STANDARD_C) && (defined(_M_IX86) || defined(__i386__) || defined(_M_X64) || defined(__x86_64__) || defined(_M_ARM64) || defined(__aarch64__) || defined(__wasm__) || defined(__EMSCRIPTEN__))
		#define UFBX_LITTLE_ENDIAN 1
	#else
		#define UFBX_LITTLE_ENDIAN 0
	#endif
#endif

// Unaligned little-endian load functions
// On platforms that support unaligned access natively (x86, x64, ARM64) just use normal loads,
// with unaligned attributes, otherwise do manual byte-wise load.

#define ufbxi_read_u8(ptr) (*(const uint8_t*)(ptr))

// Detect support for `__attribute__((aligned(1)))`
#if !defined(UFBX_STANDARD_C) && (defined(__clang__) && defined(__APPLE__))
	// Apple overrides Clang versioning, 5.0 here maps to 3.3
	#if __clang_major__ >= 5
		#define UFBXI_HAS_ATTRIBUTE_ALIGNED 1
	#endif
#elif !defined(UFBX_STANDARD_C) && defined(__clang__)
	#if (__clang_major__ >= 4) || (__clang_major__ == 3 && __clang_minor__ >= 3)
		#define UFBXI_HAS_ATTRIBUTE_ALIGNED 1
	#endif
#elif !defined(UFBX_STANDARD_C) && defined(__GNUC__)
	#if __GNUC__ >= 5
		#define UFBXI_HAS_ATTRIBUTE_ALIGNED 1
	#endif
#endif

#if defined(UFBXI_HAS_ATTRIBUTE_ALIGNED)
	#define UFBXI_HAS_UNALIGNED 1
	#define ufbxi_unaligned
	typedef uint16_t __attribute__((aligned(1))) ufbxi_unaligned_u16;
	typedef uint32_t __attribute__((aligned(1))) ufbxi_unaligned_u32;
	typedef uint64_t __attribute__((aligned(1))) ufbxi_unaligned_u64;
	typedef float __attribute__((aligned(1))) ufbxi_unaligned_f32;
	typedef double __attribute__((aligned(1))) ufbxi_unaligned_f64;
#elif !defined(UFBX_STANDARD_C) && defined(_MSC_VER)
	#define UFBXI_HAS_UNALIGNED 1
	#if defined(_M_IX86)
		// MSVC seems to assume all pointers are unaligned for x86
		#define ufbxi_unaligned
	#else
		#define ufbxi_unaligned __unaligned
	#endif
	typedef uint16_t ufbxi_unaligned_u16;
	typedef uint32_t ufbxi_unaligned_u32;
	typedef uint64_t ufbxi_unaligned_u64;
	typedef float ufbxi_unaligned_f32;
	typedef double ufbxi_unaligned_f64;
#endif

#if (defined(UFBXI_HAS_UNALIGNED) && UFBX_LITTLE_ENDIAN && !defined(UFBX_NO_UNALIGNED_LOADS)) || defined(UFBX_USE_UNALIGNED_LOADS)
	#define ufbxi_read_u16(ptr) (*(const ufbxi_unaligned ufbxi_unaligned_u16*)(ptr))
	#define ufbxi_read_u32(ptr) (*(const ufbxi_unaligned ufbxi_unaligned_u32*)(ptr))
	#define ufbxi_read_u64(ptr) (*(const ufbxi_unaligned ufbxi_unaligned_u64*)(ptr))
	#define ufbxi_read_f32(ptr) (*(const ufbxi_unaligned ufbxi_unaligned_f32*)(ptr))
	#define ufbxi_read_f64(ptr) (*(const ufbxi_unaligned ufbxi_unaligned_f64*)(ptr))
#else
	static ufbxi_forceinline uint16_t ufbxi_read_u16(const void *ptr) {
		const char *p = (const char*)ptr;
		return (uint16_t)(
			(unsigned)(uint8_t)p[0] << 0u |
			(unsigned)(uint8_t)p[1] << 8u );
	}
	static ufbxi_forceinline uint32_t ufbxi_read_u32(const void *ptr) {
		const char *p = (const char*)ptr;
		return (uint32_t)(
			(unsigned)(uint8_t)p[0] <<  0u |
			(unsigned)(uint8_t)p[1] <<  8u |
			(unsigned)(uint8_t)p[2] << 16u |
			(unsigned)(uint8_t)p[3] << 24u );
	}
	static ufbxi_forceinline uint64_t ufbxi_read_u64(const void *ptr) {
		const char *p = (const char*)ptr;
		return (uint64_t)(
			(uint64_t)(uint8_t)p[0] <<  0u |
			(uint64_t)(uint8_t)p[1] <<  8u |
			(uint64_t)(uint8_t)p[2] << 16u |
			(uint64_t)(uint8_t)p[3] << 24u |
			(uint64_t)(uint8_t)p[4] << 32u |
			(uint64_t)(uint8_t)p[5] << 40u |
			(uint64_t)(uint8_t)p[6] << 48u |
			(uint64_t)(uint8_t)p[7] << 56u );
	}
	static ufbxi_forceinline float ufbxi_read_f32(const void *ptr) {
		uint32_t u = ufbxi_read_u32(ptr);
		float f;
		memcpy(&f, &u, 4);
		return f;
	}
	static ufbxi_forceinline double ufbxi_read_f64(const void *ptr) {
		uint64_t u = ufbxi_read_u64(ptr);
		double f;
		memcpy(&f, &u, 8);
		return f;
	}
#endif

#define ufbxi_read_i8(ptr) (int8_t)(ufbxi_read_u8(ptr))
#define ufbxi_read_i16(ptr) (int16_t)(ufbxi_read_u16(ptr))
#define ufbxi_read_i32(ptr) (int32_t)(ufbxi_read_u32(ptr))
#define ufbxi_read_i64(ptr) (int64_t)(ufbxi_read_u64(ptr))

ufbx_static_assert(sizeof_bool, sizeof(bool) == 1);
ufbx_static_assert(sizeof_char, sizeof(char) == 1);
ufbx_static_assert(sizeof_i8, sizeof(int8_t) == 1);
ufbx_static_assert(sizeof_i16, sizeof(int16_t) == 2);
ufbx_static_assert(sizeof_i32, sizeof(int32_t) == 4);
ufbx_static_assert(sizeof_i64, sizeof(int64_t) == 8);
ufbx_static_assert(sizeof_u8, sizeof(uint8_t) == 1);
ufbx_static_assert(sizeof_u16, sizeof(uint16_t) == 2);
ufbx_static_assert(sizeof_u32, sizeof(uint32_t) == 4);
ufbx_static_assert(sizeof_u64, sizeof(uint64_t) == 8);
ufbx_static_assert(sizeof_f32, sizeof(float) == 4);
ufbx_static_assert(sizeof_f64, sizeof(double) == 8);

// -- Version

#define UFBX_SOURCE_VERSION ufbx_pack_version(0, 15, 0)
ufbx_abi_data_def const uint32_t ufbx_source_version = UFBX_SOURCE_VERSION;

ufbx_static_assert(source_header_version, UFBX_SOURCE_VERSION/1000u == UFBX_HEADER_VERSION/1000u);

// -- Fast copy

#if UFBXI_HAS_SSE
	#define ufbxi_copy_16_bytes(dst, src) _mm_storeu_si128((__m128i*)(dst), _mm_loadu_si128((const __m128i*)(src)))
#elif defined(UFBXI_HAS_UNALIGNED)
	#define ufbxi_copy_16_bytes(dst, src) do { \
		ufbxi_unaligned ufbxi_unaligned_u64 *mi_dst = (ufbxi_unaligned ufbxi_unaligned_u64 *)(dst); \
		const ufbxi_unaligned ufbxi_unaligned_u64 *mi_src = (const ufbxi_unaligned ufbxi_unaligned_u64 *)src; \
		mi_dst[0] = mi_src[0]; \
		mi_dst[1] = mi_src[1]; \
	} while (0)
#else
	#define ufbxi_copy_16_bytes(dst, src) memcpy((dst), (src), 16)
#endif


// -- Large fast integer

#if !defined(UFBX_STANDARD_C) && (defined(__wasm__) || defined(__EMSCRIPTEN__)) && !defined(UFBX_WASM_32BIT)
	typedef uint64_t ufbxi_fast_uint;
#else
	typedef size_t ufbxi_fast_uint;
#endif

// -- Wrapping right shift

#if !defined(UFBX_STANDARD_C) && defined(_MSC_VER) && defined(_M_X64)
	#define ufbxi_wrap_shr64(a, b) ((a) >> (b))
#else
	#define ufbxi_wrap_shr64(a, b) ((a) >> ((b) & 63))
#endif

// -- Bit manipulation

#if !defined(UFBX_STANDARD_C) && defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86))
	ufbxi_extern_c unsigned char _BitScanReverse(unsigned long * _Index, unsigned long _Mask);
	ufbxi_extern_c unsigned char _BitScanReverse64(unsigned long * _Index, unsigned __int64 _Mask);
	static ufbxi_forceinline ufbxi_unused uint32_t ufbxi_lzcnt32(uint32_t v) {
		unsigned long index;
		_BitScanReverse(&index, (unsigned long)v);
		return 31 - (uint32_t)index;
	}
	static ufbxi_forceinline ufbxi_unused uint32_t ufbxi_lzcnt64(uint64_t v) {
		unsigned long index;
		#if defined(_M_X64)
			_BitScanReverse64(&index, (unsigned __int64)v);
		#else
			uint32_t hi = (uint32_t)(v >> 32u);
			uint32_t hi_nonzero = hi != 0 ? 1 : 0;
			uint32_t part = hi_nonzero ? hi : (uint32_t)v;
			_BitScanReverse(&index, (unsigned long)part);
			index += hi_nonzero * 32u;
		#endif
		return 63 - (uint32_t)index;
	}
#elif !defined(UFBX_STANDARD_C) && (defined(__GNUC__) || defined(__clang__))
	#define ufbxi_lzcnt32(v) ((uint32_t)__builtin_clz((unsigned)(v)))
	#define ufbxi_lzcnt64(v) ((uint32_t)__builtin_clzll((unsigned long long)(v)))
#else
	// DeBrujin table lookup
	static const uint8_t ufbxi_lzcnt32_table[] =  {
		31, 22, 30, 21, 18, 10, 29, 2, 20, 17, 15, 13, 9, 6, 28, 1, 23, 19, 11, 3, 16, 14, 7, 24, 12, 4, 8, 25, 5, 26, 27, 0,
	};
	static const uint8_t ufbxi_lzcnt64_table[] = {
		63, 16, 62, 7, 15, 36, 61, 3, 6, 14, 22, 26, 35, 47, 60, 2, 9, 5, 28, 11, 13, 21, 42,
		19, 25, 31, 34, 40, 46, 52, 59, 1, 17, 8, 37, 4, 23, 27, 48, 10, 29, 12, 43, 20, 32, 41,
		53, 18, 38, 24, 49, 30, 44, 33, 54, 39, 50, 45, 55, 51, 56, 57, 58, 0,
	};
	static ufbxi_noinline ufbxi_unused uint32_t ufbxi_lzcnt32(uint32_t v) {
		v |= v >> 1;
		v |= v >> 2;
		v |= v >> 4;
		v |= v >> 8;
		v |= v >> 16;
		return ufbxi_lzcnt32_table[(v * 0x07c4acddu) >> 27];
	}
	static ufbxi_noinline ufbxi_unused uint32_t ufbxi_lzcnt64(uint64_t v) {
		v |= v >> 1;
		v |= v >> 2;
		v |= v >> 4;
		v |= v >> 8;
		v |= v >> 16;
		v |= v >> 32;
		return ufbxi_lzcnt64_table[(v * UINT64_C(0x03f79d71b4cb0a89)) >> 58];
	}
#endif

// -- Bit conversion

#if defined(__cplusplus)
	#define ufbxi_bit_cast(m_dst_type, m_dst, m_src_type, m_src) memcpy(&(m_dst), &(m_src), sizeof(m_dst_type))
#else
	#define ufbxi_bit_cast(m_dst_type, m_dst, m_src_type, m_src) do { \
		union { m_dst_type mi_dst; m_src_type mi_src; } mi_union; \
		mi_union.mi_src = (m_src); (m_dst) = mi_union.mi_dst; } while (0)
#endif

// -- Debug

#if defined(UFBX_DEBUG_BINARY_SEARCH) || defined(UFBX_REGRESSION)
	#define ufbxi_clamp_linear_threshold(v) (2)
#else
	#define ufbxi_clamp_linear_threshold(v) (v)
#endif

#if defined(UFBX_REGRESSION)
	#undef UFBXI_MAX_SKIP_SIZE
	#define UFBXI_MAX_SKIP_SIZE 128

	#undef UFBXI_MAP_MAX_SCAN
	#define UFBXI_MAP_MAX_SCAN 2

	#undef UFBXI_KD_FAST_DEPTH
	#define UFBXI_KD_FAST_DEPTH 2

	#undef UFBXI_FACE_GROUP_HASH_BITS
	#define UFBXI_FACE_GROUP_HASH_BITS 2
#endif

#if defined(UFBX_REGRESSION) || defined(UFBX_EXTENSIVE_THREADING)
	#undef UFBXI_MIN_THREADED_DEFLATE_BYTES
	#define UFBXI_MIN_THREADED_DEFLATE_BYTES 2

	#undef UFBXI_MIN_THREADED_ASCII_VALUES
	#define UFBXI_MIN_THREADED_ASCII_VALUES 2
#endif

#if defined(UFBX_REGRESSION)
	#define ufbxi_regression_assert(cond) ufbx_assert(cond)
#else
	#define ufbxi_regression_assert(cond) (void)0
#endif

#if defined(UFBX_REGRESSION) || defined(UFBX_DEV)
	#define ufbxi_dev_assert(cond) ufbx_assert(cond)
#else
	#define ufbxi_dev_assert(cond) (void)0
#endif

#define ufbxi_unreachable(reason) do { ufbx_assert(0 && reason); } while (0)

#if defined(UFBX_REGRESSION)
	#define UFBXI_IS_REGRESSION 1
#else
	#define UFBXI_IS_REGRESSION 0
#endif

#if defined(_MSC_VER)
	#define ufbxi_thread_local __declspec(thread)
#elif defined(__GNUC__) || defined(__clang__)
	#define ufbxi_thread_local __thread
#elif UFBXI_HAS_CPP11
	#define ufbxi_thread_local thread_local
#elif UFBX_STDC >= 201112L
	#define ufbxi_thread_local _Thread_local
#endif

#if defined(UFBXI_ANALYSIS_RECURSIVE)
	#define ufbxi_recursive_function(m_ret, m_name, m_args, m_max_depth, m_params) UFBXI_RECURSIVE_FUNCTION(m_name, m_max_depth);
	#define ufbxi_recursive_function_void(m_name, m_args, m_max_depth, m_params) UFBXI_RECURSIVE_FUNCTION(m_name, m_max_depth);
#elif UFBXI_IS_REGRESSION && defined(ufbxi_thread_local)
	#define ufbxi_recursive_function(m_ret, m_name, m_args, m_max_depth, m_params) \
		{ \
			m_ret m_name##_rec m_params; \
			static ufbxi_thread_local unsigned ufbxi_recursion_depth; \
			ufbx_assert(ufbxi_recursion_depth < m_max_depth); \
			++ufbxi_recursion_depth; \
			m_ret ret = m_name##_rec m_args; \
			--ufbxi_recursion_depth; \
			return ret; \
		} \
		m_ret m_name##_rec m_params
	#define ufbxi_recursive_function_void(m_name, m_args, m_max_depth, m_params) \
		{ \
			void m_name##_rec m_params; \
			static ufbxi_thread_local unsigned ufbxi_recursion_depth; \
			ufbx_assert(ufbxi_recursion_depth < m_max_depth); \
			++ufbxi_recursion_depth; \
			m_name##_rec m_args; \
			--ufbxi_recursion_depth; \
		} \
		void m_name##_rec m_params
#else
	#define ufbxi_recursive_function(m_ret, m_name, m_args, m_max_depth, m_params)
	#define ufbxi_recursive_function_void(m_name, m_args, m_max_depth, m_params)
#endif

// -- Utility

#if defined(UFBX_UBSAN)
	static void ufbxi_assert_zero(size_t offset) { ufbx_assert(offset == 0); }
	#define ufbxi_add_ptr(ptr, offset) ((ptr) ? (ptr) + (offset) : (ufbxi_assert_zero((size_t)(offset)), (ptr)))
	#define ufbxi_sub_ptr(ptr, offset) ((ptr) ? (ptr) - (offset) : (ufbxi_assert_zero((size_t)(offset)), (ptr)))
#else
	#define ufbxi_add_ptr(ptr, offset) ((ptr) + (offset))
	#define ufbxi_sub_ptr(ptr, offset) ((ptr) - (offset))
#endif

#define ufbxi_arraycount(arr) (sizeof(arr) / sizeof(*(arr)))
#define ufbxi_for(m_type, m_name, m_begin, m_num) for (m_type *m_name = m_begin, *m_name##_end = ufbxi_add_ptr(m_name, m_num); m_name != m_name##_end; m_name++)
#define ufbxi_for_ptr(m_type, m_name, m_begin, m_num) for (m_type **m_name = m_begin, **m_name##_end = ufbxi_add_ptr(m_name, m_num); m_name != m_name##_end; m_name++)

// WARNING: Evaluates `m_list` twice!
#define ufbxi_for_list(m_type, m_name, m_list) for (m_type *m_name = (m_list).data, *m_name##_end = ufbxi_add_ptr(m_name, (m_list).count); m_name != m_name##_end; m_name++)
#define ufbxi_for_ptr_list(m_type, m_name, m_list) for (m_type **m_name = (m_list).data, **m_name##_end = ufbxi_add_ptr(m_name, (m_list).count); m_name != m_name##_end; m_name++)

#define ufbxi_string_literal(str) { str, sizeof(str) - 1 }

static ufbxi_forceinline uint32_t ufbxi_min32(uint32_t a, uint32_t b) { return a < b ? a : b; }
static ufbxi_forceinline uint32_t ufbxi_max32(uint32_t a, uint32_t b) { return a < b ? b : a; }
static ufbxi_forceinline uint64_t ufbxi_min64(uint64_t a, uint64_t b) { return a < b ? a : b; }
static ufbxi_forceinline uint64_t ufbxi_max64(uint64_t a, uint64_t b) { return a < b ? b : a; }
static ufbxi_forceinline size_t ufbxi_min_sz(size_t a, size_t b) { return a < b ? a : b; }
static ufbxi_forceinline size_t ufbxi_max_sz(size_t a, size_t b) { return a < b ? b : a; }
static ufbxi_forceinline ufbx_real ufbxi_min_real(ufbx_real a, ufbx_real b) { return a < b ? a : b; }
static ufbxi_forceinline ufbx_real ufbxi_max_real(ufbx_real a, ufbx_real b) { return a < b ? b : a; }

static ufbxi_forceinline int32_t ufbxi_f64_to_i32(double value)
{
	if (ufbx_fabs(value) <= (double)INT32_MAX) {
		return (int32_t)value;
	} else {
		return value >= 0.0 ? INT32_MAX : INT32_MIN;
	}
}

static ufbxi_forceinline int64_t ufbxi_f64_to_i64(double value)
{
	if (ufbx_fabs(value) <= (double)INT64_MAX) {
		return (int64_t)value;
	} else {
		return value >= 0.0 ? INT64_MAX : INT64_MIN;
	}
}

#if defined(UFBX_REGRESSION)
	static size_t ufbxi_to_size(ptrdiff_t delta) {
		ufbx_assert(delta >= 0);
		return (size_t)delta;
	}
#else
	#define ufbxi_to_size(delta) ((size_t)(delta))
#endif

// Stable sort array `m_type m_data[m_size]` using the predicate `m_cmp_lambda(a, b)`
// `m_linear_size` is a hint for how large blocks handle initially do with insertion sort
// `m_tmp` must be a memory buffer with at least the same size and alignment as `m_data`
#define ufbxi_macro_stable_sort(m_type, m_linear_size, m_data, m_tmp, m_size, m_cmp_lambda) do { \
	typedef m_type mi_type; \
	mi_type *mi_src = (mi_type*)(m_tmp); \
	mi_type *mi_data = m_data, *mi_dst = mi_data; \
	size_t mi_block_size = ufbxi_clamp_linear_threshold(m_linear_size), mi_size = m_size; \
	/* Insertion sort in `m_linear_size` blocks */ \
	for (size_t mi_base = 0; mi_base < mi_size; mi_base += mi_block_size) { \
		size_t mi_i_end = mi_base + mi_block_size; \
		if (mi_i_end > mi_size) mi_i_end = mi_size; \
		for (size_t mi_i = mi_base + 1; mi_i < mi_i_end; mi_i++) { \
			size_t mi_j = mi_i; \
			mi_src[0] = mi_dst[mi_i]; \
			for (; mi_j != mi_base; --mi_j) { \
				mi_type *a = &mi_src[0], *b = &mi_dst[mi_j - 1]; \
				if (!( m_cmp_lambda )) break; \
				mi_dst[mi_j] = mi_dst[mi_j - 1]; \
			} \
			mi_dst[mi_j] = mi_src[0]; \
		} \
	} \
	/* Merge sort ping-ponging between `m_data` and `m_tmp` */ \
	for (; mi_block_size < mi_size; mi_block_size *= 2) { \
		mi_type *mi_swap = mi_dst; mi_dst = mi_src; mi_src = mi_swap; \
		for (size_t mi_base = 0; mi_base < mi_size; mi_base += mi_block_size * 2) { \
			size_t mi_i = mi_base, mi_i_end = mi_base + mi_block_size; \
			size_t mi_j = mi_i_end, mi_j_end = mi_j + mi_block_size; \
			size_t mi_k = mi_base; \
			if (mi_i_end > mi_size) mi_i_end = mi_size; \
			if (mi_j_end > mi_size) mi_j_end = mi_size; \
			while ((mi_i < mi_i_end) & (mi_j < mi_j_end)) { \
				mi_type *a = &mi_src[mi_j], *b = &mi_src[mi_i]; \
				if ( m_cmp_lambda ) { \
					mi_dst[mi_k] = *a; mi_j++; \
				} else { \
					mi_dst[mi_k] = *b; mi_i++; \
				} \
				mi_k++; \
			} \
			while (mi_i < mi_i_end) mi_dst[mi_k++] = mi_src[mi_i++]; \
			while (mi_j < mi_j_end) mi_dst[mi_k++] = mi_src[mi_j++]; \
		} \
	} \
	/* Copy the result to `m_data` if we ended up in `m_tmp` */ \
	if (mi_dst != mi_data) memcpy((void*)mi_data, mi_dst, sizeof(mi_type) * mi_size); \
	} while (0)

#define ufbxi_macro_lower_bound_eq(m_type, m_linear_size, m_result_ptr, m_data, m_begin, m_size, m_cmp_lambda, m_eq_lambda) do { \
	typedef m_type mi_type; \
	const mi_type *mi_data = (m_data); \
	size_t mi_lo = m_begin, mi_hi = m_size, mi_linear_size = ufbxi_clamp_linear_threshold(m_linear_size); \
	ufbx_assert(mi_linear_size > 1); \
	/* Binary search until we get down to `m_linear_size` elements */ \
	while (mi_hi - mi_lo > mi_linear_size) { \
		size_t mi_mid = mi_lo + (mi_hi - mi_lo) / 2; \
		const mi_type *a = &mi_data[mi_mid]; \
		if ( m_cmp_lambda ) { mi_lo = mi_mid + 1; } else { mi_hi = mi_mid + 1; } \
	} \
	/* Linearly scan until we find the edge */ \
	for (; mi_lo < mi_hi; mi_lo++) { \
		const mi_type *a = &mi_data[mi_lo]; \
		if ( m_eq_lambda ) { *(m_result_ptr) = mi_lo; break; } \
	} \
	} while (0)

#define ufbxi_macro_upper_bound_eq(m_type, m_linear_size, m_result_ptr, m_data, m_begin, m_size, m_eq_lambda) do { \
	typedef m_type mi_type; \
	const mi_type *mi_data = (m_data); \
	size_t mi_lo = m_begin, mi_hi = m_size, mi_linear_size = ufbxi_clamp_linear_threshold(m_linear_size); \
	ufbx_assert(mi_linear_size > 1); \
	/* Linearly scan with galloping */ \
	for (size_t mi_step = 1; mi_step < 100 && mi_hi - mi_lo > mi_step; mi_step *= 2) { \
		const mi_type *a = &mi_data[mi_lo + mi_step]; \
		if (!( m_eq_lambda )) { mi_hi = mi_lo + mi_step; break; } \
		mi_lo += mi_step; \
	} \
	/* Binary search until we get down to `m_linear_size` elements */ \
	while (mi_hi - mi_lo > mi_linear_size) { \
		size_t mi_mid = mi_lo + (mi_hi - mi_lo) / 2; \
		const mi_type *a = &mi_data[mi_mid]; \
		if ( m_eq_lambda ) { mi_lo = mi_mid + 1; } else { mi_hi = mi_mid + 1; } \
	} \
	/* Linearly scan until we find the edge */ \
	for (; mi_lo < mi_hi; mi_lo++) { \
		const mi_type *a = &mi_data[mi_lo]; \
		if (!( m_eq_lambda )) break; \
	} \
	*(m_result_ptr) = mi_lo; \
	} while (0)

typedef bool ufbxi_less_fn(void *user, const void *a, const void *b);

static ufbxi_noinline void ufbxi_stable_sort(size_t stride, size_t linear_size, void *in_data, void *in_tmp, size_t size, ufbxi_less_fn *less_fn, void *less_user)
{
	(void)linear_size;

	char *src = (char*)in_tmp;
	char *data = (char*)in_data, *dst = (char*)data;
	size_t block_size = ufbxi_clamp_linear_threshold(linear_size);
	/* Insertion sort in `linear_size` blocks */
	for (size_t base = 0; base < size; base += block_size) {
		size_t i_end = base + block_size;
		if (i_end > size) i_end = size;
		for (size_t i = base + 1; i < i_end; i++) {

			{
				char *a = dst + i * stride, *b = dst + (i - 1) * stride;
				if (!less_fn(less_user, a, b)) continue;
			}

			size_t j = i - 1;
			memcpy(src, dst + i * stride, stride);
			memcpy(dst + i * stride, dst + j * stride, stride);
			for (; j != base; --j) {
				char *a = src, *b = dst + (j - 1) * stride;
				if (!less_fn(less_user, a, b)) break;
				memcpy(dst + j * stride, dst + (j - 1) * stride, stride);
			}
			memcpy(dst + j * stride, src, stride);
		}
	}
	/* Merge sort ping-ponging between `data` and `tmp` */
	for (; block_size < size; block_size *= 2) {
		char *swap = dst; dst = src; src = swap;
		for (size_t base = 0; base < size; base += block_size * 2) {
			size_t i = base, i_end = base + block_size;
			size_t j = i_end, j_end = j + block_size;
			size_t k = base;
			if (i_end > size) i_end = size;
			if (j_end > size) j_end = size;
			while ((i < i_end) & (j < j_end)) {
				char *a = src + j * stride, *b = src + i * stride;
				if (less_fn(less_user, a, b)) {
					memcpy(dst + k * stride, a, stride);
					j++;
				} else {
					memcpy(dst + k * stride, b, stride);
					i++;
				}
				k++;
			}

			memcpy(dst + k * stride, src + i * stride, (i_end - i) * stride);
			if (j < j_end) {
				memcpy(dst + (k + (i_end - i)) * stride, src + j * stride, (j_end - j) * stride);
			}
		}
	}
	/* Copy the result to `data` if we ended up in `tmp` */
	if (dst != data) memcpy((void*)data, dst, size * stride);
}

static ufbxi_forceinline void ufbxi_swap(void *a, void *b, size_t size)
{
	char *ca = (char*)a, *cb = (char*)b;
#if defined(UFBXI_HAS_UNALIGNED)
	ufbxi_nounroll while (size >= 4) {
		uint32_t t = *(ufbxi_unaligned ufbxi_unaligned_u32*)ca;
		*(ufbxi_unaligned ufbxi_unaligned_u32*)ca = *(ufbxi_unaligned ufbxi_unaligned_u32*)cb;
		*(ufbxi_unaligned ufbxi_unaligned_u32*)cb = t;
		ca += 4; cb += 4; size -= 4;
	}
#endif
	ufbxi_nounroll while (size > 0) {
		char t = *ca; *ca = *cb; *cb = t;
		ca++; cb++; size--;
	}
}

static ufbxi_noinline void ufbxi_unstable_sort(void *in_data, size_t size, size_t stride, ufbxi_less_fn *less_fn, void *less_user)
{
	if (size <= 1) return;
	char *data = (char*)in_data;
	size_t start = (size - 1) >> 1;
	size_t end = size - 1;
	for (;;) {
		size_t root = start;
		size_t child;
		while ((child = root*2 + 1) <= end) {
			size_t next = less_fn(less_user, data + child * stride, data + root * stride) ? root : child;
			if (child + 1 <= end && less_fn(less_user, data + next * stride, data + (child + 1) * stride)) {
				next = child + 1;
			}
			if (next == root) break;
			ufbxi_swap(data + root * stride, data + next * stride, stride);
			root = next;
		}

		if (start > 0) {
			start--;
		} else if (end > 0) {
			ufbxi_swap(data + end * stride, data, stride);
			end--;
		} else {
			break;
		}
	}
}

// -- Float parsing

#define UFBXI_BIGINT_LIMB_BITS 32
#define UFBXI_BIGINT_ACCUM_BITS (UFBXI_BIGINT_LIMB_BITS * 2)
#define UFBXI_BIGINT_LIMB_MAX (ufbxi_bigint_limb)(((ufbxi_bigint_accum)1 << UFBXI_BIGINT_LIMB_BITS) - 1)
typedef uint32_t ufbxi_bigint_limb;
typedef uint64_t ufbxi_bigint_accum;

typedef struct {
	ufbxi_bigint_limb *limbs;
	uint32_t capacity;
	uint32_t length;
} ufbxi_bigint;

static ufbxi_bigint ufbxi_bigint_make(ufbxi_bigint_limb *limbs, size_t capacity)
{
	ufbxi_bigint bi = { limbs, (uint32_t)capacity };
	return bi;
}

#define ufbxi_bigint_array(arr) ufbxi_bigint_make((arr), sizeof(arr) / sizeof(*(arr)))

static const uint64_t ufbxi_pow5_tab[] = {
	UINT64_C(0x1), UINT64_C(0x5), UINT64_C(0x19), UINT64_C(0x7d), UINT64_C(0x271), UINT64_C(0xc35), UINT64_C(0x3d09), UINT64_C(0x1312d), UINT64_C(0x5f5e1),
	UINT64_C(0x1dcd65), UINT64_C(0x9502f9), UINT64_C(0x2e90edd), UINT64_C(0xe8d4a51), UINT64_C(0x48c27395), UINT64_C(0x16bcc41e9), UINT64_C(0x71afd498d),
	UINT64_C(0x2386f26fc1), UINT64_C(0xb1a2bc2ec5), UINT64_C(0x3782dace9d9), UINT64_C(0x1158e460913d), UINT64_C(0x56bc75e2d631), UINT64_C(0x1b1ae4d6e2ef5),
	UINT64_C(0x878678326eac9), UINT64_C(0x2a5a058fc295ed), UINT64_C(0xd3c21bcecceda1), UINT64_C(0x422ca8b0a00a425), UINT64_C(0x14adf4b7320334b9), UINT64_C(0x6765c793fa10079d),
};

static const double ufbxi_pow10_tab_f64[] = {
	1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22,
};

static ufbxi_noinline void ufbxi_bigint_mad(ufbxi_bigint *bigint, ufbxi_bigint_accum multiplicand, ufbxi_bigint_accum addend)
{
	ufbxi_dev_assert((multiplicand | addend) >> (UFBXI_BIGINT_ACCUM_BITS - 1) == 0);
	ufbxi_bigint b = *bigint;
	ufbxi_bigint_limb m_lo = (ufbxi_bigint_limb)multiplicand;
	ufbxi_bigint_limb m_hi = (ufbxi_bigint_limb)(multiplicand >> UFBXI_BIGINT_LIMB_BITS);
	ufbxi_bigint_accum carry = addend;
	for (uint32_t i = 0; i < b.length; i++) {
		ufbxi_bigint_accum limb = (ufbxi_bigint_accum)b.limbs[i];
		ufbxi_bigint_accum lo = limb * m_lo + (carry & UFBXI_BIGINT_LIMB_MAX);
		ufbxi_bigint_accum hi = limb * m_hi;
		b.limbs[i] = (ufbxi_bigint_limb)lo;
		carry = (carry >> 32u) + (lo >> 32u) + hi;
	}
	while (carry) {
		b.limbs[b.length++] = (ufbxi_bigint_limb)carry;
		ufbxi_dev_assert(b.length < b.capacity);
		carry >>= 32u;
	}
	bigint->length = b.length;
}

static ufbxi_noinline bool ufbxi_bigint_div(ufbxi_bigint *q, ufbxi_bigint *u, ufbxi_bigint *v)
{
	int32_t n = (int32_t)v->length;
	int32_t m = (int32_t)u->length - n;
	ufbxi_bigint_limb v_hi = v->limbs[v->length - 1];
	ufbxi_bigint_limb *un = u->limbs, *vn = v->limbs;
	ufbxi_dev_assert(n >= 2 && m >= 1 && v_hi >> (UFBXI_BIGINT_LIMB_BITS - 1) != 0 && un[n+m - 1] >> (UFBXI_BIGINT_LIMB_BITS - 1) == 0);
	un[n + m] = 0;
	q->length = 0;
	for (int32_t j = m - 1; j >= 0; j--) {
		ufbxi_bigint_accum u_hi = ((ufbxi_bigint_accum)un[n+j] << UFBXI_BIGINT_LIMB_BITS) | un[n+j-1];
		ufbxi_bigint_accum t, qhat = u_hi / v_hi, rhat = u_hi % v_hi;
		while (qhat >> UFBXI_BIGINT_LIMB_BITS != 0 || qhat*vn[n-2] > ((rhat<<UFBXI_BIGINT_LIMB_BITS)|un[j+n-2])) {
			qhat -= 1;
			rhat += v_hi;
			if (rhat >> UFBXI_BIGINT_LIMB_BITS != 0) break;
		}
		ufbxi_bigint_limb carry = 0;
		for (int32_t i = 0; i < n; i++) {
			ufbxi_bigint_accum p = qhat * vn[i];
			t = (ufbxi_bigint_accum)un[i+j] - carry - (ufbxi_bigint_limb)p;
			un[i+j] = (ufbxi_bigint_limb)t;
			carry = (ufbxi_bigint_limb)((p >> UFBXI_BIGINT_LIMB_BITS) - (t >> UFBXI_BIGINT_LIMB_BITS));
		}
		t = (ufbxi_bigint_accum)un[j+n] - carry;
		un[j+n] = (ufbxi_bigint_limb)t;
		if (t >> UFBXI_BIGINT_LIMB_BITS != 0) {
			qhat -= 1;
			carry = 0;
			for (int32_t i = 0; i < n; i++) {
				t = (ufbxi_bigint_accum)un[i+j] + vn[i] + carry;
				un[i+j] = (ufbxi_bigint_limb)t;
				carry = (ufbxi_bigint_limb)(t >> UFBXI_BIGINT_LIMB_BITS);
			}
			un[j+n] += carry;
		}
		q->limbs[j] = (ufbxi_bigint_limb)qhat;
		if (qhat && !q->length) {
			ufbxi_dev_assert(j + 1 < (int32_t)q->capacity);
			q->length = (uint32_t)(j + 1);
		}
	}
	for (int32_t i = 0; i < n; i++) {
		if (un[i]) return true;
	}
	return false;
}

static void ufbxi_bigint_mul_pow5(ufbxi_bigint *b, uint32_t power)
{
	for (; power > 27; power -= 27) {
		ufbxi_bigint_mad(b, ufbxi_pow5_tab[27], 0);
	}
	ufbxi_bigint_mad(b, ufbxi_pow5_tab[power], 0);
}

static ufbxi_noinline void ufbxi_bigint_shift_left(ufbxi_bigint *bigint, uint32_t amount)
{
	uint32_t words = amount / UFBXI_BIGINT_LIMB_BITS, bits = amount % UFBXI_BIGINT_LIMB_BITS;
	ufbxi_bigint b = *bigint;
	ufbxi_dev_assert(b.length + words + 1 < b.capacity && b.capacity >= 4);
	uint32_t bits_down = UFBXI_BIGINT_LIMB_BITS - bits - 1;
	bigint->length += words + (b.limbs[b.length - 1] >> 1 >> bits_down != 0 ? 1 : 0);
	b.limbs[b.length] = 0;
	if (b.length <= 3 && words <= 3) {
		ufbxi_bigint_limb l0 = ufbxi_maybe_uninit(b.length >= 0, b.limbs[0], ~0u);
		ufbxi_bigint_limb l1 = ufbxi_maybe_uninit(b.length >= 1, b.limbs[1], ~0u);
		ufbxi_bigint_limb l2 = ufbxi_maybe_uninit(b.length >= 2, b.limbs[2], ~0u);
		b.limbs[0] = 0;
		b.limbs[1] = 0;
		b.limbs[2] = 0;
		b.limbs[words + 0] = l0 << bits;
		b.limbs[words + 1] = (l1 << bits) | (l0 >> 1 >> bits_down);
		b.limbs[words + 2] = (l2 << bits) | (l1 >> 1 >> bits_down);
		b.limbs[words + 3] = (l2 >> 1 >> bits_down);
	} else {
		for (uint32_t i = b.length + 1; i-- > 1; ) {
			b.limbs[i + words] = (b.limbs[i] << bits) | (b.limbs[i - 1] >> 1 >> bits_down);
		}
		b.limbs[words] = b.limbs[0] << bits;
		for (uint32_t i = 0; i < words; i++) {
			b.limbs[i] = 0;
		}
	}
}

static ufbxi_bigint_limb ufbxi_bigint_top_limb(const ufbxi_bigint b, uint32_t index) {
	return index < b.length ? b.limbs[b.length - 1 - index] : 0;
}

static ufbxi_noinline uint64_t ufbxi_bigint_extract_high(const ufbxi_bigint b, int32_t *p_exponent, bool *p_tail)
{
	ufbxi_dev_assert(b.length != 0);
	uint64_t result = 0;
	const uint32_t limb_count = 64 / UFBXI_BIGINT_LIMB_BITS;
	for (uint32_t i = 0; i < limb_count; i++) {
		result = (result << UFBXI_BIGINT_LIMB_BITS) | ufbxi_bigint_top_limb(b, i);
	}
	uint32_t shift = ufbxi_lzcnt64(result);
	result <<= shift;
	ufbxi_bigint_limb lo = ufbxi_bigint_top_limb(b, limb_count);
	if (shift > 0) {
		result |= lo >> (UFBXI_BIGINT_LIMB_BITS - shift);
	}
	*p_tail |= (ufbxi_bigint_limb)(lo << shift) != 0;
	for (uint32_t i = limb_count + 1; i < b.length; i++) {
		*p_tail |= ufbxi_bigint_top_limb(b, i) != 0;
	}
	*p_exponent += (int32_t)(b.length * UFBXI_BIGINT_LIMB_BITS - shift - 1);
	return result;
}

static uint64_t ufbxi_shift_right_round(uint64_t value, uint32_t shift, bool tail)
{
	if (shift == 0) return value;
	if (shift > 64) return 0;
	uint64_t result = value >> (shift - 1);
	uint64_t tail_mask = (UINT64_C(1) << (shift - 1)) - 1;

	bool r_odd = (result & 0x2) != 0;
	bool r_round = (result & 0x1) != 0;
	bool r_tail = tail || (value & tail_mask) != 0;
	uint64_t round_bit = (r_round && (r_odd || r_tail)) ? 1u : 0u;

	return (result >> 1u) + round_bit;
}

typedef enum {
	UFBXI_PARSE_DOUBLE_ALLOW_FAST_PATH = 0x1,
	UFBXI_PARSE_DOUBLE_VERIFY_LENGTH = 0x2,
	UFBXI_PARSE_DOUBLE_AS_BINARY32 = 0x4,
} ufbxi_parse_double_flag;

static ufbxi_noinline double ufbxi_parse_double(const char *str, size_t max_length, char **end, uint32_t flags)
{
	const uint32_t max_limbs = 14;

	ufbxi_bigint_limb mantissa_limbs[42], divisor_limbs[42], quotient_limbs[42];
	ufbxi_bigint big_mantissa = ufbxi_bigint_array(mantissa_limbs);
	ufbxi_bigint big_quotient = ufbxi_bigint_array(quotient_limbs);
	int32_t dec_exponent = 0, has_dot = 0;
	bool negative = false, tail = false, digits_valid = true;
	uint64_t digits = 0;
	uint32_t num_digits = 0;

	const char *p = str;
	if (*p == '+' || *p == '-') {
		negative = *p++ == '-';
	}
	for (;;) {
		char c = *p++;
		if (c >= '0' && c <= '9') {
			if (big_mantissa.length < max_limbs) {
				digits = digits * 10 + (uint64_t)(c - '0');
				num_digits++;
				if (num_digits >= 18) {
					ufbxi_bigint_mad(&big_mantissa, ufbxi_pow5_tab[num_digits] << num_digits, digits);
					digits = 0;
					num_digits = 0;
					digits_valid = false;
				}
				dec_exponent -= has_dot;
			} else {
				dec_exponent += 1 - has_dot;
			}
		} else if (c == '.' && !has_dot) {
			has_dot = true;
		} else {
			break;
		}
	}
	p--;
	if (*p == 'e' || *p == 'E') {
		p++;
		bool exp_negative = false;
		if (*p == '+' || *p == '-') {
			exp_negative = *p == '-';
			p++;
		}
		int32_t exp = 0;
		for (;;) {
			char c = *p;
			if (c >= '0' && c <= '9') {
				p++;
				exp = exp * 10 + (c - '0');
				if (exp >= 10000) break;
			} else {
				break;
			}
		}
		dec_exponent += exp_negative ? -exp : exp;
	}

	*end = (char*)p;
	// Check that the number is not potentially truncated.
	if (ufbxi_to_size(p - str) >= max_length && (flags & UFBXI_PARSE_DOUBLE_VERIFY_LENGTH) != 0) {
		*end = NULL;
		return 0.0;
	}

	// Both power of 10 and integer are exactly representable as doubles
	// Powers of 10 are factored as 2*5, and 2^N can be always exactly represented.
	if ((flags & UFBXI_PARSE_DOUBLE_ALLOW_FAST_PATH) != 0 && big_mantissa.length == 0 && dec_exponent >= -22 && dec_exponent <= 22 && (digits >> 53) == 0) {
		double value;
		if (dec_exponent < 0) {
			value = (double)digits / ufbxi_pow10_tab_f64[-dec_exponent];
		} else {
			value = (double)digits * ufbxi_pow10_tab_f64[dec_exponent];
		}
		return negative ? -value : value;
	}

	if (big_mantissa.length == 0) {
		big_mantissa.limbs[0] = (ufbxi_bigint_limb)digits;
		big_mantissa.limbs[1] = (ufbxi_bigint_limb)(digits >> 32u);
		big_mantissa.length = (digits >> 32u) ? 2 : digits ? 1 : 0;
		if (big_mantissa.length == 0) return negative ? -0.0 : 0.0;
	} else {
		ufbxi_bigint_mad(&big_mantissa, ufbxi_pow5_tab[num_digits] << num_digits, digits);
	}

	uint32_t enc_sign_shift = 63;
	uint32_t enc_mantissa_bits = 53;
	int32_t enc_max_exponent = 1023;
	if (flags & UFBXI_PARSE_DOUBLE_AS_BINARY32) {
		enc_sign_shift = 31;
		enc_mantissa_bits = 24;
		enc_max_exponent = 127;
	}

	int32_t exponent = 0;
	if (dec_exponent < 0) {
		if (dec_exponent + (int32_t)big_mantissa.length * 10 <= -325) return negative ? -0.0 : 0.0;

		ufbxi_bigint big_divisor = ufbxi_bigint_array(divisor_limbs);
		uint32_t pow5 = (uint32_t)-dec_exponent;
		uint32_t initial_pow5 = pow5 <= 27 ? pow5 : 27;
		uint64_t pow5_value = ufbxi_pow5_tab[initial_pow5];
		pow5 -= initial_pow5;
		exponent += dec_exponent;

		if (pow5 == 0 && digits_valid && digits >> 63 == 0) {
			uint32_t divisor_zeros = ufbxi_lzcnt64(pow5_value);
			uint64_t mantissa_zeros = ufbxi_lzcnt64(digits) - 1;
			uint64_t divisor_bits = pow5_value << divisor_zeros;
			uint64_t mantissa_bits = digits << mantissa_zeros;
			big_divisor.limbs[0] = (ufbxi_bigint_limb)divisor_bits;
			big_divisor.limbs[1] = (ufbxi_bigint_limb)(divisor_bits >> 32u);
			big_divisor.length = 2;
			big_mantissa.limbs[0] = 0;
			big_mantissa.limbs[1] = 0;
			big_mantissa.limbs[2] = (ufbxi_bigint_limb)mantissa_bits;
			big_mantissa.limbs[3] = (ufbxi_bigint_limb)(mantissa_bits >> 32u);
			big_mantissa.length = 4;
			exponent += (int32_t)divisor_zeros - (int32_t)mantissa_zeros - 64;
		} else {
			big_divisor.limbs[0] = (ufbxi_bigint_limb)pow5_value;
			big_divisor.limbs[1] = (ufbxi_bigint_limb)(pow5_value >> 32u);
			big_divisor.length = (pow5_value >> 32u) != 0 ? 2 : 1;
			if (pow5 > 0) {
				ufbxi_bigint_mul_pow5(&big_divisor, pow5);
			}

			uint32_t divisor_zeros = ufbxi_lzcnt32(big_divisor.limbs[big_divisor.length - 1]);
			if (big_divisor.length == 1) divisor_zeros += UFBXI_BIGINT_LIMB_BITS;
			ufbxi_bigint_shift_left(&big_divisor, divisor_zeros);
			uint32_t divisor_bits = big_divisor.length * UFBXI_BIGINT_LIMB_BITS;

			uint32_t mantissa_zeros = ufbxi_lzcnt32(big_mantissa.limbs[big_mantissa.length - 1]);
			uint32_t mantissa_bits = big_mantissa.length * UFBXI_BIGINT_LIMB_BITS - mantissa_zeros;
			uint32_t mantissa_min_bits = divisor_bits + enc_mantissa_bits + 2;
			uint32_t mantissa_shift = mantissa_bits < mantissa_min_bits ? mantissa_min_bits - mantissa_bits : 0;
			// Align mantissa to never have a high bit, this means we can skip the first digit during division.
			mantissa_shift += ((mantissa_shift - mantissa_zeros) & (UFBXI_BIGINT_LIMB_BITS - 1)) == 0 ? 1 : 0;
			if (mantissa_shift > 0) {
				ufbxi_bigint_shift_left(&big_mantissa, mantissa_shift);
			}
			exponent += (int32_t)divisor_zeros - (int32_t)mantissa_shift;
		}

		tail = ufbxi_bigint_div(&big_quotient, &big_mantissa, &big_divisor);
		big_mantissa = big_quotient;
	} else if (dec_exponent > 0) {
		if (dec_exponent + (int32_t)(big_mantissa.length - 1) * 9 >= 310) return negative ? -UFBX_INFINITY : UFBX_INFINITY;

		exponent += dec_exponent;
		ufbxi_bigint_mul_pow5(&big_mantissa, (uint32_t)dec_exponent);
	}

	uint64_t mantissa = ufbxi_bigint_extract_high(big_mantissa, &exponent, &tail);
	uint64_t sign_bit = (uint64_t)(negative ? 1u : 0u) << enc_sign_shift;

	uint32_t mantissa_shift = 64 - enc_mantissa_bits;
	if (exponent > enc_max_exponent) {
		return negative ? -UFBX_INFINITY : UFBX_INFINITY;
	} else if (exponent <= -enc_max_exponent) {
		mantissa_shift += (uint32_t)(-enc_max_exponent + 1 - exponent);
		exponent = -enc_max_exponent + 1;
	}

	mantissa = ufbxi_shift_right_round(mantissa, mantissa_shift, tail);
	if (mantissa == 0) return negative ? -0.0 : 0.0;

	uint64_t bits = mantissa;
	bits += (uint64_t)(exponent + enc_max_exponent - 1) << (enc_mantissa_bits - 1);
	bits |= sign_bit;

	if (flags & UFBXI_PARSE_DOUBLE_AS_BINARY32) {
		uint32_t bits_lo = (uint32_t)bits;
		float result;
		ufbxi_bit_cast(float, result, uint32_t, bits_lo);
		return result;
	} else {
		double result;
		ufbxi_bit_cast(double, result, uint64_t, bits);
		return result;
	}
}

static ufbxi_noinline uint32_t ufbxi_parse_double_init_flags()
{
	// We require evaluation in double precision, either for doubles (0) or always (1)
	// and rounding to nearest, which we can check for with `1 + eps == 1 - eps`.
	#if UFBX_FLT_EVAL_METHOD == 0 || UFBX_FLT_EVAL_METHOD == 1
		static volatile double ufbxi_volatile_eps = 2.2250738585072014e-308;
		if (1.0 + ufbxi_volatile_eps == 1.0 - ufbxi_volatile_eps) return UFBXI_PARSE_DOUBLE_ALLOW_FAST_PATH;
	#endif

	return 0;
}

static ufbxi_forceinline int64_t ufbxi_parse_int64(const char *str, char **end)
{
	uint64_t abs_val = 0;
	bool negative = *str == '-';
	bool positive = *str == '+';

	size_t init_len = (negative | positive) ? 1 : 0;
	size_t len = init_len;
	for (; len < 30; len++) {
		char c = str[len];
		if (!(c >= '0' && c <= '9')) break;
		abs_val = 10 * abs_val + (uint64_t)(c - '0');
	}
	if (len == 30 || len == init_len) {
		*end = NULL;
		return 0;
	}

	// TODO: Wrap/clamp?
	*end = (char*)str + len;
	return negative ? -(int64_t)abs_val : (int64_t)abs_val;
}

static ufbxi_noinline uint32_t ufbxi_parse_uint32_radix(const char *str, uint32_t radix)
{
	uint32_t value = 0;
	for (const char *p = str; ; p++) {
		char c = *p;
		if (c >= '0' && c <= '9') {
			value = value * radix + (uint32_t)(c - '0');
		} else if (radix == 16 && (c >= 'a' && c <= 'f')) {
			value = value * radix + (uint32_t)(c + (10 - 'a'));
		} else if (radix == 16 && (c >= 'A' && c <= 'F')) {
			value = value * radix + (uint32_t)(c + (10 - 'A'));
		} else {
			break;
		}
	}
	return value;
}

// -- DEFLATE implementation

#if !defined(ufbx_inflate)

// Lookup data: [0:5] extra bits [5:8] flags [16:32] base value
// Generated by `misc/deflate_lut.py`
static const uint32_t ufbxi_deflate_length_lut[] = {
		0x00000020, 0x00030040, 0x00040040, 0x00050040, 0x00060040, 0x00070040, 0x00080040, 0x00090040,
		0x000a0040, 0x000b0041, 0x000d0041, 0x000f0041, 0x00110041, 0x00130042, 0x00170042, 0x001b0042,
		0x001f0042, 0x00230043, 0x002b0043, 0x00330043, 0x003b0043, 0x00430044, 0x00530044, 0x00630044,
		0x00730044, 0x00830045, 0x00a30045, 0x00c30045, 0x00e30045, 0x01020040, 0x00010020, 0x00010020,
};
static const uint32_t ufbxi_deflate_dist_lut[] = {
		0x00010000, 0x00020000, 0x00030000, 0x00040000, 0x00050001, 0x00070001, 0x00090002, 0x000d0002,
		0x00110003, 0x00190003, 0x00210004, 0x00310004, 0x00410005, 0x00610005, 0x00810006, 0x00c10006,
		0x01010007, 0x01810007, 0x02010008, 0x03010008, 0x04010009, 0x06010009, 0x0801000a, 0x0c01000a,
		0x1001000b, 0x1801000b, 0x2001000c, 0x3001000c, 0x4001000d, 0x6001000d, 0x00010020, 0x00010020,
};

static const uint8_t ufbxi_deflate_code_length_permutation[] = {
	16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15,
};

#define UFBXI_INFLATE_FAST_MIN_IN 8
#define UFBXI_INFLATE_FAST_MIN_OUT 2

#define UFBXI_HUFF_MAX_BITS 16
#define UFBXI_HUFF_MAX_VALUE 288
#define UFBXI_HUFF_FAST_BITS 10
#define UFBXI_HUFF_FAST_SIZE (1 << UFBXI_HUFF_FAST_BITS)
#define UFBXI_HUFF_FAST_MASK (UFBXI_HUFF_FAST_SIZE - 1)
#define UFBXI_HUFF_MAX_LONG_BITS 5
#define UFBXI_HUFF_MAX_LONG_SYMS 380

#define UFBXI_HUFF_CODELEN_FAST_BITS 8
#define UFBXI_HUFF_CODELEN_FAST_MASK ((1<<UFBXI_HUFF_CODELEN_FAST_BITS)-1)

#define UFBXI_HUFF_MAX_EXTRA_SYMS 32

typedef struct {

	// Number of bytes left to read from `read_fn()`
	size_t input_left;

	// User-supplied read callback
	ufbx_read_fn *read_fn;
	void *read_user;

	// Buffer to read to from `read_fn()`, may point to `local_buffer` if user
	// didn't supply a suitable buffer.
	char *buffer;
	size_t buffer_size;

	// Current chunk of data to process, either the initial buffer of input
	// or part of `buffer`.
	const char *chunk_begin;    // < Begin of the buffer
	const char *chunk_ptr;      // < Next bytes to read to `bits`
	const char *chunk_yield;    // < End of data before needing to call `ufbxi_bit_yield()`
	const char *chunk_end;      // < End of data before needing to call `ufbxi_bit_refill()`
	const char *chunk_real_end; // < Actual end of the data buffer

	// Amount of bytes read before the current chunk
	size_t num_read_before_chunk;
	uint64_t progress_bias;
	uint64_t progress_total;
	size_t progress_interval;

	uint64_t bits; // < Buffered bits
	size_t left;   // < Number of valid low bits in `bits`

	// Progress tracking, maybe `NULL` it not requested
	ufbx_progress_cb progress_cb;

	// When `progress_cb.fn()` returns `false` set the `cancelled` flag and
	// set the buffered bits to `cancel_bits`.
	uint64_t cancel_bits;
	bool cancelled;

	char local_buffer[256];
} ufbxi_bit_stream;

// Packed symbol information:
//
//   [0:5]   total_bits    // [fast=1] Number of bits in the symbol _including_ extra bits
//   [0:5]   extra_mask    // [fast=0] Mask of extra bits to use
//   [5]     end           // 1 if end/invalid symbol, always 0 if `fast` (*1)
//   [6]     match         // 1 if the symbol is a match length, always 0 in non `lit_length` trees (always 0 if `end`)
//   [7]     fast          // 1 if the symbol can be determined from the table bits and `end=0`
//   [8:16]  lit_byte      // [lit_length]                     Literal byte
//   [8:16]  len_index     // [lit_length, match=1]            Match length index
//   [8:16]  dist_index    // [dist]                           Match distance index
//   [8:16]  code_length   // [code_length]                    Dynamic Huffman code length code
//   [8:16]  long_offset   // [fast_sym, fast=0, extra_mask>0] Base offset (halved) to `long_sym[]`
//   [8:16]  code_prefix   // [fast_sym, fast=0, extra_mask=0] First 8-bits of the code (reverse of the lookup)
//
// (*1) Not allowing `end` if `fast` serves a dual purpose: It allows us to omit a check for the end symbol in the
// fast path and allows using the symbol as a 64-bit shift amount (x64/ARM64/WASM have native modulo 64 shifts).
//
// Valid bit patterns, all other patterns are forbidden (`sorted_to_sym[]` contains same patterns as `long_sym[]`):
//
//   tree                    b e m f v
//
//   lit_length.fast_sym[]   N 0 0 1 L  // Short N bit code (no extra allowed) for literal byte L
//   lit_length.fast_sym[]   N 0 1 1 I  // Short N bit code (huff+extra bits) for length index I
//   lit_length.fast_sym[]   M 0 0 0 X  // Long code at `lit_length.long_sym[X*2 + ((bits>>FAST_BITS) & M)]`
//   lit_length.fast_sym[]   0 0 0 0 R  // Extra long code with prefix R, use `lit_length.sorted_to_sym[]` to resolve (*1)
//   lit_length.fast_sym[]   N 1 0 0 0  // Short N bit code for end-of-block (256) symbol
//   lit_length.fast_sym[]   0 1 0 0 1  // Invalid lit_length code
//
//   lit_length.long_sym[]   N 0 0 0 L  // Long N bit code (no extra allowed) for literal byte L
//   lit_length.long_sym[]   N 0 1 0 I  // Long N bit code (huff+extra bits) for length index I
//   lit_length.long_sym[]   N 1 0 0 0  // Long N bit code for end-of-block (256) symbol
//   lit_length.long_sym[]   0 1 0 0 1  // Invalid lit_length code
//
//   dist.fast_sym[]         N 0 0 1 L  // Short N bit code (huff+extra bits) for distance index I
//   dist.fast_sym[]         M 0 0 0 X  // Long code at `dist.long_sym[X*2 + ((bits>>FAST_BITS) & M)]`
//   dist.fast_sym[]         0 0 0 0 R  // Extra long code with prefix R, use `dist.sorted_to_sym[]` to resolve (*1)
//   dist.fast_sym[]         N 1 0 0 1  // Unused symbol 30-31 or invalid distance code
//
//   dist.long_sym[]         N 0 0 0 I  // Long N bit code (huff+extra bits) for distance index I
//   dist.long_sym[]         N 1 0 0 1  // Unused symbol 30-31 or invalid distance code
//
//   code_length.fast_sym[]  N 0 0 1 B  // Short N bit code (huff only, extra handled explicitly) for symbol bit count B
//   code_length.fast_sym[]  M 0 0 0 X  // Long code at `dist.long_sym[X*2 + ((bits>>FAST_BITS) & M)]`
//   code_length.fast_sym[]  0 0 0 0 R  // Extra long code with prefix R, use `code_length.sorted_to_sym[]` to resolve (*1)
//
//   code_length.long_sym[]  N 0 0 0 B  // Long N bit code (huff only, extra handled explicitly) for symbol bit count B
//
// (*1) Never necessary if `fast_bits >= 10` due to `long_sym[]` covering all possible codes,
//
typedef uint16_t ufbxi_huff_sym;

#define ufbxi_huff_sym_total_bits(sym) ((uint32_t)(sym) & 0x1f)
#define ufbxi_huff_sym_long_mask(sym) ((uint32_t)(sym) & 0x1f)
#define ufbxi_huff_sym_long_offset(sym) ((uint32_t)(sym) >> 7u)
#define ufbxi_huff_sym_value(sym) ((uint32_t)(sym) >> 8u)

enum {
	UFBXI_HUFF_SYM_END   = 0x20,
	UFBXI_HUFF_SYM_MATCH = 0x40,
	UFBXI_HUFF_SYM_FAST  = 0x80,
};

#define UFBXI_HUFF_ERROR_SYM ((ufbxi_huff_sym)0x0120)         // Error symbol, END (value 1)
#define UFBXI_HUFF_UNINITIALIZED_SYM ((ufbxi_huff_sym)0x0220) // Uninitialized symbol for regression, END (value 2)

typedef struct {
	ufbxi_huff_sym fast_sym[UFBXI_HUFF_FAST_SIZE];      // < Lookup from N bytes to symbol information
	ufbxi_huff_sym long_sym[UFBXI_HUFF_MAX_LONG_SYMS];  // < Fast long symbol lookup
	ufbxi_huff_sym sorted_to_sym[UFBXI_HUFF_MAX_VALUE]; // < Symbol information per sorted index

	uint32_t extra_shift_base[UFBXI_HUFF_MAX_EXTRA_SYMS]; // < [0:6] shift [16:32] base value
	uint16_t extra_mask[UFBXI_HUFF_MAX_EXTRA_SYMS];       // < Mask for extra bits

	uint16_t past_max_code[UFBXI_HUFF_MAX_BITS]; // < One past maximum code value per bit length
	int16_t code_to_sorted[UFBXI_HUFF_MAX_BITS]; // < Code to sorted symbol index per bit length
	uint32_t num_symbols;

	uint32_t end_of_block_bits;
} ufbxi_huff_tree;

typedef struct {
	union {
		struct {
			ufbxi_huff_tree lit_length;
			ufbxi_huff_tree dist;
		};
		ufbxi_huff_tree trees[2];
	};
	uint32_t fast_bits;
} ufbxi_trees;

typedef struct {
	bool initialized;
	ufbxi_trees static_trees;
} ufbxi_inflate_retain_imp;

ufbx_static_assert(inflate_retain_size, sizeof(ufbxi_inflate_retain_imp) <= sizeof(ufbx_inflate_retain));

typedef struct {
	ufbxi_bit_stream stream;
	uint32_t fast_bits;

	char *out_begin;
	char *out_ptr;
	char *out_end;
} ufbxi_deflate_context;

static ufbxi_forceinline uint32_t
ufbxi_bit_reverse(uint32_t mask, uint32_t num_bits)
{
	ufbxi_dev_assert(num_bits <= 16);
	uint32_t x = mask;
	x = (((x & 0xaaaa) >> 1) | ((x & 0x5555) << 1));
	x = (((x & 0xcccc) >> 2) | ((x & 0x3333) << 2));
	x = (((x & 0xf0f0) >> 4) | ((x & 0x0f0f) << 4));
	x = (((x & 0xff00) >> 8) | ((x & 0x00ff) << 8));
	return x >> (16 - num_bits);
}

static ufbxi_noinline const char *
ufbxi_bit_chunk_refill(ufbxi_bit_stream *s, const char *ptr)
{
	// Copy any left-over data to the beginning of `buffer`
	size_t left = ufbxi_to_size(s->chunk_real_end - ptr);
	ufbxi_dev_assert(left < 64);
	if (left > 0) memmove(s->buffer, ptr, left);

	s->num_read_before_chunk += ufbxi_to_size(ptr - s->chunk_begin);

	// Read more user data if the user supplied a `read_fn()`, otherwise
	// we assume the initial data chunk is the whole input buffer.
	if (s->read_fn && !s->cancelled) {
		size_t to_read = ufbxi_min_sz(s->input_left, s->buffer_size - left);
		if (to_read > 0) {
			size_t num_read = s->read_fn(s->read_user, s->buffer + left, to_read);
			// TODO: IO error, should unify with (currently broken) cancel logic
			if (num_read > to_read) num_read = 0;
			ufbxi_dev_assert(s->input_left >= num_read);
			s->input_left -= num_read;
			left += num_read;
		}
	}

	// Pad the rest with zeros
	if (left < 64) {
		memset(s->buffer + left, 0, 64 - left);
		left = 64;
	}

	s->chunk_begin = s->buffer;
	s->chunk_ptr = s->buffer;
	s->chunk_end = s->buffer + left - 8;
	s->chunk_real_end = s->buffer + left;
	return s->buffer;
}

static ufbxi_noinline void ufbxi_bit_stream_init(ufbxi_bit_stream *s, const ufbx_inflate_input *input)
{
	size_t data_size = input->data_size;
	if (data_size > input->total_size) {
		data_size = input->total_size;
	}

	s->read_fn = input->read_fn;
	s->read_user = input->read_user;
	s->progress_cb = input->progress_cb;
	s->chunk_begin = (const char*)input->data;
	s->chunk_ptr = (const char*)input->data;
	s->chunk_end = ufbxi_add_ptr((const char*)input->data, ufbxi_max_sz(8, data_size) - 8);
	s->chunk_real_end = ufbxi_add_ptr((const char*)input->data, data_size);
	s->input_left = input->total_size - data_size;

	// Use the user buffer if it's large enough, otherwise `local_buffer`
	if (input->buffer_size > sizeof(s->local_buffer)) {
		s->buffer = (char*)input->buffer;
		s->buffer_size = input->buffer_size;
	} else {
		s->buffer = s->local_buffer;
		s->buffer_size = sizeof(s->local_buffer);
	}
	s->num_read_before_chunk = 0;
	s->progress_bias = input->progress_size_before;
	s->progress_total = input->total_size + input->progress_size_before + input->progress_size_after;
	if (!s->progress_cb.fn || input->progress_interval_hint >= SIZE_MAX) {
		s->progress_interval = SIZE_MAX;
	} else if (input->progress_interval_hint > 0) {
		s->progress_interval = (size_t)input->progress_interval_hint;
	} else {
		s->progress_interval = 0x4000;
	}
	s->cancelled = false;

	// Clear the initial bit buffer
	s->bits = 0;
	s->left = 0;

	// If the initial data buffer is not large enough to be read directly
	// from refill the chunk once.
	if (data_size < 64) {
		ufbxi_bit_chunk_refill(s, s->chunk_begin);
	}

	if (s->progress_cb.fn && ufbxi_to_size(s->chunk_end - s->chunk_ptr) > s->progress_interval + 8) {
		s->chunk_yield = s->chunk_ptr + s->progress_interval;
	} else {
		s->chunk_yield = s->chunk_end;
	}
}

static ufbxi_noinline const char *
ufbxi_bit_yield(ufbxi_bit_stream *s, const char *ptr)
{
	if (ptr > s->chunk_end) {
		ptr = ufbxi_bit_chunk_refill(s, ptr);
	}

	if (s->progress_cb.fn) {
		size_t num_read = s->num_read_before_chunk + ufbxi_to_size(ptr - s->chunk_begin);

		ufbx_progress progress = { s->progress_bias + num_read, s->progress_total };
		uint32_t result = (uint32_t)s->progress_cb.fn(s->progress_cb.user, &progress);
		ufbx_assert(result == UFBX_PROGRESS_CONTINUE || result == UFBX_PROGRESS_CANCEL);
		if (result == UFBX_PROGRESS_CANCEL) {
			s->cancelled = true;
			ptr = s->local_buffer;
			s->buffer = s->local_buffer;
			s->buffer_size = sizeof(s->local_buffer);
			s->chunk_begin = ptr;
			s->chunk_ptr = ptr;
			s->chunk_end = ptr + sizeof(s->local_buffer) - 8;
			s->chunk_real_end = ptr + sizeof(s->local_buffer);
			memset(s->local_buffer, 0, sizeof(s->local_buffer));
		}
	}

	if (s->progress_cb.fn && ufbxi_to_size(s->chunk_end - ptr) > s->progress_interval + 8) {
		s->chunk_yield = ptr + s->progress_interval;
	} else {
		s->chunk_yield = s->chunk_end;
	}

	return ptr;
}

static ufbxi_forceinline void
ufbxi_bit_refill(uint64_t *p_bits, size_t *p_left, const char **p_data, ufbxi_bit_stream *s)
{
	if (*p_data > s->chunk_yield) {
		*p_data = ufbxi_bit_yield(s, *p_data);
		if (s->cancelled) {
			// Force an end-of-block symbol when cancelled so we don't need an
			// extra branch in the chunk decoding loop.
			*p_bits = s->cancel_bits;
		}
	}

	// See https://fgiesen.wordpress.com/2018/02/20/reading-bits-in-far-too-many-ways-part-2/
	// variant 4. This branchless refill guarantees [56,63] bits to be valid in `*p_bits`.
	ufbxi_regression_assert(*p_left <= 64); \
	*p_bits |= ufbxi_read_u64(*p_data) << *p_left;
	*p_data += (63 - *p_left) >> 3;
	*p_left |= 56;
}

// See `ufbxi_bit_refill()`
#define ufbxi_macro_bit_refill_fast(m_bits, m_left, m_data, m_refill_bits) do { \
		ufbxi_regression_assert(m_left <= 64); \
		m_bits |= m_refill_bits << m_left; \
		m_data += (63 - m_left) >> 3; \
		m_left |= 56; \
	} while (0)

static ufbxi_noinline int
ufbxi_bit_copy_bytes(void *dst, ufbxi_bit_stream *s, size_t len)
{
	ufbx_assert(s->left % 8 == 0);
	char *ptr = (char*)dst;

	// Copy the buffered bits first
	while (len > 0 && s->left > 0) {
		*ptr++ = (char)(uint8_t)s->bits;
		len -= 1;
		s->bits >>= 8;
		s->left -= 8;
	}

	// We need to clear the top bits as there may be data
	// read ahead past `s->left` in some cases
	s->bits = 0;

	// Copy the current chunk
	size_t chunk_left = ufbxi_to_size(s->chunk_real_end - s->chunk_ptr);
	if (chunk_left >= len) {
		memcpy(ptr, s->chunk_ptr, len);
		s->chunk_ptr += len;
		return 1;
	} else {
		memcpy(ptr, s->chunk_ptr, chunk_left);
		s->chunk_ptr += chunk_left;
		ptr += chunk_left;
		len -= chunk_left;
	}

	// Read extra bytes from user
	if (len > s->input_left) return 0;
	size_t num_read = 0;
	if (s->read_fn) {
		num_read = s->read_fn(s->read_user, ptr, len);
		s->input_left -= num_read;
	}
	return num_read == len;
}

// 0: Success
// -1: Overfull
// -2: Underfull
static ufbxi_noinline ptrdiff_t
ufbxi_huff_build_imp(ufbxi_huff_tree *tree, uint8_t *sym_bits, uint32_t sym_count, const uint32_t *sym_extra, uint32_t sym_extra_offset, uint32_t fast_bits, uint32_t *bits_counts)
{
	uint32_t fast_mask = (1u << fast_bits) - 1;

	ufbx_assert(sym_count <= UFBXI_HUFF_MAX_VALUE);
	tree->num_symbols = sym_count;

	uint32_t nonzero_sym_count = sym_count - bits_counts[0];

	uint32_t total_syms[UFBXI_HUFF_MAX_BITS]; // ufbxi_uninit
	uint32_t first_code[UFBXI_HUFF_MAX_BITS]; // ufbxi_uninit

	tree->code_to_sorted[0] = INT16_MAX;
	tree->past_max_code[0] = 0;
	total_syms[0] = 0;

	// Clear to uninitialized symbols
	#if defined(UFBX_REGRESSION)
	{
		for (size_t i = 0; i < UFBXI_HUFF_FAST_SIZE; i++) {
			tree->fast_sym[i] = UFBXI_HUFF_UNINITIALIZED_SYM;
		}
		for (size_t i = 0; i < UFBXI_HUFF_MAX_VALUE; i++) {
			tree->sorted_to_sym[i] = UFBXI_HUFF_UNINITIALIZED_SYM;
		}
		for (size_t i = 0; i < UFBXI_HUFF_MAX_LONG_SYMS; i++) {
			tree->long_sym[i] = UFBXI_HUFF_UNINITIALIZED_SYM;
		}
	}
	#endif

	uint32_t last_valid_prefix = 0;

	// Resolve the maximum code per bit length and ensure that the tree is not
	// overfull or underfull.
	{
		int num_codes_left = 1;
		uint32_t code = 0;
		uint32_t prev_count = 0;
		uint32_t long_offset = 0;
		for (uint32_t bits = 1; bits < UFBXI_HUFF_MAX_BITS; bits++) {
			uint32_t count = bits_counts[bits];
			code = (code + prev_count) << 1;
			first_code[bits] = code;
			tree->past_max_code[bits] = (uint16_t)(code + count);

			uint32_t prev_syms = total_syms[bits - 1];
			total_syms[bits] = prev_syms + count;

			// Each bit level doubles the amount of codes and potentially removes some
			num_codes_left = (num_codes_left << 1) - (int32_t)count;
			if (num_codes_left < 0) {
				return -1;
			}

			if (count > 0 && bits > fast_bits && bits - fast_bits <= UFBXI_HUFF_MAX_LONG_BITS) {
				uint32_t shift = bits - fast_bits;
				uint32_t last_inclusive = num_codes_left == 0 ? (1u<<shift) - 1u : 0u;
				uint32_t first_prefix = code >> shift;
				uint32_t last_prefix = (code + count + last_inclusive) >> shift;
				uint32_t mask = (1u << shift) - 1u;
				uint32_t half_step = 1u << (shift - 1u);
				for (uint32_t prefix = first_prefix; prefix < last_prefix; prefix++) {
					uint32_t rev_prefix = ufbxi_bit_reverse(prefix, fast_bits);
					tree->fast_sym[rev_prefix] = (ufbxi_huff_sym)(mask | (long_offset << 8));
					long_offset += half_step;
				}

				last_valid_prefix = last_prefix;
			}

			if (count > 0) {
				tree->code_to_sorted[bits] = (int16_t)((int)prev_syms - (int)code);
			} else {
				tree->code_to_sorted[bits] = INT16_MAX;
			}
			prev_count = count;
		}

		// All codes should be used if there's more than one symbol, if there's only one symbol there should be
		// only a single 1-bit code.
		if (nonzero_sym_count > 1 && num_codes_left != 0) {
			return -2;
		} else if (nonzero_sym_count == 1 && total_syms[1] != 1) {
			return -2;
		}

		// We should always have enough space for long symbols as we support up to 5 (UFBXI_HUFF_MAX_LONG_BITS)
		// bits and the largest tree has 286 symbols. For each bit we may waste at most 2^bits slots (conservative)
		// and in the end we may waste 2^5 slots giving us `286+2+4+8+16+32+32 = 380` (UFBXI_HUFF_MAX_LONG_SYMS)
		ufbx_assert(long_offset <= UFBXI_HUFF_MAX_LONG_SYMS);
	}

	tree->end_of_block_bits = 0;
	uint32_t num_extra = 0;
	tree->extra_shift_base[0] = 0;
	tree->extra_mask[0] = 0;

	// Fill `fast_sym[]` with error symbols if necessary, we don't need to do this if we have two or more symbols
	// as the tree is guaranteed to be full, which means we will populate the whole `fast_sym[]`
	if (nonzero_sym_count <= 1) {
		for (uint32_t i = 0; i <= fast_mask; i++) {
			tree->fast_sym[i] = UFBXI_HUFF_ERROR_SYM;
		}
	}

	// Generate per-length sorted-to-symbol and fast lookup tables
	uint32_t bits_index[UFBXI_HUFF_MAX_BITS] = { 0 };
	for (uint32_t i = 0; i < sym_count; i++) {
		uint32_t bits = sym_bits[i];
		if (bits == 0) continue;

		uint32_t sym = i << 8 | bits;
		if (i >= sym_extra_offset) {
			uint32_t extra = sym_extra[i - sym_extra_offset];
			sym += extra;

			// Store length/distance codes with extra values in a table.
			// TODO: This is unnecessary for small values
			if ((extra & 0xffff001f) != 0 && (extra & 0x20) == 0) {
				uint32_t ix = ++num_extra;
				tree->extra_shift_base[ix] = (extra & 0xffff0000) | bits;
				tree->extra_mask[ix] = (uint16_t)((1u << (extra & 0x1f)) - 1);
				sym = (sym & 0xff) | ix << 8;
			}

		}

		uint32_t index = bits_index[bits]++;
		uint32_t sorted = total_syms[bits - 1] + index;
		tree->sorted_to_sym[sorted] = (ufbxi_huff_sym)sym;

		// Reverse the code and fill all fast lookups with the reversed prefix
		uint32_t code = first_code[bits] + index;
		uint32_t rev_code = ufbxi_bit_reverse(code, bits);

		if (bits <= fast_bits) {
			uint32_t fast_sym = sym;
			// The `end` and `fast` flags are mutually exclusive
			if ((fast_sym & UFBXI_HUFF_SYM_END) == 0) {
				fast_sym |= UFBXI_HUFF_SYM_FAST;
			}
			uint32_t hi_max = 1u << (fast_bits - bits);
			for (uint32_t hi = 0; hi < hi_max; hi++) {
				ufbxi_regression_assert(nonzero_sym_count <= 1 || tree->fast_sym[rev_code | hi << bits] == UFBXI_HUFF_UNINITIALIZED_SYM);
				tree->fast_sym[rev_code | hi << bits] = (ufbxi_huff_sym)fast_sym;
			}
		} else if (bits <= fast_bits + UFBXI_HUFF_MAX_LONG_BITS && (code >> (bits - fast_bits)) < last_valid_prefix) {
			uint32_t fast_sym = tree->fast_sym[rev_code & fast_mask];
			ufbxi_regression_assert(fast_sym != UFBXI_HUFF_UNINITIALIZED_SYM);
			uint32_t long_bits = 0;

			uint32_t long_mask = fast_sym;
			while (long_bits < UFBXI_HUFF_MAX_LONG_BITS && (long_mask & 1) != 0) {
				long_mask >>= 1;
				long_bits += 1;
			}
			ufbxi_dev_assert(long_bits >= 1);

			uint32_t long_base = fast_sym >> 7u; // aka (fast_sym >> 8) * 2
			uint32_t lo_bits = bits - fast_bits;
			uint32_t hi_max = 1u << (long_bits - lo_bits);
			uint32_t rev_suffix = rev_code >> fast_bits;
			for (uint32_t hi = 0; hi < hi_max; hi++) {
				ufbxi_regression_assert(tree->long_sym[long_base + (rev_suffix | hi << lo_bits)] == UFBXI_HUFF_UNINITIALIZED_SYM);
				tree->long_sym[long_base + (rev_suffix | hi << lo_bits)] = (ufbxi_huff_sym)sym;
			}
		} else {
			uint32_t fast_sym = (code >> (bits - fast_bits)) << 8;
			ufbxi_regression_assert(
				tree->fast_sym[rev_code & fast_mask] == UFBXI_HUFF_UNINITIALIZED_SYM ||
				tree->fast_sym[rev_code & fast_mask] == (ufbxi_huff_sym)fast_sym);
			tree->fast_sym[rev_code & fast_mask] = (ufbxi_huff_sym)fast_sym;
		}

		// Make sure the end-of-block symbol goes through the slow path
		// Also store the end-of-block code so we can interrupt decoding
		if (i == 256) {
			tree->end_of_block_bits = rev_code;
		}
	}

	// Make sure all `fast_sym[]` are filled with an initialized value.
	#if defined(UFBX_REGRESSION)
	{
		for (size_t i = 0; i < UFBXI_HUFF_FAST_SIZE; i++) {
			if (i <= fast_mask) {
				ufbx_assert(tree->fast_sym[i] != UFBXI_HUFF_UNINITIALIZED_SYM);
			} else {
				ufbx_assert(tree->fast_sym[i] == UFBXI_HUFF_UNINITIALIZED_SYM);
			}
		}
		for (size_t i = 0; i < nonzero_sym_count; i++) {
			ufbx_assert(tree->sorted_to_sym[i] != UFBXI_HUFF_UNINITIALIZED_SYM);
		}
	}
	#endif

	return 0;
}

// 0: Success
// -1: Overfull
// -2: Underfull
static ufbxi_noinline ptrdiff_t
ufbxi_huff_build(ufbxi_huff_tree *tree, uint8_t *sym_bits, uint32_t sym_count, const uint32_t *sym_extra, uint32_t sym_extra_offset, uint32_t fast_bits)
{
	// Count the number of codes per bit length
	// `bits_counts[0]` contains the number of non-used symbols
	uint32_t bits_counts[UFBXI_HUFF_MAX_BITS]; // ufbxi_uninit
	memset(bits_counts, 0, sizeof(bits_counts));
	for (uint32_t i = 0; i < sym_count; i++) {
		uint32_t bits = sym_bits[i];
		ufbx_assert(bits < UFBXI_HUFF_MAX_BITS);
		bits_counts[bits]++;
	}

	return ufbxi_huff_build_imp(tree, sym_bits, sym_count, sym_extra, sym_extra_offset, fast_bits, bits_counts);
}

static ufbxi_forceinline ufbxi_huff_sym
ufbxi_huff_decode_bits(const ufbxi_huff_tree *tree, uint64_t bits, uint32_t fast_bits, uint32_t fast_mask)
{
	ufbxi_huff_sym sym = tree->fast_sym[bits & fast_mask];
	ufbxi_regression_assert(sym != UFBXI_HUFF_UNINITIALIZED_SYM);

	if ((sym & (UFBXI_HUFF_SYM_FAST|UFBXI_HUFF_SYM_END)) != 0) {
		return sym;
	}

	uint32_t tail = (uint32_t)(bits >> fast_bits);
	uint32_t long_mask = ufbxi_huff_sym_long_mask(sym);
	if (long_mask) {
		sym = tree->long_sym[ufbxi_huff_sym_long_offset(sym) + (tail & long_mask)];
		ufbxi_regression_assert(sym != UFBXI_HUFF_UNINITIALIZED_SYM);
		return sym;
	}

	ufbxi_dev_assert(fast_bits <= 8);

	uint32_t code = ufbxi_huff_sym_value(sym);
	uint32_t num_bits = fast_bits;
	for (;;) {
		code = code << 1 | (tail & 1);
		tail >>= 1;
		num_bits++;

		ufbxi_regression_assert(num_bits < UFBXI_HUFF_MAX_BITS);
		if (code < tree->past_max_code[num_bits]) {
			sym = tree->sorted_to_sym[(int32_t)code + (int32_t)tree->code_to_sorted[num_bits]];
			ufbxi_regression_assert(sym != UFBXI_HUFF_UNINITIALIZED_SYM);
			return sym;
		}
	}
}

static ufbxi_noinline void ufbxi_init_static_huff(ufbxi_trees *trees, const ufbx_inflate_input *input)
{
	ptrdiff_t err = 0;

	// Override `fast_bits` if necessary, this must always be valid as it's checked in the beginning of `ufbx_inflate()`.
	if (input && input->internal_fast_bits != 0) {
		trees->fast_bits = (uint32_t)input->internal_fast_bits;
		ufbx_assert(!(trees->fast_bits < 1 || trees->fast_bits == 9 || trees->fast_bits > 10));
	} else {
		trees->fast_bits = UFBXI_HUFF_FAST_BITS;
	}

	// 0-143: 8 bits, 144-255: 9 bits, 256-279: 7 bits, 280-287: 8 bits
	uint8_t lit_length_bits[288]; // ufbxi_uninit
	memset(lit_length_bits +   0, 8, 144 -   0);
	memset(lit_length_bits + 144, 9, 256 - 144);
	memset(lit_length_bits + 256, 7, 280 - 256);
	memset(lit_length_bits + 280, 8, 288 - 280);
	err |= ufbxi_huff_build(&trees->lit_length, lit_length_bits, sizeof(lit_length_bits), ufbxi_deflate_length_lut, 256, trees->fast_bits);

	// "Distance codes 0-31 are represented by (fixed-length) 5-bit codes"
	uint8_t dist_bits[32]; // ufbxi_uninit
	memset(dist_bits + 0, 5, 32 - 0);
	err |= ufbxi_huff_build(&trees->dist, dist_bits, sizeof(dist_bits), ufbxi_deflate_dist_lut, 0, trees->fast_bits);

	// Building the static trees cannot fail as we use pre-defined code lengths.
	ufbxi_ignore(err);
	ufbx_assert(err == 0);
}

// 0: Success
// -1: Huffman Overfull
// -2: Huffman Underfull
// -3: Code 16 repeat overflow
// -4: Code 17 repeat overflow
// -5: Code 18 repeat overflow
// -6: Bad length code
// -7: Cancelled
static ufbxi_noinline ptrdiff_t ufbxi_init_dynamic_huff_tree(ufbxi_deflate_context *dc, const ufbxi_huff_tree *huff_code_length, ufbxi_huff_tree *tree,
	uint32_t num_symbols, const uint32_t *sym_extra, uint32_t sym_extra_offset, uint32_t fast_bits)
{
	uint8_t code_lengths[UFBXI_HUFF_MAX_VALUE]; // ufbxi_uninit
	ufbx_assert(num_symbols <= UFBXI_HUFF_MAX_VALUE);

	uint64_t bits = dc->stream.bits;
	size_t left = dc->stream.left;
	const char *data = dc->stream.chunk_ptr;
	uint32_t bits_counts[UFBXI_HUFF_MAX_BITS]; // ufbxi_uninit
	memset(bits_counts, 0, sizeof(bits_counts));

	uint32_t symbol_index = 0;
	uint8_t prev = 0;
	while (symbol_index < num_symbols) {
		ufbxi_bit_refill(&bits, &left, &data, &dc->stream);
		if (dc->stream.cancelled) return -7;

		ufbxi_huff_sym sym = ufbxi_huff_decode_bits(huff_code_length, bits, UFBXI_HUFF_CODELEN_FAST_BITS, UFBXI_HUFF_CODELEN_FAST_MASK);
		ufbxi_regression_assert(sym != UFBXI_HUFF_UNINITIALIZED_SYM);

		uint32_t inst = ufbxi_huff_sym_value(sym);
		uint32_t sym_len = ufbxi_huff_sym_total_bits(sym);

		bits >>= sym_len;
		left -= sym_len;

		if (inst <= 15) {
			// "0 - 15: Represent code lengths of 0 - 15"
			prev = (uint8_t)inst;
			code_lengths[symbol_index++] = (uint8_t)inst;
			bits_counts[(int32_t)inst]++;
		} else if (inst == 16) {
			// "16: Copy the previous code length 3 - 6 times. The next 2 bits indicate repeat length."
			uint32_t num = 3 + ((uint32_t)bits & 0x3);
			bits >>= 2;
			left -= 2;
			if (symbol_index + num > num_symbols) return -3;
			memset(code_lengths + symbol_index, prev, num);
			symbol_index += num;
			bits_counts[(int32_t)prev] += num;
		} else if (inst == 17) {
			// "17: Repeat a code length of 0 for 3 - 10 times. (3 bits of length)"
			uint32_t num = 3 + ((uint32_t)bits & 0x7);
			bits >>= 3;
			left -= 3;
			if (symbol_index + num > num_symbols) return -4;
			memset(code_lengths + symbol_index, 0, num);
			symbol_index += num;
			prev = 0;
			bits_counts[0] += num;
		} else if (inst == 18) {
			// "18: Repeat a code length of 0 for 11 - 138 times (7 bits of length)"
			uint32_t num = 11 + ((uint32_t)bits & 0x7f);
			bits >>= 7;
			left -= 7;
			if (symbol_index + num > num_symbols) return -5;
			memset(code_lengths + symbol_index, 0, num);
			symbol_index += num;
			prev = 0;
			bits_counts[0] += num;
		} else {
			return -6;
		}
	}

	ptrdiff_t err = ufbxi_huff_build_imp(tree, code_lengths, num_symbols, sym_extra, sym_extra_offset, fast_bits, bits_counts);
	if (err != 0) return err;

	dc->stream.bits = bits;
	dc->stream.left = left;
	dc->stream.chunk_ptr = data;

	return 0;
}

static ufbxi_noinline ptrdiff_t
ufbxi_init_dynamic_huff(ufbxi_deflate_context *dc, ufbxi_trees *trees)
{
	uint64_t bits = dc->stream.bits;
	size_t left = dc->stream.left;
	const char *data = dc->stream.chunk_ptr;
	ufbxi_bit_refill(&bits, &left, &data, &dc->stream);
	if (dc->stream.cancelled) return -28;

	trees->fast_bits = dc->fast_bits;

	// The header contains the number of Huffman codes in each of the three trees.
	uint32_t num_lit_lengths = 257 + (uint32_t)(bits & 0x1f);
	uint32_t num_dists = 1 + (uint32_t)(bits >> 5 & 0x1f);
	uint32_t num_code_lengths = 4 + (uint32_t)(bits >> 10 & 0xf);
	bits >>= 14;
	left -= 14;

	// Code lengths for the "code length" Huffman tree are represented literally
	// 3 bits in order of: 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 up to
	// `num_code_lengths`, rest of the code lengths are 0 (unused)
	uint8_t code_lengths[19]; // ufbxi_uninit
	memset(code_lengths, 0, sizeof(code_lengths));
	for (size_t len_i = 0; len_i < num_code_lengths; len_i++) {
		if (len_i == 14) {
			ufbxi_bit_refill(&bits, &left, &data, &dc->stream);
			if (dc->stream.cancelled) return -28;
		}
		code_lengths[ufbxi_deflate_code_length_permutation[len_i]] = (uint32_t)bits & 0x7;
		bits >>= 3;
		left -= 3;
	}

	dc->stream.bits = bits;
	dc->stream.left = left;
	dc->stream.chunk_ptr = data;

	ufbxi_huff_tree huff_code_length; // ufbxi_uninit
	ptrdiff_t err; // ufbxi_uninit

	// Build the temporary "code length" Huffman tree used to encode the actual
	// trees used to compress the data. Use that to build the literal/length and
	// distance trees.
	err = ufbxi_huff_build(&huff_code_length, code_lengths, ufbxi_arraycount(code_lengths), NULL, INT32_MAX, UFBXI_HUFF_CODELEN_FAST_BITS);
	if (err) return -14 + 1 + err;
	err = ufbxi_init_dynamic_huff_tree(dc, &huff_code_length, &trees->lit_length, num_lit_lengths, ufbxi_deflate_length_lut, 256, dc->fast_bits);
	if (err) return err == -7 ? -28 : -16 + 1 + err;
	err = ufbxi_init_dynamic_huff_tree(dc, &huff_code_length, &trees->dist, num_dists, ufbxi_deflate_dist_lut, 0, dc->fast_bits);
	if (err) return err == -7 ? -28 : -22 + 1 + err;

	return 0;
}

static ufbxi_noinline uint32_t ufbxi_adler32(const void *data, size_t size)
{
	ufbxi_fast_uint a = 1, b = 0;
	const char *p = (const char*)data;

	// Adler-32 consists of two running sums modulo 65521. As an optimization
	// we can accumulate N sums before applying the modulo, where N depends on
	// the size of the type holding the sum.
	const ufbxi_fast_uint num_before_wrap = sizeof(ufbxi_fast_uint) == 8 ? 380368439u : 5552u;

	ufbxi_fast_uint size_left = size;
	while (size_left > 0) {
		ufbxi_fast_uint num = size_left <= num_before_wrap ? size_left : num_before_wrap;
		size_left -= num;
		const char *end = p + num;

		// Align to 16 bytes
		while (p != end && ((uintptr_t)p & 0xf) != 0) {
			a += (ufbxi_fast_uint)(uint8_t)p[0]; b += a;
			p++;
		}

#if UFBXI_HAS_SSE
		static const uint16_t factors[2][8] = {
			{ 16, 15, 14, 13, 12, 11, 10, 9, },
			{ 8, 7, 6, 5, 4, 3, 2, 1, },
		};

		const __m128i zero = _mm_setzero_si128();
		const __m128i factor_1 = _mm_set1_epi16(1);
		const __m128i factor_16 = _mm_set1_epi16(16);
		const __m128i factor_lo = _mm_loadu_si128((const __m128i*)factors[0]);
		const __m128i factor_hi = _mm_loadu_si128((const __m128i*)factors[1]);

		for (;;) {
			size_t chunk_size = ufbxi_min_sz(ufbxi_to_size(end - p), 5803) & ~(size_t)0xff;
			if (chunk_size == 0) break;
			const char *chunk_end = p + chunk_size;

			__m128i s1 = zero;
			__m128i s2 = zero;

			while (p != chunk_end) {
				__m128i s1_lo = zero, s1_hi = zero;
				__m128i tmp_lo = zero, tmp_hi = zero;

				ufbxi_nounroll for (size_t i = 0; i < 256; i += 32) {
					__m128i d0 = _mm_load_si128((const __m128i*)(p + i + 0));
					__m128i d1 = _mm_load_si128((const __m128i*)(p + i + 16));

					tmp_lo = _mm_add_epi16(tmp_lo, s1_lo);
					tmp_hi = _mm_add_epi16(tmp_hi, s1_hi);
					s1_lo = _mm_add_epi16(s1_lo, _mm_unpacklo_epi8(d0, zero));
					s1_hi = _mm_add_epi16(s1_hi, _mm_unpackhi_epi8(d0, zero));

					tmp_lo = _mm_add_epi16(tmp_lo, s1_lo);
					tmp_hi = _mm_add_epi16(tmp_hi, s1_hi);
					s1_lo = _mm_add_epi16(s1_lo, _mm_unpacklo_epi8(d1, zero));
					s1_hi = _mm_add_epi16(s1_hi, _mm_unpackhi_epi8(d1, zero));
				}

				s2 = _mm_add_epi32(s2, _mm_slli_epi32(s1, 8));
				s1 = _mm_add_epi32(s1, _mm_madd_epi16(s1_lo, factor_1));
				s1 = _mm_add_epi32(s1, _mm_madd_epi16(s1_hi, factor_1));

				s2 = _mm_add_epi32(s2, _mm_madd_epi16(tmp_lo, factor_16));
				s2 = _mm_add_epi32(s2, _mm_madd_epi16(tmp_hi, factor_16));
				s2 = _mm_add_epi32(s2, _mm_madd_epi16(s1_lo, factor_lo));
				s2 = _mm_add_epi32(s2, _mm_madd_epi16(s1_hi, factor_hi));

				p += 256;
			}

			s1 = _mm_add_epi32(s1, _mm_shuffle_epi32(s1, _MM_SHUFFLE(2,3,0,1)));
			s2 = _mm_add_epi32(s2, _mm_shuffle_epi32(s2, _MM_SHUFFLE(2,3,0,1)));
			s1 = _mm_add_epi32(s1, _mm_shuffle_epi32(s1, _MM_SHUFFLE(1,0,3,2)));
			s2 = _mm_add_epi32(s2, _mm_shuffle_epi32(s2, _MM_SHUFFLE(1,0,3,2)));

			b += chunk_size * a;
			a += (uint32_t)_mm_cvtsi128_si32(s1);
			b += (uint32_t)_mm_cvtsi128_si32(s2);
		}
#elif UFBX_LITTLE_ENDIAN
		for (;;) {
			size_t chunk_size = ufbxi_min_sz(ufbxi_to_size(end - p), 256*8/4) & ~(size_t)0xf;
			if (chunk_size == 0) break;
			const char *chunk_end = p + chunk_size;

			uint64_t s1_lo = 0, s1_hi = 0;
			uint64_t tmp, s2 = 0;
			uint64_t mask8  = UINT64_C(0x00ff00ff00ff00ff);
			uint64_t mask16 = UINT64_C(0x0000ffff0000ffff);

			while (p != chunk_end) {
				uint64_t d0 = *(const uint64_t*)p;
				uint64_t d1 = *(const uint64_t*)(p + 8);

				tmp = s1_lo + s1_hi;
				s1_lo += d0 & mask8;
				s1_hi += (d0 >> 8) & mask8;

				tmp += s1_lo + s1_hi;
				s1_lo += d1 & mask8;
				s1_hi += (d1 >> 8) & mask8;

				s2 += (tmp & mask16) + ((tmp >> 16) & mask16);
				p += 16;
			}

			uint64_t s1 = s1_lo + s1_hi;
			s1 = (s1 & mask16) + ((s1 >> 16u) & mask16);
			ufbxi_fast_uint s1_sum = (ufbxi_fast_uint)(s1 + (s1 >> 32u));

			ufbxi_fast_uint s2_sum = (ufbxi_fast_uint)(s2 + (s2 >> 32u)) * 8;
			s2_sum += ((ufbxi_fast_uint)(s1_lo >>  0) & 0xffff) * 8;
			s2_sum += ((ufbxi_fast_uint)(s1_hi >>  0) & 0xffff) * 7;
			s2_sum += ((ufbxi_fast_uint)(s1_lo >> 16) & 0xffff) * 6;
			s2_sum += ((ufbxi_fast_uint)(s1_hi >> 16) & 0xffff) * 5;
			s2_sum += ((ufbxi_fast_uint)(s1_lo >> 32) & 0xffff) * 4;
			s2_sum += ((ufbxi_fast_uint)(s1_hi >> 32) & 0xffff) * 3;
			s2_sum += ((ufbxi_fast_uint)(s1_lo >> 48) & 0xffff) * 2;
			s2_sum += ((ufbxi_fast_uint)(s1_hi >> 48) & 0xffff) * 1;

			b += chunk_size * a;
			a += s1_sum & 0xffffffffu;
			b += s2_sum & 0xffffffffu;
		}
#endif

		while (p != end) {
			a += (size_t)(uint8_t)p[0]; b += a;
			p++;
		}

		a %= 65521u;
		b %= 65521u;
	}

	return (uint32_t)((b << 16) | (a & 0xffff));
}

static ufbxi_noinline int
ufbxi_inflate_block_slow(ufbxi_deflate_context *dc, ufbxi_trees *trees, size_t max_symbols)
{
	char *out_ptr = dc->out_ptr;
	char *const out_begin = dc->out_begin;
	char *const out_end = dc->out_end;

	uint32_t fast_bits = trees->fast_bits;
	uint32_t fast_mask = (1u << fast_bits) - 1;

	uint64_t bits = dc->stream.bits;
	size_t left = dc->stream.left;
	const char *data = dc->stream.chunk_ptr;

	for (;;) {
		if (max_symbols-- == 0) break;

		ufbxi_bit_refill(&bits, &left, &data, &dc->stream);
		uint64_t sym_bits = bits;

		ufbxi_huff_sym sym0 = ufbxi_huff_decode_bits(&trees->lit_length, bits, fast_bits, fast_mask);
		ufbxi_regression_assert(sym0 != UFBXI_HUFF_UNINITIALIZED_SYM);

		uint32_t sym0_bits = ufbxi_huff_sym_total_bits(sym0);

		bits >>= sym0_bits;
		left -= sym0_bits;
		if (sym0 & UFBXI_HUFF_SYM_END) {
			if (ufbxi_huff_sym_value(sym0) != 0) return -13;

			dc->out_ptr = out_ptr;
			dc->stream.bits = bits;
			dc->stream.left = left;
			dc->stream.chunk_ptr = data;
			return 0;
		} else if ((sym0 & UFBXI_HUFF_SYM_MATCH) == 0) {
			if (out_ptr == out_end) return -10;
			*out_ptr++ = (char)ufbxi_huff_sym_value(sym0);
			continue;
		}

		uint32_t sym0_value = ufbxi_huff_sym_value(sym0);
		uint32_t len_shift_base = trees->lit_length.extra_shift_base[sym0_value];
		uint16_t len_mask = trees->lit_length.extra_mask[sym0_value];
		uint32_t length = (len_shift_base >> 16) + (uint32_t)(ufbxi_wrap_shr64(sym_bits, len_shift_base) & len_mask);

		ufbxi_huff_sym sym1 = ufbxi_huff_decode_bits(&trees->dist, bits, fast_bits, fast_mask);
		ufbxi_regression_assert(sym1 != UFBXI_HUFF_UNINITIALIZED_SYM);
		if (sym1 & UFBXI_HUFF_SYM_END) return -11;

		uint32_t sym1_bits = ufbxi_huff_sym_total_bits(sym1);

		bits >>= sym1_bits;
		left -= sym1_bits;

		uint32_t sym1_value = ufbxi_huff_sym_value(sym1);
		uint32_t dist_shift_base = trees->dist.extra_shift_base[sym1_value];
		uint16_t dist_mask = trees->dist.extra_mask[sym1_value];
		uint32_t distance = (dist_shift_base >> 16) + (uint32_t)(ufbxi_wrap_shr64(sym_bits, dist_shift_base + sym0) & dist_mask);

		// Bounds checking
		size_t out_space = ufbxi_to_size(out_end - out_ptr);
		if ((ptrdiff_t)distance > out_ptr - out_begin || length > out_space) {
			return -12;
		}

		// Copy the match
		const char *src = out_ptr - distance;
		char *dst = out_ptr;
		char *end = dst + length;
		out_ptr += length;

		if (out_space >= length + 16) {
			uint32_t min_dist = length < 16 ? length : 16;
			if (distance >= min_dist) {
				ufbxi_copy_16_bytes(dst, src);
				while (length > 16) {
					src += 16;
					dst += 16;
					length -= 16;
					ufbxi_copy_16_bytes(dst, src);
				}
			} else {
				while (dst != end) {
					*dst++ = *src++;
				}
			}
		} else {
			while (dst != end) {
				*dst++ = *src++;
			}
		}
	}

	dc->out_ptr = out_ptr;
	dc->stream.bits = bits;
	dc->stream.left = left;
	dc->stream.chunk_ptr = data;
	return 1;
}

ufbx_static_assert(inflate_huff_fast_bits, UFBXI_HUFF_FAST_BITS <= 11); // `fast lit, fast len, slow dist` in 56 bits
ufbx_static_assert(inflate_huff_long_bits, UFBXI_HUFF_FAST_BITS + UFBXI_HUFF_MAX_LONG_BITS >= 15); // Largest code fits in a single long lookup

// Optimized version of `ufbxi_inflate_block_slow()`.
// Has a lot of assumptions (see asserts) and does not call _any_ (even forceinlined) functions.
static ufbxi_noinline int
ufbxi_inflate_block_fast(ufbxi_deflate_context *dc, ufbxi_trees *trees)
{
	ufbxi_dev_assert(!dc->stream.cancelled);
	ufbxi_dev_assert(trees->fast_bits == UFBXI_HUFF_FAST_BITS);
	ufbxi_dev_assert(dc->stream.chunk_yield - dc->stream.chunk_ptr >= UFBXI_INFLATE_FAST_MIN_IN);
	ufbxi_dev_assert(dc->out_end - dc->out_ptr >= UFBXI_INFLATE_FAST_MIN_OUT);

	char *out_ptr = dc->out_ptr;
	char *const out_begin = dc->out_begin;
	char *const out_end = dc->out_end - UFBXI_INFLATE_FAST_MIN_OUT;

	const ufbxi_huff_tree *tree_lit_length = &trees->lit_length;
	const ufbxi_huff_tree *tree_dist = &trees->dist;

	uint64_t bits = dc->stream.bits;
	size_t left = dc->stream.left;
	const char *data = dc->stream.chunk_ptr;
	const char *data_end = dc->stream.chunk_yield - UFBXI_INFLATE_FAST_MIN_IN;

	uint64_t sym01_bits;
	ufbxi_huff_sym sym0, sym1;
	uint64_t refill_bits = ufbxi_read_u64(data);

	#define ufbxi_fast_inflate_refill_and_decode() do { \
		ufbxi_macro_bit_refill_fast(bits, left, data, refill_bits); \
		sym01_bits = bits; \
		sym0 = tree_lit_length->fast_sym[sym01_bits & UFBXI_HUFF_FAST_MASK]; \
		sym1 = ((sym0 & UFBXI_HUFF_SYM_MATCH) ? tree_dist : tree_lit_length)->fast_sym[ufbxi_wrap_shr64(sym01_bits, sym0) & UFBXI_HUFF_FAST_MASK]; \
		refill_bits = ufbxi_read_u64(data); \
	} while (0)

	#define ufbxi_fast_inflate_should_continue() \
		(((data_end - data) | (out_end - out_ptr)) >= 0)

	ufbxi_fast_inflate_refill_and_decode();

	for (;;) {
		if ((sym0 & sym1) & UFBXI_HUFF_SYM_FAST) {
			bits = ufbxi_wrap_shr64(sym01_bits, sym0 + sym1);
			left -= (sym0 + sym1) & 0x3f;

			if (((sym0 | sym1) & UFBXI_HUFF_SYM_MATCH) == 0) {
				// Literal, Literal
				// -> Output the two literals and loop back to start.

				out_ptr[0] = (char)ufbxi_huff_sym_value(sym0);
				out_ptr[1] = (char)ufbxi_huff_sym_value(sym1);
				out_ptr += 2;

				ufbxi_fast_inflate_refill_and_decode();
				if (ufbxi_fast_inflate_should_continue()) continue;
				break;

			} else if ((sym0 & UFBXI_HUFF_SYM_MATCH) == 0) {
				// Literal, Match, (Distance)
				// -> Output a single literal, decode the missing distance and fall through to match.

				out_ptr[0] = (char)ufbxi_huff_sym_value(sym0);
				out_ptr += 1;

				sym01_bits = ufbxi_wrap_shr64(sym01_bits, sym0);

				// This must fit as literals never have extra bits and the match length is fast so:
				// 10 (lit) + 10 (len code) + 5 (len extra) + 15 (dist code) + 13 (dist extra) = 53 <= 56
				sym0 = sym1;
				sym1 = tree_dist->fast_sym[bits & UFBXI_HUFF_FAST_MASK];

				if ((sym1 & UFBXI_HUFF_SYM_FAST) == 0) {
					// Slow sym1
					if (sym1 & UFBXI_HUFF_SYM_END) return -11;
					uint32_t tail = (uint32_t)(bits >> UFBXI_HUFF_FAST_BITS);
					uint32_t long_mask = ufbxi_huff_sym_long_mask(sym1);
					sym1 = tree_dist->long_sym[ufbxi_huff_sym_long_offset(sym1) + (tail & long_mask)];
					if (sym1 & UFBXI_HUFF_SYM_END) return -11;
				}

				bits = ufbxi_wrap_shr64(bits, sym1);
				left -= sym1 & 0x3f;
			} else {
				// Match, Distance
				// -> Fall through to match copy.
			}

		} else {
			if ((sym0 & (UFBXI_HUFF_SYM_FAST|UFBXI_HUFF_SYM_END)) == 0) {
				// Slow sym0
				uint32_t tail = (uint32_t)(sym01_bits >> UFBXI_HUFF_FAST_BITS);
				uint32_t long_mask = ufbxi_huff_sym_long_mask(sym0);
				sym0 = tree_lit_length->long_sym[ufbxi_huff_sym_long_offset(sym0) + (tail & long_mask)];
			}

			uint32_t sym0_bits = ufbxi_huff_sym_total_bits(sym0);
			bits >>= sym0_bits;
			left -= sym0_bits;

			if (sym0 & UFBXI_HUFF_SYM_END) {
				if (ufbxi_huff_sym_value(sym0) != 0) return -13;
				dc->out_ptr = out_ptr;
				dc->stream.bits = bits;
				dc->stream.left = left;
				dc->stream.chunk_ptr = data;
				return 0;
			}

			if (sym0 & UFBXI_HUFF_SYM_MATCH) {
				sym1 = tree_dist->fast_sym[bits & UFBXI_HUFF_FAST_MASK];

				if ((sym1 & UFBXI_HUFF_SYM_FAST) == 0) {
					// Slow sym1
					if (sym1 & UFBXI_HUFF_SYM_END) return -11;
					uint32_t tail = (uint32_t)(bits >> UFBXI_HUFF_FAST_BITS);
					uint32_t long_mask = ufbxi_huff_sym_long_mask(sym1);
					sym1 = tree_dist->long_sym[ufbxi_huff_sym_long_offset(sym1) + (tail & long_mask)];
					if (sym1 & UFBXI_HUFF_SYM_END) return -11;
				}

				bits = ufbxi_wrap_shr64(bits, sym1);
				left -= sym1 & 0x3f;
			} else {
				*out_ptr++ = (char)ufbxi_huff_sym_value(sym0);

				ufbxi_fast_inflate_refill_and_decode();
				if (ufbxi_fast_inflate_should_continue()) continue;
				break;
			}
		}

		uint32_t sym0_value = ufbxi_huff_sym_value(sym0);
		uint32_t len_shift_base = trees->lit_length.extra_shift_base[sym0_value];
		uint16_t len_mask = trees->lit_length.extra_mask[sym0_value];
		uint32_t length = (len_shift_base >> 16) + (uint32_t)(ufbxi_wrap_shr64(sym01_bits, len_shift_base) & len_mask);

		uint32_t sym1_value = ufbxi_huff_sym_value(sym1);
		uint32_t dist_shift_base = trees->dist.extra_shift_base[sym1_value];
		uint16_t dist_mask = trees->dist.extra_mask[sym1_value];
		uint32_t distance = (dist_shift_base >> 16) + (uint32_t)(ufbxi_wrap_shr64(sym01_bits, dist_shift_base + sym0) & dist_mask);

		ufbxi_fast_inflate_refill_and_decode();

		// Bounds checking: We don't actually handle the error here, just bail out to the slow implementation
		ptrdiff_t dst_space = out_end - out_ptr - (ptrdiff_t)length + UFBXI_INFLATE_FAST_MIN_OUT;
		ptrdiff_t src_space = out_ptr - out_begin - (ptrdiff_t)distance;
		if ((dst_space | src_space) < 0) {
			return -12;
		}

		const char *src = out_ptr - distance;
		char *dst = out_ptr;
		char *end = dst + length;
		out_ptr += length;

		// Copy the match

		uint32_t min_dist = length < 16 ? length : 16;
		if (distance >= min_dist && dst_space >= 16) {
			ufbxi_copy_16_bytes(dst, src);
			while (length > 16) {
				src += 16;
				dst += 16;
				length -= 16;
				ufbxi_copy_16_bytes(dst, src);
			}
		} else {
			while (dst != end) {
				*dst++ = *src++;
			}
		}

		if (ufbxi_fast_inflate_should_continue()) continue;
		break;
	}

	dc->out_ptr = out_ptr;
	dc->stream.bits = bits;
	dc->stream.left = left;
	dc->stream.chunk_ptr = data;
	return 1;

	#undef ufbxi_fast_inflate_refill_and_decode
	#undef ufbxi_fast_inflate_should_continue
}

static void ufbxi_inflate_init_retain(ufbx_inflate_retain *retain)
{
	ufbxi_inflate_retain_imp *ret_imp = (ufbxi_inflate_retain_imp*)retain;
	if (!ret_imp->initialized) {
		ufbxi_init_static_huff(&ret_imp->static_trees, NULL);
		ret_imp->initialized = true;
	}
}

// TODO: Error codes should have a quick test if the destination buffer overflowed
// Returns actual number of decompressed bytes or negative error:
// -1: Bad compression method (ZLIB header)
// -2: Requires dictionary (ZLIB header)
// -3: Bad FCHECK (ZLIB header)
// -4: Bad NLEN (Uncompressed LEN != ~NLEN)
// -5: Uncompressed source overflow
// -6: Uncompressed destination overflow
// -7: Bad block type
// -8: Truncated checksum (deprecated, reported as -9)
// -9: Checksum mismatch
// -10: Literal destination overflow
// -11: Bad distance code or distance of (30..31)
// -12: Match out of bounds
// -13: Bad lit/length code
// -14: Codelen Huffman Overfull
// -15: Codelen Huffman Underfull
// -16 - -21: Litlen Huffman: Overfull / Underfull / Repeat 16/17/18 overflow / Bad length code
// -22 - -27: Distance Huffman: Overfull / Underfull / Repeat 16/17/18 overflow / Bad length code
// -28: Cancelled
// -29: Invalid ufbx_inflate_input.internal_fast_bits value
ufbxi_extern_c ptrdiff_t ufbx_inflate(void *dst, size_t dst_size, const ufbx_inflate_input *input, ufbx_inflate_retain *retain)
{
	ufbxi_inflate_retain_imp *ret_imp = (ufbxi_inflate_retain_imp*)retain;

	ptrdiff_t err;
	ufbxi_deflate_context dc;
	ufbxi_bit_stream_init(&dc.stream, input);
	dc.out_begin = (char*)dst;
	dc.out_ptr = (char*)dst;
	dc.out_end = (char*)dst + dst_size;
	if (input->internal_fast_bits != 0) {
		dc.fast_bits = (uint32_t)input->internal_fast_bits;
		if (dc.fast_bits < 1 || dc.fast_bits == 9 || dc.fast_bits > 10) return -29;
	} else {
		// TODO: Profile this
		dc.fast_bits = input->total_size > 2048 ? 10 : 8;
	}

	uint64_t bits = dc.stream.bits;
	size_t left = dc.stream.left;
	const char *data = dc.stream.chunk_ptr;

	ufbxi_bit_refill(&bits, &left, &data, &dc.stream);
	if (dc.stream.cancelled) return -28;

	// Zlib header
	if (!input->no_header) {
		size_t cmf = (size_t)(bits & 0xff);
		size_t flg = (size_t)(bits >> 8) & 0xff;
		bits >>= 16;
		left -= 16;

		if ((cmf & 0xf) != 0x8) return -1;
		if ((flg & 0x20) != 0) return -2;
		if ((cmf << 8 | flg) % 31u != 0) return -3;
	}

	for (;;) {
		ufbxi_bit_refill(&bits, &left, &data, &dc.stream);
		if (dc.stream.cancelled) return -28;

		// Block header: [0:1] BFINAL [1:3] BTYPE
		size_t header = (size_t)bits & 0x7;
		bits >>= 3;
		left -= 3;

		size_t type = header >> 1;
		if (type == 0) {

			// Round up to the next byte
			size_t align_bits = left & 0x7;
			bits >>= align_bits;
			left -= align_bits;

			size_t len = (size_t)(bits & 0xffff);
			size_t nlen = (size_t)((bits >> 16) & 0xffff);
			if ((len ^ nlen) != 0xffff) return -4;
			if (dc.out_end - dc.out_ptr < (ptrdiff_t)len) return -6;
			bits >>= 32;
			left -= 32;

			dc.stream.bits = bits;
			dc.stream.left = left;
			dc.stream.chunk_ptr = data;

			// Copy `len` bytes of literal data
			if (!ufbxi_bit_copy_bytes(dc.out_ptr, &dc.stream, len)) return -5;

			dc.out_ptr += len;

		} else if (type <= 2) {

			dc.stream.bits = bits;
			dc.stream.left = left;
			dc.stream.chunk_ptr = data;

			ufbxi_trees tree_data; // ufbxi_uninit
			ufbxi_trees *trees; // ufbxi_uninit
			if (type == 1) {
				// Static Huffman: Initialize the trees once and cache them in `retain`.
				if (!ret_imp->initialized) {
					ufbxi_init_static_huff(&ret_imp->static_trees, input);
					ret_imp->initialized = true;
				}
				trees = &ret_imp->static_trees;
			} else {
				// Dynamic Huffman
				err = ufbxi_init_dynamic_huff(&dc, &tree_data);
				if (err) return err;
				trees = &tree_data;
			}

			for (;;) {
				bool fast_viable = trees->fast_bits == UFBXI_HUFF_FAST_BITS && dc.out_end - dc.out_ptr >= UFBXI_INFLATE_FAST_MIN_OUT;

				// `ufbxi_inflate_block_fast()` needs a bit more upfront setup, see asserts on top of the function
				if (fast_viable && dc.stream.chunk_yield - dc.stream.chunk_ptr >= UFBXI_INFLATE_FAST_MIN_IN) {
					err = ufbxi_inflate_block_fast(&dc, trees);
				} else {
					err = ufbxi_inflate_block_slow(&dc, trees, fast_viable ? 32 : SIZE_MAX);
				}

				if (err < 0) return err;

				// `ufbxi_inflate_block()` returns normally on cancel so check it here
				if (dc.stream.cancelled) return -28;

				if (err == 0) break;
			}

		} else {
			// 0b11 - reserved (error)
			return -7;
		}

		bits = dc.stream.bits;
		left = dc.stream.left;
		data = dc.stream.chunk_ptr;

		// BFINAL: End of stream
		if (header & 1) break;
	}

	// Check Adler-32
	{
		// Round up to the next byte
		size_t align_bits = left & 0x7;
		bits >>= align_bits;
		left -= align_bits;
		ufbxi_bit_refill(&bits, &left, &data, &dc.stream);
		if (dc.stream.cancelled) return -28;

		if (!input->no_checksum) {
			uint32_t ref = (uint32_t)bits;
			ref = (ref>>24) | ((ref>>8)&0xff00) | ((ref<<8)&0xff0000) | (ref<<24);

			uint32_t checksum = ufbxi_adler32(dc.out_begin, ufbxi_to_size(dc.out_ptr - dc.out_begin));
			if (ref != checksum) {
				return -9;
			}
		}
	}

	return dc.out_ptr - dc.out_begin;
}

#endif // !defined(ufbx_inflate)

// -- Printf

typedef struct {
	char *dst;
	size_t length;
	size_t pos;
} ufbxi_print_buffer;

#define UFBXI_PRINT_UNSIGNED 0x1
#define UFBXI_PRINT_STRING 0x2
#define UFBXI_PRINT_SIZE_T 0x10

static void ufbxi_print_append(ufbxi_print_buffer *buf, size_t min_width, size_t max_width, const char *str)
{
	size_t width = 0;
	for (width = 0; width < max_width; width++) {
		if (!str[width]) break;
	}
	size_t pad = min_width > width ? min_width - width : 0;
	for (size_t i = 0; i < pad; i++) {
		if (buf->pos < buf->length) buf->dst[buf->pos++] = ' ';
	}
	for (size_t i = 0; i < width; i++) {
		if (buf->pos < buf->length) buf->dst[buf->pos++] = str[i];
	}
}

static char *ufbxi_print_format_int(char *buffer, uint64_t value)
{
	*--buffer = '\0';
	do {
		uint32_t digit = (uint32_t)(value % 10);
		value = value / 10;
		*--buffer = (char)('0' + digit);
	} while (value > 0);
	return buffer;
}

static void ufbxi_vprint(ufbxi_print_buffer *buf, const char *fmt, va_list args)
{
	char buffer[96]; // ufbxi_uninit
	for (const char *p = fmt; *p;) {
		if (*p == '%' && *++p != '%') {
			size_t min_width = 0, max_width = SIZE_MAX;
			if (*p == '*') {
				p++;
				min_width = (size_t)va_arg(args, int);
			}
			if (*p == '.') {
				ufbxi_dev_assert(p[1] == '*');
				p += 2;
				max_width = (size_t)va_arg(args, int);
			}
			uint32_t flags = 0;
			switch (*p) {
			case 'z': p++; flags |= UFBXI_PRINT_SIZE_T; break;
			default: break;
			}
			switch (*p++) {
			case 'u': flags |= UFBXI_PRINT_UNSIGNED; break;
			case 's': flags |= UFBXI_PRINT_STRING; break;
			default: break;
			}
			if (flags & UFBXI_PRINT_STRING) {
				const char *str = va_arg(args, const char*);
				ufbxi_print_append(buf, min_width, max_width, str);
			} else if (flags & UFBXI_PRINT_UNSIGNED) {
				uint64_t value = (flags & UFBXI_PRINT_SIZE_T) != 0 ? (uint64_t)va_arg(args, size_t) : (uint64_t)va_arg(args, uint32_t);
				char *str = ufbxi_print_format_int(buffer + sizeof(buffer), value);
				ufbxi_print_append(buf, min_width, max_width, str);
			} else {
				ufbxi_unreachable("Bad printf format");
			}
		} else {
			if (buf->pos < buf->length) buf->dst[buf->pos++] = *p;
			p++;
		}
	}
	if (buf->length && buf->dst) {
		size_t end = buf->pos <= buf->length - 1 ? buf->pos : buf->length - 1;
		buf->dst[end] = '\0';
	}
}

// -- Errors

static const char ufbxi_empty_char[1] = { '\0' };

static ufbxi_noinline int ufbxi_vsnprintf(char *buf, size_t buf_size, const char *fmt, va_list args)
{
	ufbxi_print_buffer buffer = { buf, buf_size };
	ufbxi_vprint(&buffer, fmt, args);
	return (int)ufbxi_min_sz(buffer.pos, buf_size - 1);
}

static ufbxi_noinline int ufbxi_snprintf(char *buf, size_t buf_size, const char *fmt, ...)
{
	va_list args; // ufbxi_uninit
	va_start(args, fmt);
	int result = ufbxi_vsnprintf(buf, buf_size, fmt, args);
	va_end(args);
	return result;
}

static ufbxi_noinline void ufbxi_panicf_imp(ufbx_panic *panic, const char *fmt, ...)
{
	if (panic && panic->did_panic) return;

	va_list args; // ufbxi_uninit

	if (panic) {
		va_start(args, fmt);
		panic->did_panic = true;
		panic->message_length = (size_t)ufbxi_vsnprintf(panic->message, sizeof(panic->message), fmt, args);
		va_end(args);
	} else {
		va_start(args, fmt);
		char message[UFBX_PANIC_MESSAGE_LENGTH];
		ufbxi_vsnprintf(message, sizeof(message), fmt, args);
		va_end(args);

		ufbx_panic_handler(message);
	}
}

#define ufbxi_panicf(panic, cond, ...) \
	((cond) ? false : (ufbxi_panicf_imp((panic), __VA_ARGS__), true))

// Prefix the error condition with $Description\0 for a human readable description
#define ufbxi_error_msg(cond, msg) "$" msg "\0" cond

static ufbxi_noinline int ufbxi_fail_imp_err(ufbx_error *err, const char *cond, const char *func, uint32_t line)
{
	if (cond[0] == '$') {
		if (!err->description.data) {
			err->description.data = cond + 1;
			err->description.length = strlen(err->description.data);
		}

#if UFBXI_FEATURE_ERROR_STACK
		// Skip the description part if adding to a stack
		cond = cond + strlen(cond) + 1;
#endif
	}

	// NOTE: This is the base function all fails boil down to, place a breakpoint here to
	// break at the first error
#if UFBXI_FEATURE_ERROR_STACK
	if (err->stack_size < UFBX_ERROR_STACK_MAX_DEPTH) {
		ufbx_error_frame *frame = &err->stack[err->stack_size++];
		frame->description.data = cond;
		frame->description.length = strlen(cond);
		frame->function.data = func;
		frame->function.length = strlen(func);
		frame->source_line = line;
	}
#else
	ufbxi_ignore(func);
	ufbxi_ignore(line);
#endif

	return 0;
}

ufbxi_nodiscard static ufbxi_noinline size_t ufbxi_utf8_valid_length(const char *str, size_t length)
{
	size_t index = 0;
	while (index < length) {
		uint8_t c = (uint8_t)str[index];
		size_t left = length - index;

		if ((c & 0x80) == 0) {
			if (c != 0) {
				index += 1;
				continue;
			}
		} else if ((c & 0xe0) == 0xc0 && left >= 2) {
			uint8_t t0 = (uint8_t)str[index + 1];
			uint32_t code = (uint32_t)c << 8 | (uint32_t)t0;
			if ((code & 0xc0) == 0x80 && code >= 0xc280) {
				index += 2;
				continue;
			}
		} else if ((c & 0xf0) == 0xe0 && left >= 3) {
			uint8_t t0 = (uint8_t)str[index + 1], t1 = (uint8_t)str[index + 2];
			uint32_t code = (uint32_t)c << 16 | (uint32_t)t0 << 8 | (uint32_t)t1;
			if ((code & 0xc0c0) == 0x8080 && code >= 0xe0a080 && (code < 0xeda080 || code >= 0xee8080)) {
				index += 3;
				continue;
			}
		} else if ((c & 0xf8) == 0xf0 && left >= 4) {
			uint8_t t0 = (uint8_t)str[index + 1], t1 = (uint8_t)str[index + 2], t2 = (uint8_t)str[index + 3];
			uint32_t code = (uint32_t)c << 24 | (uint32_t)t0 << 16 | (uint32_t)t1 << 8 | (uint32_t)t2;
			if ((code & 0xc0c0c0) == 0x808080 && code >= 0xf0908080u && code <= 0xf48fbfbfu) {
				index += 4;
				continue;
			}
		}

		break;
	}

	ufbx_assert(index <= length);
	return index;
}

static ufbxi_noinline void ufbxi_clean_string_utf8(char *str, size_t length)
{
	size_t pos = 0;
	for (;;) {
		pos += ufbxi_utf8_valid_length(str + pos, length);
		if (pos == length) break;
		str[pos++] = '?';
	}
}

static ufbxi_noinline void ufbxi_set_err_info(ufbx_error *err, const char *data, size_t length)
{
	if (!err) return;

	if (length == SIZE_MAX) length = strlen(data);
	size_t to_copy = ufbxi_min_sz(sizeof(err->info) - 1, length);
	memcpy(err->info, data, to_copy);
	err->info[to_copy] = '\0';
	err->info_length = to_copy;
	ufbxi_clean_string_utf8(err->info, err->info_length);
}

static ufbxi_noinline void ufbxi_fmt_err_info(ufbx_error *err, const char *fmt, ...)
{
	if (!err) return;

	va_list args; // ufbxi_uninit
	va_start(args, fmt);
	err->info_length = (size_t)ufbxi_vsnprintf(err->info, sizeof(err->info), fmt, args);
	va_end(args);
	ufbxi_clean_string_utf8(err->info, err->info_length);
}

static ufbxi_noinline void ufbxi_clear_error(ufbx_error *err)
{
	if (!err) return;

	err->type = UFBX_ERROR_NONE;
	err->description.data = ufbxi_empty_char;
	err->description.length = 0;
	err->stack_size = 0;
	err->info[0] = '\0';
	err->info_length = 0;
}

#if UFBXI_FEATURE_ERROR_STACK
	#define ufbxi_function __FUNCTION__
	#define ufbxi_line __LINE__
	#define ufbxi_cond_str(cond) #cond
#else
	#define ufbxi_function ""
	#define ufbxi_line 0
	#define ufbxi_cond_str(cond) ""
#endif

#define ufbxi_check_err(err, cond) do { if (ufbxi_unlikely(!ufbxi_trace(cond))) { ufbxi_fail_imp_err((err), ufbxi_cond_str(cond), ufbxi_function, ufbxi_line); return 0; } } while (0)
#define ufbxi_check_return_err(err, cond, ret) do { if (ufbxi_unlikely(!ufbxi_trace(cond))) { ufbxi_fail_imp_err((err), ufbxi_cond_str(cond), ufbxi_function, ufbxi_line); return ret; } } while (0)
#define ufbxi_fail_err(err, desc) return ufbxi_fail_imp_err(err, desc, ufbxi_function, ufbxi_line)

#define ufbxi_check_err_msg(err, cond, msg) do { if (ufbxi_unlikely(!ufbxi_trace(cond))) { ufbxi_fail_imp_err((err), ufbxi_error_msg(ufbxi_cond_str(cond), msg), ufbxi_function, ufbxi_line); return 0; } } while (0)
#define ufbxi_check_return_err_msg(err, cond, ret, msg) do { if (ufbxi_unlikely(!ufbxi_trace(cond))) { ufbxi_fail_imp_err((err), ufbxi_error_msg(ufbxi_cond_str(cond), msg), ufbxi_function, ufbxi_line); return ret; } } while (0)
#define ufbxi_fail_err_msg(err, desc, msg) return ufbxi_fail_imp_err(err, ufbxi_error_msg(desc, msg), ufbxi_function, ufbxi_line)
#define ufbxi_report_err_msg(err, desc, msg) (void)ufbxi_fail_imp_err(err, ufbxi_error_msg(desc, msg), ufbxi_function, ufbxi_line)

static ufbxi_noinline void ufbxi_fix_error_type(ufbx_error *error, const char *default_desc, ufbx_error *p_error)
{
	const char *desc = error->description.data;
	if (!desc) desc = default_desc;
	error->type = UFBX_ERROR_UNKNOWN;
	if (!strcmp(desc, "Out of memory")) {
		error->type = UFBX_ERROR_OUT_OF_MEMORY;
	} else if (!strcmp(desc, "Memory limit exceeded")) {
		error->type = UFBX_ERROR_MEMORY_LIMIT;
	} else if (!strcmp(desc, "Allocation limit exceeded")) {
		error->type = UFBX_ERROR_ALLOCATION_LIMIT;
	} else if (!strcmp(desc, "Truncated file")) {
		error->type = UFBX_ERROR_TRUNCATED_FILE;
	} else if (!strcmp(desc, "IO error")) {
		error->type = UFBX_ERROR_IO;
	} else if (!strcmp(desc, "Cancelled")) {
		error->type = UFBX_ERROR_CANCELLED;
	} else if (!strcmp(desc, "Unrecognized file format")) {
		error->type = UFBX_ERROR_UNRECOGNIZED_FILE_FORMAT;
	} else if (!strcmp(desc, "File not found")) {
		error->type = UFBX_ERROR_FILE_NOT_FOUND;
	} else if (!strcmp(desc, "Empty file")) {
		error->type = UFBX_ERROR_EMPTY_FILE;
	} else if (!strcmp(desc, "External file not found")) {
		error->type = UFBX_ERROR_EXTERNAL_FILE_NOT_FOUND;
	} else if (!strcmp(desc, "Uninitialized options")) {
		error->type = UFBX_ERROR_UNINITIALIZED_OPTIONS;
	} else if (!strcmp(desc, "Zero vertex size")) {
		error->type = UFBX_ERROR_ZERO_VERTEX_SIZE;
	} else if (!strcmp(desc, "Truncated vertex stream")) {
		error->type = UFBX_ERROR_TRUNCATED_VERTEX_STREAM;
	} else if (!strcmp(desc, "Invalid UTF-8")) {
		error->type = UFBX_ERROR_INVALID_UTF8;
	} else if (!strcmp(desc, "Feature disabled")) {
		error->type = UFBX_ERROR_FEATURE_DISABLED;
	} else if (!strcmp(desc, "Bad NURBS geometry")) {
		error->type = UFBX_ERROR_BAD_NURBS;
	} else if (!strcmp(desc, "Bad index")) {
		error->type = UFBX_ERROR_BAD_INDEX;
	} else if (!strcmp(desc, "Node depth limit exceeded")) {
		error->type = UFBX_ERROR_NODE_DEPTH_LIMIT;
	} else if (!strcmp(desc, "Threaded ASCII parse error")) {
		error->type = UFBX_ERROR_THREADED_ASCII_PARSE;
	} else if (!strcmp(desc, "Unsafe options")) {
		error->type = UFBX_ERROR_UNSAFE_OPTIONS;
	} else if (!strcmp(desc, "Duplicate override")) {
		error->type = UFBX_ERROR_DUPLICATE_OVERRIDE;
	}
	error->description.data = desc;
	error->description.length = strlen(desc);
	if (p_error) {
		memcpy(p_error, error, sizeof(ufbx_error));
	}
}

// -- Allocator

// Returned for zero size allocations, place in the constant data
// to catch writes to bad allocations.
#if defined(UFBX_REGRESSION)
static const char ufbxi_zero_size_buffer[4096] = { 0 };
#else
static const char ufbxi_zero_size_buffer[64] = { 0 };
#endif

static ufbxi_forceinline size_t ufbxi_align_to_mask(size_t value, size_t align_mask)
{
	return value + (((size_t)0 - value) & align_mask);
}

static ufbxi_forceinline size_t ufbxi_size_align_mask(size_t size)
{
	// Align to the all bits below the lowest set one in `size`
	// up to a maximum of 0x7 (align to 8 bytes).
	return ((size ^ (size - 1)) >> 1) & 0x7;
}

typedef struct {
	ufbx_error *error;
	size_t current_size;
	size_t max_size;
	size_t num_allocs;
	size_t max_allocs;
	size_t huge_size;
	size_t chunk_max;
	ufbx_allocator_opts ator;
	const char *name;
} ufbxi_allocator;

static ufbxi_forceinline bool ufbxi_does_overflow(size_t total, size_t a, size_t b)
{
	// If `a` and `b` have at most 4 bits per `size_t` byte, the product can't overflow.
	if (((a | b) >> sizeof(size_t)*4) != 0) {
		if (a != 0 && total / a != b) return true;
	}
	return false;
}

static ufbxi_noinline void *ufbxi_alloc_size(ufbxi_allocator *ator, size_t size, size_t n)
{
	// Always succeed with an empty non-NULL buffer for empty allocations
	ufbx_assert(size > 0);
	if (n == 0) return (void*)ufbxi_zero_size_buffer;

	size_t total = size * n;
	ufbxi_check_return_err(ator->error, !ufbxi_does_overflow(total, size, n), NULL);
	ufbxi_check_return_err(ator->error, total <= SIZE_MAX / 2, NULL); // Make sure it's always safe to double allocations
	if (!(total < ator->max_size - ator->current_size)) {
		ufbxi_report_err_msg(ator->error, "total <= ator->max_size - ator->current_size", "Memory limit exceeded");
		ufbxi_fmt_err_info(ator->error, "%s", ator->name);
		return NULL;
	}
	if (!(ator->num_allocs < ator->max_allocs)) {
		ufbxi_report_err_msg(ator->error, "ator->num_allocs < ator->max_allocs", "Allocation limit exceeded");
		ufbxi_fmt_err_info(ator->error, "%s", ator->name);
		return NULL;
	}
	ator->num_allocs++;

	void *ptr;
	if (ator->ator.allocator.alloc_fn) {
		ptr = ator->ator.allocator.alloc_fn(ator->ator.allocator.user, total);
	} else if (ator->ator.allocator.realloc_fn) {
		ptr = ator->ator.allocator.realloc_fn(ator->ator.allocator.user, NULL, 0, total);
	} else {
		ptr = ufbx_malloc(total);
	}

	if (!ptr) {
		ufbxi_report_err_msg(ator->error, "ptr", "Out of memory");
		ufbxi_fmt_err_info(ator->error, "%s", ator->name);
		return NULL;
	}
	ufbx_assert(((uintptr_t)ptr & ufbxi_size_align_mask(total)) == 0);

	ator->current_size += total;

	return ptr;
}

static void ufbxi_free_size(ufbxi_allocator *ator, size_t size, void *ptr, size_t n);
static ufbxi_noinline void *ufbxi_realloc_size(ufbxi_allocator *ator, size_t size, void *old_ptr, size_t old_n, size_t n)
{
	ufbx_assert(size > 0);
	// realloc() with zero old/new size is equivalent to alloc()/free()
	if (old_n == 0) return ufbxi_alloc_size(ator, size, n);
	if (n == 0) { ufbxi_free_size(ator, size, old_ptr, old_n); return NULL; }

	size_t old_total = size * old_n;
	size_t total = size * n;

	// The old values have been checked by a previous allocate call
	ufbx_assert(!ufbxi_does_overflow(old_total, size, old_n));
	ufbx_assert(old_total <= ator->current_size);

	ufbxi_check_return_err(ator->error, !ufbxi_does_overflow(total, size, n), NULL);
	ufbxi_check_return_err(ator->error, total <= SIZE_MAX / 2, NULL); // Make sure it's always safe to double allocations
	ufbxi_check_return_err_msg(ator->error, total <= ator->max_size - ator->current_size, NULL, "Memory limit exceeded");
	ufbxi_check_return_err_msg(ator->error, ator->num_allocs < ator->max_allocs, NULL, "Allocation limit exceeded");
	ator->num_allocs++;

	void *ptr;
	if (ator->ator.allocator.realloc_fn) {
		ptr = ator->ator.allocator.realloc_fn(ator->ator.allocator.user, old_ptr, old_total, total);
	} else if (ator->ator.allocator.alloc_fn) {
		// Use user-provided alloc_fn() and free_fn()
		ptr = ator->ator.allocator.alloc_fn(ator->ator.allocator.user, total);
		if (ptr) memcpy(ptr, old_ptr, old_total);
		if (ator->ator.allocator.free_fn) {
			ator->ator.allocator.free_fn(ator->ator.allocator.user, old_ptr, old_total);
		}
	} else {
		ptr = ufbx_realloc(old_ptr, old_total, total);
	}

	ufbxi_check_return_err_msg(ator->error, ptr, NULL, "Out of memory");
	ufbx_assert(((uintptr_t)ptr & ufbxi_size_align_mask(total)) == 0);

	ator->current_size += total;
	ator->current_size -= old_total;

	return ptr;
}

static ufbxi_noinline void ufbxi_free_size(ufbxi_allocator *ator, size_t size, void *ptr, size_t n)
{
	ufbx_assert(size > 0);
	if (n == 0) return;
	ufbx_assert(ptr);

	size_t total = size * n;

	// The old values have been checked by a previous allocate call
	ufbx_assert(!ufbxi_does_overflow(total, size, n));
	ufbx_assert(total <= ator->current_size);

	ator->current_size -= total;

	if (ator->ator.allocator.alloc_fn || ator->ator.allocator.realloc_fn) {
		// Don't call default free() if there is an user-provided `alloc_fn()`
		if (ator->ator.allocator.free_fn) {
			ator->ator.allocator.free_fn(ator->ator.allocator.user, ptr, total);
		} else if (ator->ator.allocator.realloc_fn) {
			ator->ator.allocator.realloc_fn(ator->ator.allocator.user, ptr, total, 0);
		}
	} else {
		ufbx_free(ptr, total);
	}
}

ufbxi_noinline ufbxi_nodiscard static bool ufbxi_grow_array_size(ufbxi_allocator *ator, size_t size, void *p_ptr, size_t *p_cap, size_t n)
{
	#if defined(UFBX_REGRESSION)
	{
		ufbxi_check_return_err_msg(ator->error, ator->num_allocs < ator->max_allocs, false, "Allocation limit exceeded");
		ator->num_allocs++;
	}
	#endif

	if (n <= *p_cap) return true;
	void *ptr = *(void**)p_ptr;
	size_t old_n = *p_cap;
	if (old_n >= n) return true;
	size_t new_n = ufbxi_max_sz(old_n * 2, n);
	void *new_ptr = ufbxi_realloc_size(ator, size, ptr, old_n, new_n);
	if (!new_ptr) return false;
	*(void**)p_ptr = new_ptr;
	*p_cap = new_n;
	return true;
}

static ufbxi_noinline void ufbxi_free_ator(ufbxi_allocator *ator)
{
	ufbx_assert(ator->current_size == 0);

	ufbx_free_allocator_fn *free_fn = ator->ator.allocator.free_allocator_fn;
	if (free_fn) {
		void *user = ator->ator.allocator.user;
		free_fn(user);
	}
}

#define ufbxi_alloc(ator, type, n) ufbxi_maybe_null((type*)ufbxi_alloc_size((ator), sizeof(type), (n)))
#define ufbxi_alloc_zero(ator, type, n) ufbxi_maybe_null((type*)ufbxi_alloc_zero_size((ator), sizeof(type), (n)))
#define ufbxi_realloc(ator, type, old_ptr, old_n, n) ufbxi_maybe_null((type*)ufbxi_realloc_size((ator), sizeof(type), (old_ptr), (old_n), (n)))
#define ufbxi_realloc_zero(ator, type, old_ptr, old_n, n) ufbxi_maybe_null((type*)ufbxi_realloc_zero_size((ator), sizeof(type), (old_ptr), (old_n), (n)))
#define ufbxi_free(ator, type, ptr, n) ufbxi_free_size((ator), sizeof(type), (ptr), (n))

#define ufbxi_grow_array(ator, p_ptr, p_cap, n) ufbxi_grow_array_size((ator), sizeof(**(p_ptr)), (p_ptr), (p_cap), (n))

#define UFBXI_SCENE_IMP_MAGIC 0x58424655
#define UFBXI_MESH_IMP_MAGIC 0x48534d55
#define UFBXI_LINE_CURVE_IMP_MAGIC 0x55434c55
#define UFBXI_CACHE_IMP_MAGIC 0x48434355
#define UFBXI_ANIM_IMP_MAGIC 0x494e4155
#define UFBXI_BAKED_ANIM_IMP_MAGIC 0x4b414255
#define UFBXI_REFCOUNT_IMP_MAGIC 0x46455255
#define UFBXI_BUF_CHUNK_IMP_MAGIC 0x46554255

// -- Memory buffer
//
// General purpose memory buffer that can be used either as a chunked linear memory
// allocator or a non-contiguous stack. You can convert the contents of `ufbxi_buf`
// to a contiguous range of memory by calling `ufbxi_make_array[_all]()`

typedef struct ufbxi_buf_padding ufbxi_buf_padding;
typedef struct ufbxi_buf_chunk ufbxi_buf_chunk;

struct ufbxi_buf_padding {
	size_t original_pos; // < Original position before aligning
	size_t prev_padding; // < Starting offset of the previous `ufbxi_buf_padding`
};

struct ufbxi_buf_chunk {

	// Linked list of nodes
	ufbxi_buf_chunk *root;
	ufbxi_buf_chunk *prev;
	ufbxi_buf_chunk *next;

	union {
		size_t magic;  // < Magic for debugging
		void *align_0; // < Align to 4x pointer size (16/32 bytes)
	};

	size_t size;         // < Size of the chunk `data`, excluding this header
	size_t pushed_pos;   // < Size of valid data when pushed to the list
	size_t next_size;    // < Next geometrically growing chunk size to allocate
	size_t padding_pos;  // < One past the offset of the most recent `ufbxi_buf_padding`

	char data[]; // < Must be aligned to 8 bytes
};

ufbx_static_assert(buf_chunk_align, offsetof(ufbxi_buf_chunk, data) % 8 == 0);

typedef struct {
	ufbxi_allocator *ator;

	// Current chunks for normal and huge allocations.
	// Ordered buffers (`!ufbxi_buf.unordered`) never use `chunks[1]`
	ufbxi_buf_chunk *chunks[2];

	// Inline state for non-huge chunks
	size_t pos;  // < Next offset to allocate from
	size_t size; // < Size of the current chunk ie. `chunks[0]->size` (or 0 if `chunks[0] == NULL`)

	size_t num_items; // < Number of individual items pushed to the buffer

	size_t pushed_size; // < Cumulative size of pushed chunks, not tracked across pops

	bool unordered;  // < Does not support popping from the buffer
	bool clearable;  // < Supports clearing the whole buffer even if `unordered`
} ufbxi_buf;

typedef struct {
	ufbxi_buf_chunk *chunk;
	size_t pos;
	size_t num_items;
} ufbxi_buf_state;

static ufbxi_noinline void *ufbxi_push_size_new_block(ufbxi_buf *b, size_t size)
{
	bool huge = size >= b->ator->huge_size;

	// Use the second chunk "list" for huge unordered chunks.
	// The state of these chunks is not tracked by `ufbxi_buf.pos/size`.
	uint32_t list_ix = ((uint32_t)b->unordered & (uint32_t)huge);

	ufbxi_buf_chunk *chunk = b->chunks[list_ix];
	if (chunk) {
		if (list_ix == 0) {
			// Store the final position for the retired chunk and scan free
			// chunks in case we find one the allocation fits in.
			b->pushed_size += b->pos;
			chunk->pushed_pos = b->pos;
			ufbxi_buf_chunk *next = chunk->next;
			while (next != NULL) {
				ufbx_assert(next->magic == UFBXI_BUF_CHUNK_IMP_MAGIC);
				chunk = next;
				ufbx_assert(b->unordered || chunk->pushed_pos == 0);
				chunk->pushed_pos = 0;
				if (size <= chunk->size) {
					b->chunks[0] = chunk;
					b->pos = (uint32_t)size;
					b->size = chunk->size;
					return chunk->data;
				}
				next = chunk->next;
			}
		} else if (b->clearable) {
			// Keep track of the `UFBXI_HUGE_MAX_SCAN` largest chunks and
			// retain them. Overflowing chunks are freed in `ufbxi_buf_clear()`
			size_t align_mask = ufbxi_size_align_mask(size);
			ufbxi_buf_chunk *next = chunk;

			ufbxi_buf_chunk *best_chunk = NULL;
			size_t best_space = SIZE_MAX;

			// Clearable huge chunks are sorted by descending size. Check the first N
			// chunks for reuse and find the place a new block should be inserted if
			// no suitable space is found. Chunk ordering in the tail doesn't matter
			// as those chunks are never reused.
			// Unreachable chunks in the tail are freed in `ufbxi_buf_clear()`.
			for (size_t i = 0; next && i < UFBXI_HUGE_MAX_SCAN; i++) {
				ufbx_assert(next->magic == UFBXI_BUF_CHUNK_IMP_MAGIC);
				if (next->size < size) break;
				chunk = next;

				// Try to reuse chunks using a best-fit strategy.
				size_t pos = ufbxi_align_to_mask(chunk->pushed_pos, align_mask);
				size_t space = chunk->size - pos;
				if (size <= space) {
					if (space < best_space) {
						best_chunk = chunk;
						best_space = space;
					}
				}

				next = chunk->next;
			}

			// Early return if we found a slot.
			if (best_chunk) {
				size_t pos = ufbxi_align_to_mask(best_chunk->pushed_pos, align_mask);
				best_chunk->pushed_pos = pos + size;
				b->pushed_size += size;
				return best_chunk->data + pos;
			}
		}
	}

	// Allocate a new chunk, grow `next_size` geometrically but don't double
	// the current or previous user sizes if they are larger.
	size_t chunk_size, next_size;

	// If `size` is larger than `huge_size` don't grow `next_size` geometrically,
	// but use a dedicated allocation.
	if (huge) {
		next_size = chunk ? chunk->next_size : 4096;
		if (next_size > b->ator->chunk_max) next_size = b->ator->chunk_max;
		chunk_size = size;
	} else {
		next_size = chunk ? chunk->next_size * 2 : 4096;
		if (next_size > b->ator->chunk_max) next_size = b->ator->chunk_max;
		chunk_size = next_size - sizeof(ufbxi_buf_chunk);
		if (chunk_size < size) chunk_size = size;
	}

	// Align chunk sizes to 16 bytes
	chunk_size = ufbxi_align_to_mask(chunk_size, 0xf);

	ufbxi_buf_chunk *new_chunk = (ufbxi_buf_chunk*)ufbxi_alloc_size(b->ator, 1, sizeof(ufbxi_buf_chunk) + chunk_size);
	if (!new_chunk) return NULL;

	new_chunk->prev = chunk;
	new_chunk->size = chunk_size;
	new_chunk->next_size = next_size;
	new_chunk->magic = UFBXI_BUF_CHUNK_IMP_MAGIC;
	new_chunk->padding_pos = 0;
	new_chunk->pushed_pos = 0;

	// Link the chunk to the list and set it as the active one
	if (chunk) {
		ufbxi_buf_chunk *next = chunk->next;
		if (next) next->prev = new_chunk;
		new_chunk->next = next;
		chunk->next = new_chunk;
		new_chunk->root = chunk->root;
	} else {
		new_chunk->next = NULL;
		new_chunk->root = new_chunk;
	}

	if (list_ix == 0) {
		b->chunks[0] = new_chunk;
		b->pos = size;
		b->size = chunk_size;
	} else {
		ufbxi_buf_chunk *root = b->chunks[1];
		b->pushed_size += size;
		if (!root) {
			b->chunks[1] = new_chunk;
		} else if (root->size < chunk_size) {
			// Swap root and self if necessary, we should have bailed out
			// in the search loop in the first iteration so `new_chunk` should
			// directly follow `root`.
			// HACK: This ends up with `chunks[1]` entries having inconsistent
			// `ufbxi_buf_chunk.root` pointers but other code only reads `chunks[1].root`
			// TODO: Move roots out of the chunks?
			ufbx_assert(root->next == new_chunk);
			ufbx_assert(new_chunk->prev == root);
			if (new_chunk->next) new_chunk->next->prev = root;
			root->next = new_chunk->next;
			new_chunk->next = root;
			new_chunk->prev = NULL;
			new_chunk->root = new_chunk;
			b->chunks[1] = new_chunk;
		}
		new_chunk->pushed_pos = size;
	}

	return new_chunk->data;
}

static ufbxi_noinline void *ufbxi_push_size(ufbxi_buf *b, size_t size, size_t n)
{
	// Always succeed with an empty non-NULL buffer for empty allocations
	ufbx_assert(size > 0);
	if (n == 0) return (void*)ufbxi_zero_size_buffer;

	size_t total = size * n;
	if (ufbxi_does_overflow(total, size, n)) return NULL;

	#if defined(UFBX_REGRESSION)
	{
		ufbxi_allocator *ator = b->ator;
		ufbxi_check_return_err_msg(ator->error, ator->num_allocs < ator->max_allocs, NULL, "Allocation limit exceeded");
		ator->num_allocs++;
	}
	#endif

	b->num_items += n;

	// Align to the natural alignment based on the size
	size_t align_mask = ufbxi_size_align_mask(size);
	size_t pos = ufbxi_align_to_mask(b->pos, align_mask);

	if (!b->unordered && pos != b->pos) {
		// Alignment mismatch in an unordered block. Align to 16 bytes to guarantee
		// sufficient alignment for anything afterwards and mark the padding.
		// If we overflow the current block we don't need to care as the block
		// boundaries are not contiguous.
		pos = ufbxi_align_to_mask(b->pos, 0xf);
		if (total < SIZE_MAX - 16 && total + 16 <= b->size - pos) {
			ufbxi_buf_chunk *chunk = b->chunks[0];
			ufbxi_buf_padding *padding = (ufbxi_buf_padding*)(chunk->data + pos);
			padding->original_pos = b->pos;
			padding->prev_padding = chunk->padding_pos;
			chunk->padding_pos = pos + 16 + 1;
			b->pos = pos + 16 + total;
			return (char*)padding + 16;
		} else {
			return ufbxi_push_size_new_block(b, total);
		}
	} else {
		// Try to push to the current block. Allocate a new block
		// if the aligned size doesn't fit.
		if (total <= b->size - pos) {
			b->pos = pos + total;
			return b->chunks[0]->data + pos;
		} else {
			return ufbxi_push_size_new_block(b, total);
		}
	}
}

static ufbxi_forceinline void *ufbxi_push_size_fast(ufbxi_buf *b, size_t size, size_t n)
{
	// Always succeed with an empty non-NULL buffer for empty allocations
	ufbxi_regression_assert(size > 0);
	ufbxi_regression_assert(n > 0);

	size_t total = size * n;
	ufbxi_regression_assert(!ufbxi_does_overflow(total, size, n));

	#if defined(UFBX_REGRESSION)
	{
		ufbxi_allocator *ator = b->ator;
		ufbxi_check_return_err_msg(ator->error, ator->num_allocs < ator->max_allocs, NULL, "Allocation limit exceeded");
		ator->num_allocs++;
	}
	#endif

	b->num_items += n;

	// Homogeneous arrays should always be aligned
	size_t pos = b->pos;
	ufbxi_regression_assert((pos & ufbxi_size_align_mask(size)) == 0);

	// Try to push to the current block. Allocate a new block
	// if the aligned size doesn't fit.
	if (total <= b->size - pos) {
		b->pos = pos + total;
		return b->chunks[0]->data + pos;
	} else {
		return ufbxi_push_size_new_block(b, total);
	}
}

static ufbxi_noinline void *ufbxi_push_size_zero(ufbxi_buf *b, size_t size, size_t n)
{
	void *ptr = ufbxi_push_size(b, size, n);
	if (ptr) memset(ptr, 0, size * n);
	return ptr;
}

ufbxi_nodiscard static ufbxi_noinline void *ufbxi_push_size_copy(ufbxi_buf *b, size_t size, size_t n, const void *data)
{
	// Always succeed with an empty non-NULL buffer for empty allocations, even if `data == NULL`
	ufbx_assert(size > 0);
	if (n == 0) return (void*)ufbxi_zero_size_buffer;

	ufbx_assert(data);
	void *ptr = ufbxi_push_size(b, size, n);
	if (ptr) memcpy(ptr, data, size * n);
	return ptr;
}

ufbxi_nodiscard static ufbxi_forceinline void *ufbxi_push_size_copy_fast(ufbxi_buf *b, size_t size, size_t n, const void *data)
{
	// Always succeed with an empty non-NULL buffer for empty allocations, even if `data == NULL`
	ufbx_assert(size > 0);
	if (n == 0) return (void*)ufbxi_zero_size_buffer;

	ufbx_assert(data);
	void *ptr = ufbxi_push_size_fast(b, size, n);
	if (ptr) memcpy(ptr, data, size * n);
	return ptr;
}

static ufbxi_noinline void ufbxi_buf_free_unused(ufbxi_buf *b)
{
	ufbx_assert(!b->unordered);

	ufbxi_buf_chunk *chunk = b->chunks[0];
	if (!chunk) return;

	ufbxi_buf_chunk *next = chunk->next;
	while (next) {
		ufbxi_buf_chunk *to_free = next;
		next = next->next;
		ufbx_assert(to_free->magic == UFBXI_BUF_CHUNK_IMP_MAGIC);
		to_free->magic = 0;
		ufbxi_free_size(b->ator, 1, to_free, sizeof(ufbxi_buf_chunk) + to_free->size);
	}
	chunk->next = NULL;

	while (b->pos == 0 && chunk) {
		ufbxi_buf_chunk *prev = chunk->prev;
		ufbx_assert(chunk->magic == UFBXI_BUF_CHUNK_IMP_MAGIC);
		chunk->magic = 0;
		ufbxi_free_size(b->ator, 1, chunk, sizeof(ufbxi_buf_chunk) + chunk->size);
		chunk = prev;
		b->chunks[0] = prev;
		if (prev) {
			prev->next = NULL;
			b->pos = prev->pushed_pos;
			b->size = prev->size;
		} else {
			b->pos = 0;
			b->size = 0;
		}
	}
}

static ufbxi_noinline void ufbxi_pop_size(ufbxi_buf *b, size_t size, size_t n, void *dst, bool peek)
{
	ufbx_assert(!b->unordered);
	ufbx_assert(size > 0);
	ufbx_assert(b->num_items >= n);
	if (!peek) b->num_items -= n;

	char *ptr = (char*)dst;
	size_t bytes_left = size * n;

	// We've already pushed this, it better not overflow
	ufbx_assert(!ufbxi_does_overflow(bytes_left, size, n));

	if (ptr) {
		ptr += bytes_left;
		size_t pos = b->pos;
		ufbxi_buf_chunk *chunk = b->chunks[0];
		for (;;) {
			if (bytes_left <= pos) {
				// Rest of the data is in this single chunk
				pos -= bytes_left;
				if (!peek) b->pos = pos;
				ptr -= bytes_left;
				if (bytes_left > 0) {
					memcpy(ptr, chunk->data + pos, bytes_left);
				}
				break;
			} else {
				// Pop the whole chunk
				ptr -= pos;
				bytes_left -= pos;
				memcpy(ptr, chunk->data, pos);
				if (!peek) {
					chunk->pushed_pos = 0;
					chunk = chunk->prev;
					b->chunks[0] = chunk;
					b->size = chunk->size;
				} else {
					chunk = chunk->prev;
				}
				pos = chunk->pushed_pos;
			}
		}
	} else {
		size_t pos = b->pos;
		ufbxi_buf_chunk *chunk = b->chunks[0];
		for (;;) {
			if (bytes_left <= pos) {
				// Rest of the data is in this single chunk
				pos -= bytes_left;
				if (!peek) b->pos = pos;
				break;
			} else {
				// Pop the whole chunk
				bytes_left -= pos;
				if (!peek) {
					chunk->pushed_pos = 0;
					chunk = chunk->prev;
					b->chunks[0] = chunk;
					b->size = chunk->size;
				} else {
					chunk = chunk->prev;
				}
				pos = chunk->pushed_pos;
			}
		}
	}

	if (!peek) {
		// Check if we need to rewind past some alignment padding
		ufbxi_buf_chunk *chunk = b->chunks[0];
		if (chunk) {
			size_t pos = b->pos, padding_pos = chunk->padding_pos;
			if (pos < padding_pos) {
				ufbx_assert(pos + 1 == padding_pos);
				ufbxi_buf_padding *padding = (ufbxi_buf_padding*)(chunk->data + padding_pos - 1 - 16);
				b->pos = padding->original_pos;
				chunk->padding_pos = padding->prev_padding;
			}
		}

		// Immediately free popped items if all the allocations are huge
		// as it means we want to have dedicated allocations for each push.
		if (b->ator->huge_size <= 1) {
			ufbxi_buf_free_unused(b);
		}
	}
}

static ufbxi_noinline void *ufbxi_push_pop_size(ufbxi_buf *dst, ufbxi_buf *src, size_t size, size_t n)
{
	void *data = ufbxi_push_size(dst, size, n);
	if (!data) return NULL;
	ufbxi_pop_size(src, size, n, data, false);
	return data;
}

static ufbxi_noinline void *ufbxi_push_peek_size(ufbxi_buf *dst, ufbxi_buf *src, size_t size, size_t n)
{
	void *data = ufbxi_push_size(dst, size, n);
	if (!data) return NULL;
	ufbxi_pop_size(src, size, n, data, true);
	return data;
}

static ufbxi_noinline void ufbxi_buf_free(ufbxi_buf *buf)
{
	ufbxi_nounroll for (size_t i = 0; i < 2; i++) {
		ufbxi_buf_chunk *chunk = buf->chunks[i];
		if (chunk) {
			chunk = chunk->root;
			while (chunk) {
				ufbxi_buf_chunk *next = chunk->next;
				ufbx_assert(chunk->magic == UFBXI_BUF_CHUNK_IMP_MAGIC);
				chunk->magic = 0;
				ufbxi_free_size(buf->ator, 1, chunk, sizeof(ufbxi_buf_chunk) + chunk->size);
				chunk = next;
			}
		}
		buf->chunks[i] = NULL;
	}
	buf->pos = 0;
	buf->size = 0;
	buf->num_items = 0;
}

static ufbxi_noinline void ufbxi_buf_clear(ufbxi_buf *buf)
{
	// Only unordered or clearable buffers can be cleared
	ufbx_assert(!buf->unordered || buf->clearable);

	// Free the memory if using ASAN
	if (buf->ator->huge_size <= 1) {
		ufbxi_buf_free(buf);
		return;
	}

	// Reset the non-huge chunks as `chunk->next` is always free.
	ufbxi_buf_chunk *chunk = buf->chunks[0];
	if (chunk) {
		ufbxi_buf_chunk *root = chunk->root;
		buf->chunks[0] = root;
		buf->pos = 0;
		buf->size = root->size;
	}
	buf->num_items = 0;
	buf->pushed_size = 0;

	// Huge chunks are always sorted by descending size and
	// `chunks[1]` points to the largest one.
	ufbxi_buf_chunk *huge = buf->chunks[1];
	if (huge) {
		// Reset the first N ones that are tracked.
		for (size_t i = 0; huge && i < UFBXI_HUGE_MAX_SCAN; i++) {
			huge->pushed_pos = 0;
			huge = huge->next;
		}

		// Got unreachable tail that should be freed: Unlink from the last
		// tracked chunk and free the rest.
		if (huge) {
			huge->prev->next = NULL;
			while (huge) {
				ufbxi_buf_chunk *next = huge->next;
				ufbx_assert(huge->magic == UFBXI_BUF_CHUNK_IMP_MAGIC);
				huge->magic = 0;
				ufbxi_free_size(buf->ator, 1, huge, sizeof(ufbxi_buf_chunk) + huge->size);
				huge = next;
			}
		}
	}
}

#define ufbxi_push(b, type, n) ufbxi_maybe_null((type*)ufbxi_push_size((b), sizeof(type), (n)))
#define ufbxi_push_zero(b, type, n) ufbxi_maybe_null((type*)ufbxi_push_size_zero((b), sizeof(type), (n)))
#define ufbxi_push_copy(b, type, n, data) ufbxi_maybe_null((type*)ufbxi_push_size_copy((b), sizeof(type), (n), (data)))
#define ufbxi_push_copy_fast(b, type, n, data) ufbxi_maybe_null((type*)ufbxi_push_size_copy_fast((b), sizeof(type), (n), (data)))
#define ufbxi_push_fast(b, type, n) ufbxi_maybe_null((type*)ufbxi_push_size_fast((b), sizeof(type), (n)))
#define ufbxi_pop(b, type, n, dst) ufbxi_pop_size((b), sizeof(type), (n), (dst), false)
#define ufbxi_peek(b, type, n, dst) ufbxi_pop_size((b), sizeof(type), (n), (dst), true)
#define ufbxi_push_pop(dst, src, type, n) ufbxi_maybe_null((type*)ufbxi_push_pop_size((dst), (src), sizeof(type), (n)))
#define ufbxi_push_peek(dst, src, type, n) ufbxi_maybe_null((type*)ufbxi_push_peek_size((dst), (src), sizeof(type), (n)))

// -- Hash map
//
// The actual element comparison is left to the user of `ufbxi_map`, see usage below.
//
// NOTES:
//   ufbxi_map_insert() does not support duplicate values, use find first if duplicates are possible!
//   Inserting duplicate elements fails with an assertion if `UFBX_REGRESSION` is enabled.

typedef struct ufbxi_aa_node ufbxi_aa_node;

typedef int ufbxi_cmp_fn(void *user, const void *a, const void *b);

struct ufbxi_aa_node {
	ufbxi_aa_node *left, *right;
	uint32_t level;
	uint32_t index;
};

typedef struct {
	ufbxi_allocator *ator;
	size_t data_size;

	void *items;
	uint64_t *entries;
	uint32_t mask;

	uint32_t capacity;
	uint32_t size;

	ufbxi_cmp_fn *cmp_fn;
	void *cmp_user;

	ufbxi_buf aa_buf;
	ufbxi_aa_node *aa_root;

} ufbxi_map;

static ufbxi_noinline void ufbxi_map_init(ufbxi_map *map, ufbxi_allocator *ator, ufbxi_cmp_fn *cmp_fn, void *cmp_user)
{
	map->ator = ator;
#if defined(UFBX_REGRESSION)
	// HACK: Maps contain pointers that are not stable between runs, in regression
	// mode this causes instability in allocation patterns due to different AA trees
	// being built, which is a problem in fuzz checks that need to have deterministic
	// allocation counts. We can work around this using a local allocator that doesn't
	// count the allocations.
	{
		ufbxi_allocator *regression_ator = (ufbxi_allocator*)ufbx_malloc(sizeof(ufbxi_allocator));
		ufbx_assert(regression_ator);
		memset(regression_ator, 0, sizeof(ufbxi_allocator));
		regression_ator->name = "regression";
		regression_ator->error = ator->error;
		regression_ator->huge_size = ator->huge_size;
		regression_ator->max_size = SIZE_MAX;
		regression_ator->max_allocs = SIZE_MAX;
		regression_ator->chunk_max = 0x1000000;
		map->aa_buf.ator = regression_ator;
	}
#else
	map->aa_buf.ator = ator;
#endif
	map->cmp_fn = cmp_fn;
	map->cmp_user = cmp_user;
}

static ufbxi_noinline void ufbxi_map_free(ufbxi_map *map)
{
#if defined(UFBX_REGRESSION)
	ufbxi_allocator *regression_ator = map->aa_buf.ator;
#endif

	ufbxi_buf_free(&map->aa_buf);
	ufbxi_free(map->ator, char, map->entries, map->data_size);
	map->entries = NULL;
	map->items = NULL;
	map->aa_root = NULL;
	map->mask = map->capacity = map->size = 0;

#if defined(UFBX_REGRESSION)
	if (regression_ator) {
		ufbxi_free_ator(regression_ator);
		ufbx_free(regression_ator, sizeof(ufbxi_allocator));
	}
#endif
}

// Recursion limit: log2(2^64 / sizeof(ufbxi_aa_node))
static ufbxi_noinline ufbxi_aa_node *ufbxi_aa_tree_insert(ufbxi_map *map, ufbxi_aa_node *node, const void *value, uint32_t index, size_t item_size)
	ufbxi_recursive_function(ufbxi_aa_node *, ufbxi_aa_tree_insert, (map, node, value, index, item_size), 59,
		(ufbxi_map *map, ufbxi_aa_node *node, const void *value, uint32_t index, size_t item_size))
{
	if (!node) {
		ufbxi_aa_node *new_node = ufbxi_push(&map->aa_buf, ufbxi_aa_node, 1);
		if (!new_node) return NULL;
		new_node->left = NULL;
		new_node->right = NULL;
		new_node->level = 1;
		new_node->index = index;
		return new_node;
	}

	void *entry = (char*)map->items + node->index * item_size;
	int cmp = map->cmp_fn(map->cmp_user, value, entry);
	if (cmp < 0) {
		node->left = ufbxi_aa_tree_insert(map, node->left, value, index, item_size);
	} else if (cmp >= 0) {
		node->right = ufbxi_aa_tree_insert(map, node->right, value, index, item_size);
	}

	if (node->left && node->left->level == node->level) {
		ufbxi_aa_node *left = node->left;
		node->left = left->right;
		left->right = node;
		node = left;
	}

	if (node->right && node->right->right && node->right->right->level == node->level) {
		ufbxi_aa_node *right = node->right;
		node->right = right->left;
		right->left = node;
		right->level += 1;
		node = right;
	}

	return node;
}

static ufbxi_noinline void *ufbxi_aa_tree_find(ufbxi_map *map, const void *value, size_t item_size)
{
	ufbxi_aa_node *node = map->aa_root;
	while (node) {
		void *entry = (char*)map->items + node->index * item_size;
		int cmp = map->cmp_fn(map->cmp_user, value, entry);
		if (cmp < 0) {
			node = node->left;
		} else if (cmp > 0) {
			node = node->right;
		} else {
			return entry;
		}
	}
	return NULL;
}

static ufbxi_noinline bool ufbxi_map_grow_size_imp(ufbxi_map *map, size_t item_size, size_t min_size)
{
	ufbx_assert(min_size > 0);
	const double load_factor = 0.7;

	// Find the lowest power of two size that fits `min_size` within `load_factor`
	size_t num_entries = map->mask + 1;
	size_t new_size = (size_t)((double)num_entries * load_factor);
	if (min_size < map->capacity + 1) min_size = map->capacity + 1;
	while (new_size < min_size) {
		num_entries *= 2;
		new_size = (size_t)((double)num_entries * load_factor);
	}

	// Check for overflow
	ufbxi_check_return_err(map->ator->error, SIZE_MAX / num_entries > sizeof(uint64_t), false);
	size_t alloc_size = num_entries * sizeof(uint64_t);

	// Allocate a combined entry/item memory block
	ufbxi_check_return_err(map->ator->error, (SIZE_MAX - alloc_size) / new_size > item_size, false);
	size_t data_size = alloc_size + new_size * item_size;

	char *data = ufbxi_alloc(map->ator, char, data_size);
	ufbxi_check_return_err(map->ator->error, data, false);

	// Copy the previous user items over
	uint64_t *old_entries = map->entries;
	uint64_t *new_entries = (uint64_t*)data;
	void *new_items = data + alloc_size;
	if (map->size > 0) {
		memcpy(new_items, map->items, item_size * map->size);
	}

	// Re-hash the entries
	uint32_t old_mask = map->mask;
	uint32_t new_mask = (uint32_t)(num_entries) - 1;
	memset(new_entries, 0, sizeof(uint64_t) * num_entries);
	if (old_mask) {
		for (uint32_t i = 0; i <= old_mask; i++) {
			uint64_t entry, new_entry = old_entries[i];
			if (!new_entry) continue;

			// Reconstruct the hash of the old entry at `i`
			uint32_t old_scan = (uint32_t)(new_entry & old_mask) - 1;
			uint32_t hash = ((uint32_t)new_entry & ~old_mask) | ((i - old_scan) & old_mask);
			uint32_t slot = hash & new_mask;
			new_entry &= ~(uint64_t)new_mask;

			// Scan forward until we find an empty slot, potentially swapping
			// `new_element` if it has a shorter scan distance (Robin Hood).
			uint32_t scan = 1;
			while ((entry = new_entries[slot]) != 0) {
				uint32_t entry_scan = (uint32_t)(entry & new_mask);
				if (entry_scan < scan) {
					new_entries[slot] = new_entry + scan;
					new_entry = (entry & ~(uint64_t)new_mask);
					scan = entry_scan;
				}
				scan += 1;
				slot = (slot + 1) & new_mask;
			}
			new_entries[slot] = new_entry + scan;
		}
	}

	// And finally free the previous allocation
	ufbxi_free(map->ator, char, (char*)old_entries, map->data_size);
	map->items = new_items;
	map->data_size = data_size;
	map->entries = new_entries;
	map->mask = new_mask;
	map->capacity = (uint32_t)new_size;

	return true;
}

static ufbxi_forceinline bool ufbxi_map_grow_size(ufbxi_map *map, size_t size, size_t min_size)
{
	#if defined(UFBX_REGRESSION)
	{
		ufbxi_allocator *ator = map->ator;
		ufbxi_check_return_err_msg(ator->error, ator->num_allocs < ator->max_allocs, false, "Allocation limit exceeded");
		ator->num_allocs++;
	}
	#endif

	if (map->size < map->capacity && map->capacity >= min_size) return true;
	return ufbxi_map_grow_size_imp(map, size, min_size);
}

static ufbxi_noinline void *ufbxi_map_find_size(ufbxi_map *map, size_t size, uint32_t hash, const void *value)
{
	uint64_t *entries = map->entries;
	uint32_t mask = map->mask, scan = 0;

	uint32_t ref = hash & ~mask;
	if (!mask || scan == UINT32_MAX) return 0;

	// Scan entries until we find an exact match of the hash or until we hit
	// an element that has lower scan distance than our search (Robin Hood).
	// The encoding guarantees that zero slots also terminate with the same test.
	for (;;) {
		uint64_t entry = entries[(hash + scan) & mask];
		scan += 1;
		if ((uint32_t)entry == ref + scan) {
			uint32_t index = (uint32_t)(entry >> 32u);
			void *data = (char*)map->items + size * index;
			int cmp = map->cmp_fn(map->cmp_user, value, data);
			if (cmp == 0) return data;
		} else if ((entry & mask) < scan) {
			if (map->aa_root) {
				return ufbxi_aa_tree_find(map, value, size);
			} else {
				return NULL;
			}
		}
	}
}

static ufbxi_noinline void *ufbxi_map_insert_size(ufbxi_map *map, size_t size, uint32_t hash, const void *value)
{
	if (!ufbxi_map_grow_size(map, size, 64)) return NULL;

	ufbxi_regression_assert(ufbxi_map_find_size(map, size, hash, value) == NULL);

	uint32_t index = map->size++;

	uint64_t *entries = map->entries;
	uint32_t mask = map->mask;

	// Scan forward until we find an empty slot, potentially swapping
	// `new_element` if it has a shorter scan distance (Robin Hood).
	uint32_t slot = hash & mask;
	uint64_t entry, new_entry = (uint64_t)index << 32u | (hash & ~mask);
	uint32_t scan = 1;
	while ((entry = entries[slot]) != 0) {
		uint32_t entry_scan = (uint32_t)(entry & mask);
		if (entry_scan < scan) {
			entries[slot] = new_entry + scan;
			new_entry = (entry & ~(uint64_t)mask);
			scan = entry_scan;
		}
		scan += 1;
		slot = (slot + 1) & mask;

		if (scan > UFBXI_MAP_MAX_SCAN) {
			uint32_t new_index = (uint32_t)(new_entry >> 32u);
			const void *new_value = new_index == index ? value : (const void*)((char*)map->items + size * new_index);
			map->aa_root = ufbxi_aa_tree_insert(map, map->aa_root, new_value, new_index, size);
			return (char*)map->items + size * index;
		}
	}
	entries[slot] = new_entry + scan;

	return (char*)map->items + size * index;
}

#define ufbxi_map_grow(map, type, min_size) ufbxi_map_grow_size((map), sizeof(type), (min_size))
#define ufbxi_map_find(map, type, hash, value) ufbxi_maybe_null((type*)ufbxi_map_find_size((map), sizeof(type), (hash), (value)))
#define ufbxi_map_insert(map, type, hash, value) ufbxi_maybe_null((type*)ufbxi_map_insert_size((map), sizeof(type), (hash), (value)))

static int ufbxi_map_cmp_uint64(void *user, const void *va, const void *vb)
{
	(void)user;
	uint64_t a = *(const uint64_t*)va, b = *(const uint64_t*)vb;
	if (a < b) return -1;
	if (a > b) return +1;
	return 0;
}

static int ufbxi_map_cmp_const_char_ptr(void *user, const void *va, const void *vb)
{
	(void)user;
	const char *a = *(const char **)va, *b = *(const char **)vb;
	if (a < b) return -1;
	if (a > b) return +1;
	return 0;
}

static int ufbxi_map_cmp_uintptr(void *user, const void *va, const void *vb)
{
	(void)user;
	uintptr_t a = *(const uintptr_t*)va, b = *(const uintptr_t*)vb;
	if (a < b) return -1;
	if (a > b) return +1;
	return 0;
}

// -- Hash functions

static ufbxi_noinline uint32_t ufbxi_hash_string(const char *str, size_t length)
{
	uint32_t hash = (uint32_t)length;
	uint32_t seed = UINT32_C(0x9e3779b9);
	if (length >= 4) {
		do {
			uint32_t word = ufbxi_read_u32(str);
			hash = ((hash << 5u | hash >> 27u) ^ word) * seed;
			str += 4;
			length -= 4;
		} while (length >= 4);

		uint32_t word = ufbxi_read_u32(str + length - 4);
		hash = ((hash << 5u | hash >> 27u) ^ word) * seed;
	} else {
		uint32_t word = 0;
		if (length >= 1) word |= (uint32_t)(uint8_t)str[0] << 0;
		if (length >= 2) word |= (uint32_t)(uint8_t)str[1] << 8;
		if (length >= 3) word |= (uint32_t)(uint8_t)str[2] << 16;
		hash = ((hash << 5u | hash >> 27u) ^ word) * seed;
	}
	hash ^= hash >> 16;
	hash *= UINT32_C(0x7feb352d);
	hash ^= hash >> 15;
	return hash;
}

// NOTE: _Must_ match `ufbxi_hash_string()`
static ufbxi_noinline uint32_t ufbxi_hash_string_check_ascii(const char *str, size_t length, bool *p_non_ascii)
{
	uint32_t ascii_mask = 0;
	uint32_t zero_mask = 0;

	ufbx_assert(length > 0);

	uint32_t hash = (uint32_t)length;
	uint32_t seed = UINT32_C(0x9e3779b9);
	if (length >= 4) {
		do {
			uint32_t word = ufbxi_read_u32(str);
			ascii_mask |= word;
			zero_mask |= UINT32_C(0x80808080) - word;

			hash = ((hash << 5u | hash >> 27u) ^ word) * seed;
			str += 4;
			length -= 4;
		} while (length >= 4);

		uint32_t word = ufbxi_read_u32(str + length - 4);
		ascii_mask |= word;
		zero_mask |= UINT32_C(0x80808080) - word;

		hash = ((hash << 5u | hash >> 27u) ^ word) * seed;
	} else {
		uint32_t word = 0;
		if (length >= 1) word |= (uint32_t)(uint8_t)str[0] << 0;
		if (length >= 2) word |= (uint32_t)(uint8_t)str[1] << 8;
		if (length >= 3) word |= (uint32_t)(uint8_t)str[2] << 16;

		ascii_mask |= word;
		zero_mask |= (UINT32_C(0x80808080) >> ((4u - length) * 8u)) - word;

		hash = ((hash << 5u | hash >> 27u) ^ word) * seed;
	}

	// If any character has high bit set or is zero we're not ASCII
	if (((ascii_mask | zero_mask) & 0x80808080u) != 0) {
		*p_non_ascii = true;
	}

	hash ^= hash >> 16;
	hash *= UINT32_C(0x7feb352d);
	hash ^= hash >> 15;

	return hash;
}

static ufbxi_forceinline uint32_t ufbxi_hash32(uint32_t x)
{
	x ^= x >> 16;
	x *= UINT32_C(0x7feb352d);
	x ^= x >> 15;
	x *= UINT32_C(0x846ca68b);
	x ^= x >> 16;
	return x;
}

static ufbxi_forceinline uint32_t ufbxi_hash64(uint64_t x)
{
	x ^= x >> 32;
	x *= UINT64_C(0xd6e8feb86659fd93);
	x ^= x >> 32;
	x *= UINT64_C(0xd6e8feb86659fd93);
	x ^= x >> 32;
	return (uint32_t)x;
}

static ufbxi_forceinline uint32_t ufbxi_hash_uptr(uintptr_t ptr)
{
	return sizeof(ptr) == 8 ? ufbxi_hash64((uint64_t)ptr) : ufbxi_hash32((uint32_t)ptr);
}

#define ufbxi_hash_ptr(ptr) ufbxi_hash_uptr((uintptr_t)(ptr))

// -- Warnings

typedef struct {
	ufbx_error *error;
	ufbxi_buf *result;
	ufbxi_buf tmp_stack;
	uint32_t deferred_element_id_plus_one;
	// Separate lists for specific and non-specific warnings
	ufbx_warning *prev_warnings[UFBX_WARNING_TYPE_COUNT][2];
} ufbxi_warnings;

ufbxi_nodiscard static ufbxi_noinline int ufbxi_vwarnf_imp(ufbxi_warnings *ws, ufbx_warning_type type, uint32_t element_id, const char *fmt, va_list args)
{
	if (!ws) return 1;

	// HACK(warning-element): Encode potential deferred element ID into `ufbx_warning.element_id`,
	// `ws->element_id_index_plus_one` contains index to `uc->tmp_element_id`.
	// Tag deferred indices with the high bit.
	if (element_id == ~0u && ws->deferred_element_id_plus_one > 0) {
		element_id = (ws->deferred_element_id_plus_one - 1) | 0x80000000u;
	}

	uint32_t has_element_id = element_id != ~0u;
	if (type >= UFBX_WARNING_TYPE_FIRST_DEDUPLICATED) {
		ufbx_warning *prev = ws->prev_warnings[type][has_element_id];
		if (prev && prev->element_id == element_id) {
			prev->count++;
			return 1;
		}
	}

	char desc[256];
	size_t desc_len = (size_t)ufbxi_vsnprintf(desc, sizeof(desc), fmt, args);

	ufbxi_clean_string_utf8(desc, desc_len);

	char *desc_copy = ufbxi_push_copy(ws->result, char, desc_len + 1, desc);
	ufbxi_check_err(ws->error, desc_copy);

	ufbx_warning *warning = ufbxi_push(&ws->tmp_stack, ufbx_warning, 1);
	ufbxi_check_err(ws->error, warning);

	warning->type = type;
	warning->description.data = desc_copy;
	warning->description.length = desc_len;
	warning->element_id = element_id;
	warning->count = 1;
	ws->prev_warnings[type][has_element_id] = warning;

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_warnf_imp(ufbxi_warnings *ws, ufbx_warning_type type, uint32_t element_id, const char *fmt, ...)
{
	// NOTE: `ws` may be `NULL` here, handled by `ufbxi_vwarnf()`
	va_list args; // ufbxi_uninit
	va_start(args, fmt);
	int ok = ufbxi_vwarnf_imp(ws, type, element_id, fmt, args);
	va_end(args);
	return ok;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_pop_warnings(ufbxi_warnings *ws, ufbx_warning_list *warnings, bool *p_has_warning)
{
	warnings->count = ws->tmp_stack.num_items;
	warnings->data = ufbxi_push_pop(ws->result, &ws->tmp_stack, ufbx_warning, warnings->count);
	ufbxi_check_err(ws->error, warnings->data);
	ufbxi_for_list(ufbx_warning, warning, *warnings) {
		p_has_warning[warning->type] = true;
	}
	return 1;
}

// -- String pool

// All strings found in FBX files are interned for deduplication and fast
// comparison. Our fixed internal strings (`ufbxi_String`) are considered the
// canonical pointers for said strings so we can compare them by address.

typedef struct {
	ufbx_error *error;
	ufbxi_buf buf; // < Buffer for the actual string data
	ufbxi_map map; // < Map of `ufbxi_string`
	size_t initial_size; // < Number of initial entries
	char  *temp_str; // < Temporary string buffer of `temp_cap`
	size_t temp_cap; // < Capacity of the temporary buffer
	ufbx_unicode_error_handling error_handling;
	ufbxi_warnings *warnings;
} ufbxi_string_pool;

typedef struct {
	const char *raw_data; // < UTF-8 data follows at `raw_length+1` if `utf8_length > 0`
	uint32_t raw_length;  // < Length of the non-sanitized original string
	uint32_t utf8_length; // < Length of sanitized UTF-8 string (or zero)
} ufbxi_sanitized_string;

static ufbxi_forceinline bool ufbxi_str_equal(ufbx_string a, ufbx_string b)
{
	return a.length == b.length && !memcmp(a.data, b.data, a.length);
}

static ufbxi_forceinline bool ufbxi_str_less(ufbx_string a, ufbx_string b)
{
	size_t len = ufbxi_min_sz(a.length, b.length);
	int cmp = memcmp(a.data, b.data, len);
	if (cmp != 0) return cmp < 0;
	return a.length < b.length;
}

static ufbxi_forceinline int ufbxi_str_cmp(ufbx_string a, ufbx_string b)
{
	size_t len = ufbxi_min_sz(a.length, b.length);
	int cmp = memcmp(a.data, b.data, len);
	if (cmp != 0) return cmp;
	if (a.length != b.length) return a.length < b.length ? -1 : 1;
	return 0;
}

static ufbxi_forceinline ufbx_string ufbxi_str_c(const char *str)
{
	ufbx_string s = { str, strlen(str) };
	return s;
}

static ufbxi_noinline uint32_t ufbxi_get_concat_key(const ufbx_string *parts, size_t num_parts)
{
	uint32_t key = 0, shift = 32;
	ufbxi_for(const ufbx_string, part, parts, num_parts) {
		size_t length = part->length != SIZE_MAX ? part->length : strlen(part->data);
		for (size_t i = 0; i < length; i++) {
			shift -= 8;
			key |= (uint32_t)(uint8_t)part->data[i] << shift;
			if (shift == 0) return key;
		}
	}
	return key;
}

static ufbxi_noinline int ufbxi_concat_str_cmp(const ufbx_string *ref, const ufbx_string *parts, size_t num_parts)
{
	const char *ptr = ref->data, *end = ptr + ref->length;
	ufbxi_for(const ufbx_string, part, parts, num_parts) {
		size_t length = part->length != SIZE_MAX ? part->length : strlen(part->data);
		size_t to_cmp = ufbxi_min_sz(ufbxi_to_size(end - ptr), length);
		int cmp = to_cmp > 0 ? memcmp(ptr, part->data, to_cmp) : 0;
		if (cmp != 0) return cmp;
		if (to_cmp != length) return -1;
		ptr += length;
	}
	return ptr == end ? 0 : +1;
}

static ufbxi_forceinline bool ufbxi_starts_with(ufbx_string str, ufbx_string prefix)
{
	return str.length >= prefix.length && !memcmp(str.data, prefix.data, prefix.length);
}

static ufbxi_forceinline bool ufbxi_ends_with(ufbx_string str, ufbx_string suffix)
{
	return str.length >= suffix.length && !memcmp(str.data + str.length - suffix.length, suffix.data, suffix.length);
}

static ufbxi_noinline bool ufbxi_remove_prefix_len(ufbx_string *str, const char *prefix, size_t prefix_len)
{
	ufbx_string prefix_str = { prefix, prefix_len };
	if (ufbxi_starts_with(*str, prefix_str)) {
		str->data += prefix_len;
		str->length -= prefix_len;
		return true;
	}
	return false;
}

static ufbxi_noinline bool ufbxi_remove_suffix_len(ufbx_string *str, const char *suffix, size_t suffix_len)
{
	ufbx_string suffix_str = { suffix, suffix_len };
	if (ufbxi_ends_with(*str, suffix_str)) {
		str->length -= suffix_len;
		return true;
	}
	return false;
}

static ufbxi_forceinline bool ufbxi_remove_prefix_str(ufbx_string *str, ufbx_string prefix)
{
	return ufbxi_remove_prefix_len(str, prefix.data, prefix.length);
}

static ufbxi_forceinline bool ufbxi_remove_suffix_c(ufbx_string *str, const char *suffix)
{
	return ufbxi_remove_suffix_len(str, suffix, strlen(suffix));
}

static int ufbxi_map_cmp_string(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_string *a = (const ufbx_string*)va, *b = (const ufbx_string*)vb;
	return ufbxi_str_cmp(*a, *b);
}

static ufbxi_forceinline ufbx_string ufbxi_safe_string(const char *data, size_t length)
{
	ufbx_string str = { length > 0 ? data : ufbxi_empty_char, length };
	return str;
}

static void ufbxi_string_pool_temp_free(ufbxi_string_pool *pool)
{
	ufbxi_free(pool->map.ator, char, pool->temp_str, pool->temp_cap);
	ufbxi_map_free(&pool->map);
}

ufbxi_nodiscard static size_t ufbxi_add_replacement_char(ufbxi_string_pool *pool, char *dst, char c)
{
	switch (pool->error_handling) {

	case UFBX_UNICODE_ERROR_HANDLING_REPLACEMENT_CHARACTER:
		dst[0] = (char)(uint8_t)0xefu;
		dst[1] = (char)(uint8_t)0xbfu;
		dst[2] = (char)(uint8_t)0xbdu;
		return 3;

	case UFBX_UNICODE_ERROR_HANDLING_UNDERSCORE:
		dst[0] = '_';
		return 1;

	case UFBX_UNICODE_ERROR_HANDLING_QUESTION_MARK:
		dst[0] = '?';
		return 1;

	case UFBX_UNICODE_ERROR_HANDLING_REMOVE:
		return 0;

	case UFBX_UNICODE_ERROR_HANDLING_UNSAFE_IGNORE:
		dst[0] = c;
		return 1;

	default:
		return 0;

	}
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_sanitize_string(ufbxi_string_pool *pool, ufbxi_sanitized_string *sanitized, const char *str, size_t length, size_t valid_length, bool push_both)
{
	// Handle only invalid cases here
	ufbx_assert(valid_length < length);
	ufbxi_check_err_msg(pool->error, pool->error_handling != UFBX_UNICODE_ERROR_HANDLING_ABORT_LOADING, "Invalid UTF-8");
	ufbxi_check_err(pool->error, ufbxi_warnf_imp(pool->warnings, UFBX_WARNING_BAD_UNICODE, ~0u, "Bad UTF-8 string"));

	size_t index = valid_length;
	size_t dst_len = index;
	if (push_both) {
		// Copy both the full raw string and the initial valid part
		ufbxi_check_err(pool->error, length <= SIZE_MAX / 2 - 64);
		ufbxi_check_err(pool->error, ufbxi_grow_array(pool->map.ator, &pool->temp_str, &pool->temp_cap, length * 2 + 64));
		memcpy(pool->temp_str, str, length);
		pool->temp_str[length] = '\0';
		memcpy(pool->temp_str + length + 1, str, index);
		dst_len += length + 1;
	} else {

		// Copy the initial valid part
		ufbxi_check_err(pool->error, length <= SIZE_MAX - 64);
		ufbxi_check_err(pool->error, ufbxi_grow_array(pool->map.ator, &pool->temp_str, &pool->temp_cap, length + 64));
		memcpy(pool->temp_str, str, index);
	}

	char *dst = pool->temp_str;
	while (index < length) {
		uint8_t c = (uint8_t)str[index];
		size_t left = length - index;

		// Not optimal but not the worst thing ever
		if (pool->temp_cap - dst_len < 16) {
			ufbxi_check_err(pool->error, ufbxi_grow_array(pool->map.ator, &pool->temp_str, &pool->temp_cap, dst_len + 16));
			dst = pool->temp_str;
		}

		if ((c & 0x80) == 0) {
			if (c != 0) {
				dst[dst_len] = (char)c;
				dst_len += 1;
				index += 1;
				continue;
			}
		} else if ((c & 0xe0) == 0xc0 && left >= 2) {
			uint8_t t0 = (uint8_t)str[index + 1];
			uint32_t code = (uint32_t)c << 8 | (uint32_t)t0 << 0;
			if ((code & 0xc0) == 0x80 && code >= 0xc280) {
				dst[dst_len + 0] = (char)c;
				dst[dst_len + 1] = (char)t0;
				dst_len += 2;
				index += 2;
				continue;
			}
		} else if ((c & 0xf0) == 0xe0 && left >= 3) {
			uint8_t t0 = (uint8_t)str[index + 1], t1 = (uint8_t)str[index + 2];
			uint32_t code = (uint32_t)c << 16 | (uint32_t)t0 << 8 | (uint32_t)t1;
			if ((code & 0xc0c0) == 0x8080 && code >= 0xe0a080 && (code < 0xeda080 || code >= 0xee8080)) {
				dst[dst_len + 0] = (char)c;
				dst[dst_len + 1] = (char)t0;
				dst[dst_len + 2] = (char)t1;
				dst_len += 3;
				index += 3;
				continue;
			}
		} else if ((c & 0xf8) == 0xf0 && left >= 4) {
			uint8_t t0 = (uint8_t)str[index + 1], t1 = (uint8_t)str[index + 2], t2 = (uint8_t)str[index + 3];
			uint32_t code = (uint32_t)c << 24 | (uint32_t)t0 << 16 | (uint32_t)t1 << 8 | (uint32_t)t2;
			if ((code & 0xc0c0c0) == 0x808080 && code >= 0xf0908080u && code <= 0xf48fbfbfu) {
				dst[dst_len + 0] = (char)c;
				dst[dst_len + 1] = (char)t0;
				dst[dst_len + 2] = (char)t1;
				dst[dst_len + 3] = (char)t2;
				dst_len += 4;
				index += 4;
				continue;
			}
		}

		dst_len += ufbxi_add_replacement_char(pool, dst + dst_len, (char)c);
		index++;
	}

	// Sanitized strings are packed to 32-bit integers, in practice this should be fine
	// as strings are limited to 32-bit length in FBX itself.
	// The only problem case is a massive string that is full of unicode errors, ie.
	// >1GB binary blob, but these should never be sanitized.
	ufbxi_check_err(pool->error, length <= UINT32_MAX);
	sanitized->raw_data = pool->temp_str;
	if (push_both) {
		// Reserve `UINT32_MAX` for invalid UTF-8 without sanitization
		size_t utf8_length = dst_len - (length + 1);
		ufbxi_check_err(pool->error, utf8_length < UINT32_MAX);
		sanitized->raw_length = (uint32_t)length;
		sanitized->utf8_length = (uint32_t)utf8_length;
	} else {
		ufbxi_check_err(pool->error, dst_len <= UINT32_MAX);
		sanitized->raw_length = (uint32_t)dst_len;
		sanitized->utf8_length = 0;
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_sanitized_string(ufbxi_string_pool *pool, ufbxi_sanitized_string *sanitized, const char *str, size_t length, uint32_t hash, bool raw)
{
	ufbxi_regression_assert(hash == ufbxi_hash_string(str, length));

	ufbxi_check_err(pool->error, length <= UINT32_MAX);
	ufbxi_check_err(pool->error, ufbxi_map_grow(&pool->map, ufbx_string, pool->initial_size));

	const char *total_data = str;
	size_t total_length = length;

	sanitized->raw_length = (uint32_t)length;
	sanitized->utf8_length = 0;

	if (!raw) {
		size_t valid_length = ufbxi_utf8_valid_length(str, length);
		if (valid_length != length) {
			ufbxi_check_err(pool->error, ufbxi_sanitize_string(pool, sanitized, str, length, valid_length, true));
			total_data = sanitized->raw_data;
			total_length = sanitized->raw_length + sanitized->utf8_length + 1;
			hash = ufbxi_hash_string(str, length);
		}
	}

	ufbx_string ref = { total_data, total_length };

	ufbx_string *entry = ufbxi_map_find(&pool->map, ufbx_string, hash, &ref);
	if (entry) {
		sanitized->raw_data = entry->data;
	} else {
		entry = ufbxi_map_insert(&pool->map, ufbx_string, hash, &ref);
		ufbxi_check_err(pool->error, entry);
		entry->length = total_length;
		char *dst = ufbxi_push(&pool->buf, char, total_length + 1);
		ufbxi_check_err(pool->error, dst);
		memcpy(dst, total_data, total_length);
		dst[total_length] = '\0';
		entry->data = dst;
		sanitized->raw_data = dst;
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline const char *ufbxi_push_string_imp(ufbxi_string_pool *pool, const char *str, size_t length, size_t *p_out_length, bool copy, bool raw)
{
	if (length == 0) return ufbxi_empty_char;

	ufbxi_check_return_err(pool->error, ufbxi_map_grow(&pool->map, ufbx_string, pool->initial_size), NULL);

	uint32_t hash;
	if (raw) {
		hash = ufbxi_hash_string(str, length);
	} else {
		bool non_ascii = false;
		hash = ufbxi_hash_string_check_ascii(str, length, &non_ascii);
		if (non_ascii) {
			size_t valid_length = ufbxi_utf8_valid_length(str, length);
			if (valid_length < length) {
				ufbxi_sanitized_string sanitized;
				ufbxi_check_return_err(pool->error, ufbxi_sanitize_string(pool, &sanitized, str, length, valid_length, false), NULL);
				str = sanitized.raw_data;
				length = sanitized.raw_length;
				hash = ufbxi_hash_string(str, length);
				*p_out_length = length;
			}
		}
	}

	ufbx_string ref = { str, length };

	ufbx_string *entry = ufbxi_map_find(&pool->map, ufbx_string, hash, &ref);
	if (entry) return entry->data;
	entry = ufbxi_map_insert(&pool->map, ufbx_string, hash, &ref);
	ufbxi_check_return_err(pool->error, entry, NULL);
	entry->length = length;
	if (copy) {
		char *dst = ufbxi_push(&pool->buf, char, length + 1);
		ufbxi_check_return_err(pool->error, dst, NULL);
		memcpy(dst, str, length);
		dst[length] = '\0';
		entry->data = dst;
	} else {
		entry->data = str;
	}
	return entry->data;
}

ufbxi_nodiscard static ufbxi_forceinline const char *ufbxi_push_string(ufbxi_string_pool *pool, const char *str, size_t length, size_t *p_out_length, bool raw)
{
	return ufbxi_push_string_imp(pool, str, length, p_out_length, true, raw);
}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_push_string_place(ufbxi_string_pool *pool, const char **p_str, size_t *p_length, bool raw)
{
	const char *str = *p_str;
	size_t length = *p_length;
	ufbxi_check_err(pool->error, str || length == 0);
	str = ufbxi_push_string(pool, str, length, p_length, raw);
	ufbxi_check_err(pool->error, str);
	*p_str = str;
	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_string_place_str(ufbxi_string_pool *pool, ufbx_string *p_str, bool raw)
{
	ufbxi_check_err(pool->error, p_str);
	return ufbxi_push_string_place(pool, &p_str->data, &p_str->length, raw);
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_string_place_blob(ufbxi_string_pool *pool, ufbx_blob *p_blob, bool raw)
{
	if (p_blob->size == 0) {
		p_blob->data = NULL;
		return 1;
	}
	p_blob->data = ufbxi_push_string(pool, (const char*)p_blob->data, p_blob->size, &p_blob->size, raw);
	ufbxi_check_err(pool->error, p_blob->data);
	return 1;
}

// -- String constants
//
// All strings in FBX files are pooled so by having canonical string constant
// addresses we can compare strings to these constants by comparing pointers.
// Keep the list alphabetically sorted!

static const char ufbxi_AllSame[] = "AllSame";
static const char ufbxi_Alphas[] = "Alphas";
static const char ufbxi_AmbientColor[] = "AmbientColor";
static const char ufbxi_AnimationCurveNode[] = "AnimationCurveNode";
static const char ufbxi_AnimationCurve[] = "AnimationCurve";
static const char ufbxi_AnimationLayer[] = "AnimationLayer";
static const char ufbxi_AnimationStack[] = "AnimationStack";
static const char ufbxi_ApertureFormat[] = "ApertureFormat";
static const char ufbxi_ApertureMode[] = "ApertureMode";
static const char ufbxi_AreaLightShape[] = "AreaLightShape";
static const char ufbxi_AspectH[] = "AspectH";
static const char ufbxi_AspectHeight[] = "AspectHeight";
static const char ufbxi_AspectRatioMode[] = "AspectRatioMode";
static const char ufbxi_AspectW[] = "AspectW";
static const char ufbxi_AspectWidth[] = "AspectWidth";
static const char ufbxi_Audio[] = "Audio";
static const char ufbxi_AudioLayer[] = "AudioLayer";
static const char ufbxi_BaseLayer[] = "BaseLayer";
static const char ufbxi_BinaryData[] = "BinaryData";
static const char ufbxi_BindPose[] = "BindPose";
static const char ufbxi_BindingTable[] = "BindingTable";
static const char ufbxi_Binormals[] = "Binormals";
static const char ufbxi_BinormalsIndex[] = "BinormalsIndex";
static const char ufbxi_BinormalsW[] = "BinormalsW";
static const char ufbxi_BlendMode[] = "BlendMode";
static const char ufbxi_BlendModes[] = "BlendModes";
static const char ufbxi_BlendShapeChannel[] = "BlendShapeChannel";
static const char ufbxi_BlendShape[] = "BlendShape";
static const char ufbxi_BlendWeights[] = "BlendWeights";
static const char ufbxi_BoundaryRule[] = "BoundaryRule";
static const char ufbxi_Boundary[] = "Boundary";
static const char ufbxi_ByEdge[] = "ByEdge";
static const char ufbxi_ByPolygonVertex[] = "ByPolygonVertex";
static const char ufbxi_ByPolygon[] = "ByPolygon";
static const char ufbxi_ByVertex[] = "ByVertex";
static const char ufbxi_ByVertice[] = "ByVertice";
static const char ufbxi_Cache[] = "Cache";
static const char ufbxi_CameraProjectionType[] = "CameraProjectionType";
static const char ufbxi_CameraStereo[] = "CameraStereo";
static const char ufbxi_CameraSwitcher[] = "CameraSwitcher";
static const char ufbxi_Camera[] = "Camera";
static const char ufbxi_CastLight[] = "CastLight";
static const char ufbxi_CastShadows[] = "CastShadows";
static const char ufbxi_Channel[] = "Channel";
static const char ufbxi_Character[] = "Character";
static const char ufbxi_Children[] = "Children";
static const char ufbxi_Cluster[] = "Cluster";
static const char ufbxi_CollectionExclusive[] = "CollectionExclusive";
static const char ufbxi_Collection[] = "Collection";
static const char ufbxi_ColorIndex[] = "ColorIndex";
static const char ufbxi_Color[] = "Color";
static const char ufbxi_Colors[] = "Colors";
static const char ufbxi_Cone_angle[] = "Cone angle";
static const char ufbxi_ConeAngle[] = "ConeAngle";
static const char ufbxi_Connections[] = "Connections";
static const char ufbxi_Constraint[] = "Constraint";
static const char ufbxi_Content[] = "Content";
static const char ufbxi_CoordAxisSign[] = "CoordAxisSign";
static const char ufbxi_CoordAxis[] = "CoordAxis";
static const char ufbxi_Count[] = "Count";
static const char ufbxi_Creator[] = "Creator";
static const char ufbxi_CurrentTextureBlendMode[] = "CurrentTextureBlendMode";
static const char ufbxi_CurrentTimeMarker[] = "CurrentTimeMarker";
static const char ufbxi_CustomFrameRate[] = "CustomFrameRate";
static const char ufbxi_DecayType[] = "DecayType";
static const char ufbxi_DefaultCamera[] = "DefaultCamera";
static const char ufbxi_Default[] = "Default";
static const char ufbxi_Definitions[] = "Definitions";
static const char ufbxi_DeformPercent[] = "DeformPercent";
static const char ufbxi_Deformer[] = "Deformer";
static const char ufbxi_DiffuseColor[] = "DiffuseColor";
static const char ufbxi_Dimension[] = "Dimension";
static const char ufbxi_Dimensions[] = "Dimensions";
static const char ufbxi_DisplayLayer[] = "DisplayLayer";
static const char ufbxi_Document[] = "Document";
static const char ufbxi_Documents[] = "Documents";
static const char ufbxi_EdgeCrease[] = "EdgeCrease";
static const char ufbxi_EdgeIndexArray[] = "EdgeIndexArray";
static const char ufbxi_Edges[] = "Edges";
static const char ufbxi_EmissiveColor[] = "EmissiveColor";
static const char ufbxi_Entry[] = "Entry";
static const char ufbxi_FBXHeaderExtension[] = "FBXHeaderExtension";
static const char ufbxi_FBXVersion[] = "FBXVersion";
static const char ufbxi_FKEffector[] = "FKEffector";
static const char ufbxi_FarPlane[] = "FarPlane";
static const char ufbxi_FbxPropertyEntry[] = "FbxPropertyEntry";
static const char ufbxi_FbxSemanticEntry[] = "FbxSemanticEntry";
static const char ufbxi_FieldOfViewX[] = "FieldOfViewX";
static const char ufbxi_FieldOfViewY[] = "FieldOfViewY";
static const char ufbxi_FieldOfView[] = "FieldOfView";
static const char ufbxi_FileName[] = "FileName";
static const char ufbxi_Filename[] = "Filename";
static const char ufbxi_FilmHeight[] = "FilmHeight";
static const char ufbxi_FilmSqueezeRatio[] = "FilmSqueezeRatio";
static const char ufbxi_FilmWidth[] = "FilmWidth";
static const char ufbxi_FlipNormals[] = "FlipNormals";
static const char ufbxi_FocalLength[] = "FocalLength";
static const char ufbxi_Form[] = "Form";
static const char ufbxi_Freeze[] = "Freeze";
static const char ufbxi_FrontAxisSign[] = "FrontAxisSign";
static const char ufbxi_FrontAxis[] = "FrontAxis";
static const char ufbxi_FullWeights[] = "FullWeights";
static const char ufbxi_GateFit[] = "GateFit";
static const char ufbxi_GeometricRotation[] = "GeometricRotation";
static const char ufbxi_GeometricScaling[] = "GeometricScaling";
static const char ufbxi_GeometricTranslation[] = "GeometricTranslation";
static const char ufbxi_GeometryUVInfo[] = "GeometryUVInfo";
static const char ufbxi_Geometry[] = "Geometry";
static const char ufbxi_GlobalSettings[] = "GlobalSettings";
static const char ufbxi_Hole[] = "Hole";
static const char ufbxi_HotSpot[] = "HotSpot";
static const char ufbxi_IKEffector[] = "IKEffector";
static const char ufbxi_ImageData[] = "ImageData";
static const char ufbxi_Implementation[] = "Implementation";
static const char ufbxi_Indexes[] = "Indexes";
static const char ufbxi_InheritType[] = "InheritType";
static const char ufbxi_InnerAngle[] = "InnerAngle";
static const char ufbxi_Intensity[] = "Intensity";
static const char ufbxi_IsTheNodeInSet[] = "IsTheNodeInSet";
static const char ufbxi_KeyAttrDataFloat[] = "KeyAttrDataFloat";
static const char ufbxi_KeyAttrFlags[] = "KeyAttrFlags";
static const char ufbxi_KeyAttrRefCount[] = "KeyAttrRefCount";
static const char ufbxi_KeyCount[] = "KeyCount";
static const char ufbxi_KeyTime[] = "KeyTime";
static const char ufbxi_KeyValueFloat[] = "KeyValueFloat";
static const char ufbxi_Key[] = "Key";
static const char ufbxi_KnotVectorU[] = "KnotVectorU";
static const char ufbxi_KnotVectorV[] = "KnotVectorV";
static const char ufbxi_KnotVector[] = "KnotVector";
static const char ufbxi_LayerElementBinormal[] = "LayerElementBinormal";
static const char ufbxi_LayerElementColor[] = "LayerElementColor";
static const char ufbxi_LayerElementEdgeCrease[] = "LayerElementEdgeCrease";
static const char ufbxi_LayerElementHole[] = "LayerElementHole";
static const char ufbxi_LayerElementMaterial[] = "LayerElementMaterial";
static const char ufbxi_LayerElementNormal[] = "LayerElementNormal";
static const char ufbxi_LayerElementPolygonGroup[] = "LayerElementPolygonGroup";
static const char ufbxi_LayerElementSmoothing[] = "LayerElementSmoothing";
static const char ufbxi_LayerElementTangent[] = "LayerElementTangent";
static const char ufbxi_LayerElementUV[] = "LayerElementUV";
static const char ufbxi_LayerElementVertexCrease[] = "LayerElementVertexCrease";
static const char ufbxi_LayerElementVisibility[] = "LayerElementVisibility";
static const char ufbxi_LayerElement[] = "LayerElement";
static const char ufbxi_Layer[] = "Layer";
static const char ufbxi_LayeredTexture[] = "LayeredTexture";
static const char ufbxi_Lcl_Rotation[] = "Lcl Rotation";
static const char ufbxi_Lcl_Scaling[] = "Lcl Scaling";
static const char ufbxi_Lcl_Translation[] = "Lcl Translation";
static const char ufbxi_LeftCamera[] = "LeftCamera";
static const char ufbxi_LightType[] = "LightType";
static const char ufbxi_Light[] = "Light";
static const char ufbxi_LimbLength[] = "LimbLength";
static const char ufbxi_LimbNode[] = "LimbNode";
static const char ufbxi_Limb[] = "Limb";
static const char ufbxi_Line[] = "Line";
static const char ufbxi_Link[] = "Link";
static const char ufbxi_LocalStart[] = "LocalStart";
static const char ufbxi_LocalStop[] = "LocalStop";
static const char ufbxi_LocalTime[] = "LocalTime";
static const char ufbxi_LodGroup[] = "LodGroup";
static const char ufbxi_MappingInformationType[] = "MappingInformationType";
static const char ufbxi_Marker[] = "Marker";
static const char ufbxi_MaterialAssignation[] = "MaterialAssignation";
static const char ufbxi_Material[] = "Material";
static const char ufbxi_Materials[] = "Materials";
static const char ufbxi_Matrix[] = "Matrix";
static const char ufbxi_Media[] = "Media";
static const char ufbxi_Mesh[] = "Mesh";
static const char ufbxi_Model[] = "Model";
static const char ufbxi_Name[] = "Name";
static const char ufbxi_NearPlane[] = "NearPlane";
static const char ufbxi_NodeAttributeName[] = "NodeAttributeName";
static const char ufbxi_NodeAttribute[] = "NodeAttribute";
static const char ufbxi_Node[] = "Node";
static const char ufbxi_Normals[] = "Normals";
static const char ufbxi_NormalsIndex[] = "NormalsIndex";
static const char ufbxi_NormalsW[] = "NormalsW";
static const char ufbxi_Null[] = "Null";
static const char ufbxi_NurbsCurve[] = "NurbsCurve";
static const char ufbxi_NurbsSurfaceOrder[] = "NurbsSurfaceOrder";
static const char ufbxi_NurbsSurface[] = "NurbsSurface";
static const char ufbxi_Nurbs[] = "Nurbs";
static const char ufbxi_OO[] = "OO\0";
static const char ufbxi_OP[] = "OP\0";
static const char ufbxi_ObjectMetaData[] = "ObjectMetaData";
static const char ufbxi_ObjectType[] = "ObjectType";
static const char ufbxi_Objects[] = "Objects";
static const char ufbxi_Order[] = "Order";
static const char ufbxi_OriginalUnitScaleFactor[] = "OriginalUnitScaleFactor";
static const char ufbxi_OriginalUpAxis[] = "OriginalUpAxis";
static const char ufbxi_OriginalUpAxisSign[] = "OriginalUpAxisSign";
static const char ufbxi_OrthoZoom[] = "OrthoZoom";
static const char ufbxi_OuterAngle[] = "OuterAngle";
static const char ufbxi_PO[] = "PO\0";
static const char ufbxi_PP[] = "PP\0";
static const char ufbxi_PointsIndex[] = "PointsIndex";
static const char ufbxi_Points[] = "Points";
static const char ufbxi_PolygonGroup[] = "PolygonGroup";
static const char ufbxi_PolygonIndexArray[] = "PolygonIndexArray";
static const char ufbxi_PolygonVertexIndex[] = "PolygonVertexIndex";
static const char ufbxi_PoseNode[] = "PoseNode";
static const char ufbxi_Pose[] = "Pose";
static const char ufbxi_PostRotation[] = "PostRotation";
static const char ufbxi_PreRotation[] = "PreRotation";
static const char ufbxi_PreviewDivisionLevels[] = "PreviewDivisionLevels";
static const char ufbxi_Properties60[] = "Properties60";
static const char ufbxi_Properties70[] = "Properties70";
static const char ufbxi_PropertyTemplate[] = "PropertyTemplate";
static const char ufbxi_R[] = "R\0\0";
static const char ufbxi_ReferenceStart[] = "ReferenceStart";
static const char ufbxi_ReferenceStop[] = "ReferenceStop";
static const char ufbxi_ReferenceTime[] = "ReferenceTime";
static const char ufbxi_RelativeFileName[] = "RelativeFileName";
static const char ufbxi_RelativeFilename[] = "RelativeFilename";
static const char ufbxi_RenderDivisionLevels[] = "RenderDivisionLevels";
static const char ufbxi_RightCamera[] = "RightCamera";
static const char ufbxi_RootNode[] = "RootNode";
static const char ufbxi_Root[] = "Root";
static const char ufbxi_RotationAccumulationMode[] = "RotationAccumulationMode";
static const char ufbxi_RotationOffset[] = "RotationOffset";
static const char ufbxi_RotationOrder[] = "RotationOrder";
static const char ufbxi_RotationPivot[] = "RotationPivot";
static const char ufbxi_Rotation[] = "Rotation";
static const char ufbxi_S[] = "S\0\0";
static const char ufbxi_ScaleAccumulationMode[] = "ScaleAccumulationMode";
static const char ufbxi_ScalingOffset[] = "ScalingOffset";
static const char ufbxi_ScalingPivot[] = "ScalingPivot";
static const char ufbxi_Scaling[] = "Scaling";
static const char ufbxi_SceneInfo[] = "SceneInfo";
static const char ufbxi_SelectionNode[] = "SelectionNode";
static const char ufbxi_SelectionSet[] = "SelectionSet";
static const char ufbxi_ShadingModel[] = "ShadingModel";
static const char ufbxi_Shape[] = "Shape";
static const char ufbxi_Shininess[] = "Shininess";
static const char ufbxi_Show[] = "Show";
static const char ufbxi_Size[] = "Size";
static const char ufbxi_Skin[] = "Skin";
static const char ufbxi_SkinningType[] = "SkinningType";
static const char ufbxi_Smoothing[] = "Smoothing";
static const char ufbxi_Smoothness[] = "Smoothness";
static const char ufbxi_SnapOnFrameMode[] = "SnapOnFrameMode";
static const char ufbxi_SpecularColor[] = "SpecularColor";
static const char ufbxi_Step[] = "Step";
static const char ufbxi_SubDeformer[] = "SubDeformer";
static const char ufbxi_T[] = "T\0\0";
static const char ufbxi_Take[] = "Take";
static const char ufbxi_Takes[] = "Takes";
static const char ufbxi_Tangents[] = "Tangents";
static const char ufbxi_TangentsIndex[] = "TangentsIndex";
static const char ufbxi_TangentsW[] = "TangentsW";
static const char ufbxi_Texture[] = "Texture";
static const char ufbxi_Texture_alpha[] = "Texture alpha";
static const char ufbxi_TextureId[] = "TextureId";
static const char ufbxi_TextureRotationPivot[] = "TextureRotationPivot";
static const char ufbxi_TextureScalingPivot[] = "TextureScalingPivot";
static const char ufbxi_TextureUV[] = "TextureUV";
static const char ufbxi_TextureUVVerticeIndex[] = "TextureUVVerticeIndex";
static const char ufbxi_Thumbnail[] = "Thumbnail";
static const char ufbxi_TimeMarker[] = "TimeMarker";
static const char ufbxi_TimeMode[] = "TimeMode";
static const char ufbxi_TimeProtocol[] = "TimeProtocol";
static const char ufbxi_TimeSpanStart[] = "TimeSpanStart";
static const char ufbxi_TimeSpanStop[] = "TimeSpanStop";
static const char ufbxi_TransformLink[] = "TransformLink";
static const char ufbxi_Transform[] = "Transform";
static const char ufbxi_Translation[] = "Translation";
static const char ufbxi_TrimNurbsSurface[] = "TrimNurbsSurface";
static const char ufbxi_Type[] = "Type";
static const char ufbxi_TypedIndex[] = "TypedIndex";
static const char ufbxi_UVIndex[] = "UVIndex";
static const char ufbxi_UVSet[] = "UVSet";
static const char ufbxi_UVSwap[] = "UVSwap";
static const char ufbxi_UV[] = "UV\0";
static const char ufbxi_UnitScaleFactor[] = "UnitScaleFactor";
static const char ufbxi_UpAxisSign[] = "UpAxisSign";
static const char ufbxi_UpAxis[] = "UpAxis";
static const char ufbxi_Version5[] = "Version5";
static const char ufbxi_VertexCacheDeformer[] = "VertexCacheDeformer";
static const char ufbxi_VertexCrease[] = "VertexCrease";
static const char ufbxi_VertexCreaseIndex[] = "VertexCreaseIndex";
static const char ufbxi_VertexIndexArray[] = "VertexIndexArray";
static const char ufbxi_Vertices[] = "Vertices";
static const char ufbxi_Video[] = "Video";
static const char ufbxi_Visibility[] = "Visibility";
static const char ufbxi_Weight[] = "Weight";
static const char ufbxi_Weights[] = "Weights";
static const char ufbxi_WrapModeU[] = "WrapModeU";
static const char ufbxi_WrapModeV[] = "WrapModeV";
static const char ufbxi_X[] = "X\0\0";
static const char ufbxi_Y[] = "Y\0\0";
static const char ufbxi_Z[] = "Z\0\0";
static const char ufbxi_d_X[] = "d|X";
static const char ufbxi_d_Y[] = "d|Y";
static const char ufbxi_d_Z[] = "d|Z";

static const ufbx_string ufbxi_strings[] = {
	{ ufbxi_AllSame, 7 },
	{ ufbxi_Alphas, 6 },
	{ ufbxi_AmbientColor, 12 },
	{ ufbxi_AnimationCurve, 14 },
	{ ufbxi_AnimationCurveNode, 18 },
	{ ufbxi_AnimationLayer, 14 },
	{ ufbxi_AnimationStack, 14 },
	{ ufbxi_ApertureFormat, 14 },
	{ ufbxi_ApertureMode, 12 },
	{ ufbxi_AreaLightShape, 14 },
	{ ufbxi_AspectH, 7 },
	{ ufbxi_AspectHeight, 12 },
	{ ufbxi_AspectRatioMode, 15 },
	{ ufbxi_AspectW, 7 },
	{ ufbxi_AspectWidth, 11 },
	{ ufbxi_Audio, 5 },
	{ ufbxi_AudioLayer, 10 },
	{ ufbxi_BaseLayer, 9 },
	{ ufbxi_BinaryData, 10 },
	{ ufbxi_BindPose, 8 },
	{ ufbxi_BindingTable, 12 },
	{ ufbxi_Binormals, 9 },
	{ ufbxi_BinormalsIndex, 14 },
	{ ufbxi_BinormalsW, 10 },
	{ ufbxi_BlendMode, 9 },
	{ ufbxi_BlendModes, 10 },
	{ ufbxi_BlendShape, 10 },
	{ ufbxi_BlendShapeChannel, 17 },
	{ ufbxi_BlendWeights, 12 },
	{ ufbxi_Boundary, 8 },
	{ ufbxi_BoundaryRule, 12 },
	{ ufbxi_ByEdge, 6 },
	{ ufbxi_ByPolygon, 9 },
	{ ufbxi_ByPolygonVertex, 15 },
	{ ufbxi_ByVertex, 8 },
	{ ufbxi_ByVertice, 9 },
	{ ufbxi_Cache, 5 },
	{ ufbxi_Camera, 6 },
	{ ufbxi_CameraProjectionType, 20 },
	{ ufbxi_CameraStereo, 12 },
	{ ufbxi_CameraSwitcher, 14 },
	{ ufbxi_CastLight, 9 },
	{ ufbxi_CastShadows, 11 },
	{ ufbxi_Channel, 7 },
	{ ufbxi_Character, sizeof(ufbxi_Character) - 1 },
	{ ufbxi_Children, 8 },
	{ ufbxi_Cluster, 7 },
	{ ufbxi_Collection, 10 },
	{ ufbxi_CollectionExclusive, 19 },
	{ ufbxi_Color, 5 },
	{ ufbxi_ColorIndex, 10 },
	{ ufbxi_Colors, 6 },
	{ ufbxi_Cone_angle, 10 },
	{ ufbxi_ConeAngle, 9 },
	{ ufbxi_Connections, 11 },
	{ ufbxi_Constraint, sizeof(ufbxi_Constraint) - 1 },
	{ ufbxi_Content, 7 },
	{ ufbxi_CoordAxis, 9 },
	{ ufbxi_CoordAxisSign, 13 },
	{ ufbxi_Count, 5 },
	{ ufbxi_Creator, 7 },
	{ ufbxi_CurrentTextureBlendMode, 23 },
	{ ufbxi_CurrentTimeMarker, 17 },
	{ ufbxi_CustomFrameRate, 15 },
	{ ufbxi_DecayType, 9 },
	{ ufbxi_Default, 7 },
	{ ufbxi_DefaultCamera, 13 },
	{ ufbxi_Definitions, 11 },
	{ ufbxi_DeformPercent, 13 },
	{ ufbxi_Deformer, 8 },
	{ ufbxi_DiffuseColor, 12 },
	{ ufbxi_Dimension, 9 },
	{ ufbxi_Dimensions, 10 },
	{ ufbxi_DisplayLayer, 12 },
	{ ufbxi_Document, 8 },
	{ ufbxi_Documents, 9 },
	{ ufbxi_EdgeCrease, 10 },
	{ ufbxi_EdgeIndexArray, 14 },
	{ ufbxi_Edges, 5 },
	{ ufbxi_EmissiveColor, 13 },
	{ ufbxi_Entry, 5 },
	{ ufbxi_FBXHeaderExtension, 18 },
	{ ufbxi_FBXVersion, 10 },
	{ ufbxi_FKEffector, 10 },
	{ ufbxi_FarPlane, 8 },
	{ ufbxi_FbxPropertyEntry, 16 },
	{ ufbxi_FbxSemanticEntry, 16 },
	{ ufbxi_FieldOfView, 11 },
	{ ufbxi_FieldOfViewX, 12 },
	{ ufbxi_FieldOfViewY, 12 },
	{ ufbxi_FileName, 8 },
	{ ufbxi_Filename, 8 },
	{ ufbxi_FilmHeight, 10 },
	{ ufbxi_FilmSqueezeRatio, 16 },
	{ ufbxi_FilmWidth, 9 },
	{ ufbxi_FlipNormals, 11 },
	{ ufbxi_FocalLength, 11 },
	{ ufbxi_Form, 4 },
	{ ufbxi_Freeze, 6 },
	{ ufbxi_FrontAxis, 9 },
	{ ufbxi_FrontAxisSign, 13 },
	{ ufbxi_FullWeights, 11 },
	{ ufbxi_GateFit, 7 },
	{ ufbxi_GeometricRotation, 17 },
	{ ufbxi_GeometricScaling, 16 },
	{ ufbxi_GeometricTranslation, 20 },
	{ ufbxi_Geometry, 8 },
	{ ufbxi_GeometryUVInfo, 14 },
	{ ufbxi_GlobalSettings, 14 },
	{ ufbxi_Hole, 4 },
	{ ufbxi_HotSpot, 7 },
	{ ufbxi_IKEffector, 10 },
	{ ufbxi_ImageData, 9 },
	{ ufbxi_Implementation, 14 },
	{ ufbxi_Indexes, 7 },
	{ ufbxi_InheritType, 11 },
	{ ufbxi_InnerAngle, 10 },
	{ ufbxi_Intensity, 9 },
	{ ufbxi_IsTheNodeInSet, 14 },
	{ ufbxi_Key, 3 },
	{ ufbxi_KeyAttrDataFloat, 16 },
	{ ufbxi_KeyAttrFlags, 12 },
	{ ufbxi_KeyAttrRefCount, 15 },
	{ ufbxi_KeyCount, 8 },
	{ ufbxi_KeyTime, 7 },
	{ ufbxi_KeyValueFloat, 13 },
	{ ufbxi_KnotVector, 10 },
	{ ufbxi_KnotVectorU, 11 },
	{ ufbxi_KnotVectorV, 11 },
	{ ufbxi_Layer, 5 },
	{ ufbxi_LayerElement, 12 },
	{ ufbxi_LayerElementBinormal, 20 },
	{ ufbxi_LayerElementColor, 17 },
	{ ufbxi_LayerElementEdgeCrease, 22 },
	{ ufbxi_LayerElementHole, 16 },
	{ ufbxi_LayerElementMaterial, 20 },
	{ ufbxi_LayerElementNormal, 18 },
	{ ufbxi_LayerElementPolygonGroup, 24 },
	{ ufbxi_LayerElementSmoothing, 21 },
	{ ufbxi_LayerElementTangent, 19 },
	{ ufbxi_LayerElementUV, 14 },
	{ ufbxi_LayerElementVertexCrease, 24 },
	{ ufbxi_LayerElementVisibility, 22 },
	{ ufbxi_LayeredTexture, 14 },
	{ ufbxi_Lcl_Rotation, 12 },
	{ ufbxi_Lcl_Scaling, 11 },
	{ ufbxi_Lcl_Translation, 15 },
	{ ufbxi_LeftCamera, 10 },
	{ ufbxi_Light, 5 },
	{ ufbxi_LightType, 9 },
	{ ufbxi_Limb, 4 },
	{ ufbxi_LimbLength, 10 },
	{ ufbxi_LimbNode, 8 },
	{ ufbxi_Line, 4 },
	{ ufbxi_Link, 4 },
	{ ufbxi_LocalStart, 10 },
	{ ufbxi_LocalStop, 9 },
	{ ufbxi_LocalTime, 9 },
	{ ufbxi_LodGroup, 8 },
	{ ufbxi_MappingInformationType, 22 },
	{ ufbxi_Marker, 6 },
	{ ufbxi_Material, 8 },
	{ ufbxi_MaterialAssignation, 19 },
	{ ufbxi_Materials, 9 },
	{ ufbxi_Matrix, 6 },
	{ ufbxi_Media, 5 },
	{ ufbxi_Mesh, 4 },
	{ ufbxi_Model, 5 },
	{ ufbxi_Name, 4 },
	{ ufbxi_NearPlane, 9 },
	{ ufbxi_Node, 4 },
	{ ufbxi_NodeAttribute, 13 },
	{ ufbxi_NodeAttributeName, 17 },
	{ ufbxi_Normals, 7 },
	{ ufbxi_NormalsIndex, 12 },
	{ ufbxi_NormalsW, 8 },
	{ ufbxi_Null, 4 },
	{ ufbxi_Nurbs, 5 },
	{ ufbxi_NurbsCurve, 10 },
	{ ufbxi_NurbsSurface, 12 },
	{ ufbxi_NurbsSurfaceOrder, 17 },
	{ ufbxi_OO, 2 },
	{ ufbxi_OP, 2 },
	{ ufbxi_ObjectMetaData, 14 },
	{ ufbxi_ObjectType, 10 },
	{ ufbxi_Objects, 7 },
	{ ufbxi_Order, 5 },
	{ ufbxi_OriginalUnitScaleFactor, 23 },
	{ ufbxi_OriginalUpAxis, 14 },
	{ ufbxi_OriginalUpAxisSign, 18 },
	{ ufbxi_OrthoZoom, 9 },
	{ ufbxi_OuterAngle, 10 },
	{ ufbxi_PO, 2 },
	{ ufbxi_PP, 2 },
	{ ufbxi_Points, 6 },
	{ ufbxi_PointsIndex, 11 },
	{ ufbxi_PolygonGroup, 12 },
	{ ufbxi_PolygonIndexArray, 17 },
	{ ufbxi_PolygonVertexIndex, 18 },
	{ ufbxi_Pose, 4 },
	{ ufbxi_PoseNode, 8 },
	{ ufbxi_PostRotation, 12 },
	{ ufbxi_PreRotation, 11 },
	{ ufbxi_PreviewDivisionLevels, 21 },
	{ ufbxi_Properties60, 12 },
	{ ufbxi_Properties70, 12 },
	{ ufbxi_PropertyTemplate, 16 },
	{ ufbxi_R, 1 },
	{ ufbxi_ReferenceStart, 14 },
	{ ufbxi_ReferenceStop, 13 },
	{ ufbxi_ReferenceTime, 13 },
	{ ufbxi_RelativeFileName, 16 },
	{ ufbxi_RelativeFilename, 16 },
	{ ufbxi_RenderDivisionLevels, 20 },
	{ ufbxi_RightCamera, 11 },
	{ ufbxi_Root, 4 },
	{ ufbxi_RootNode, 8 },
	{ ufbxi_Rotation, 8 },
	{ ufbxi_RotationAccumulationMode, 24 },
	{ ufbxi_RotationOffset, 14 },
	{ ufbxi_RotationOrder, 13 },
	{ ufbxi_RotationPivot, 13 },
	{ ufbxi_S, 1 },
	{ ufbxi_ScaleAccumulationMode, 21 },
	{ ufbxi_Scaling, 7 },
	{ ufbxi_ScalingOffset, 13 },
	{ ufbxi_ScalingPivot, 12 },
	{ ufbxi_SceneInfo, 9 },
	{ ufbxi_SelectionNode, 13 },
	{ ufbxi_SelectionSet, 12 },
	{ ufbxi_ShadingModel, 12 },
	{ ufbxi_Shape, 5 },
	{ ufbxi_Shininess, 9 },
	{ ufbxi_Show, 4 },
	{ ufbxi_Size, 4 },
	{ ufbxi_Skin, 4 },
	{ ufbxi_SkinningType, 12 },
	{ ufbxi_Smoothing, 9 },
	{ ufbxi_Smoothness, 10 },
	{ ufbxi_SnapOnFrameMode, 15 },
	{ ufbxi_SpecularColor, 13 },
	{ ufbxi_Step, 4 },
	{ ufbxi_SubDeformer, 11 },
	{ ufbxi_T, 1 },
	{ ufbxi_Take, 4 },
	{ ufbxi_Takes, 5 },
	{ ufbxi_Tangents, 8 },
	{ ufbxi_TangentsIndex, 13 },
	{ ufbxi_TangentsW, 9 },
	{ ufbxi_Texture, 7 },
	{ ufbxi_Texture_alpha, 13 },
	{ ufbxi_TextureId, 9 },
	{ ufbxi_TextureRotationPivot, 20 },
	{ ufbxi_TextureScalingPivot, 19 },
	{ ufbxi_TextureUV, 9 },
	{ ufbxi_TextureUVVerticeIndex, 21 },
	{ ufbxi_Thumbnail, 9 },
	{ ufbxi_TimeMarker, 10 },
	{ ufbxi_TimeMode, 8 },
	{ ufbxi_TimeProtocol, 12 },
	{ ufbxi_TimeSpanStart, 13 },
	{ ufbxi_TimeSpanStop, 12 },
	{ ufbxi_Transform, 9 },
	{ ufbxi_TransformLink, 13 },
	{ ufbxi_Translation, 11 },
	{ ufbxi_TrimNurbsSurface, 16 },
	{ ufbxi_Type, 4 },
	{ ufbxi_TypedIndex, 10 },
	{ ufbxi_UV, 2 },
	{ ufbxi_UVIndex, 7 },
	{ ufbxi_UVSet, 5 },
	{ ufbxi_UVSwap, 6 },
	{ ufbxi_UnitScaleFactor, 15 },
	{ ufbxi_UpAxis, 6 },
	{ ufbxi_UpAxisSign, 10 },
	{ ufbxi_Version5, 8 },
	{ ufbxi_VertexCacheDeformer, 19 },
	{ ufbxi_VertexCrease, 12 },
	{ ufbxi_VertexCreaseIndex, 17 },
	{ ufbxi_VertexIndexArray, 16 },
	{ ufbxi_Vertices, 8 },
	{ ufbxi_Video, 5 },
	{ ufbxi_Visibility, 10 },
	{ ufbxi_Weight, 6 },
	{ ufbxi_Weights, 7 },
	{ ufbxi_WrapModeU, 9 },
	{ ufbxi_WrapModeV, 9 },
	{ ufbxi_X, 1 },
	{ ufbxi_Y, 1 },
	{ ufbxi_Z, 1 },
	{ ufbxi_d_X, 3 },
	{ ufbxi_d_Y, 3 },
	{ ufbxi_d_Z, 3 },
};

static const ufbx_vec3 ufbxi_one_vec3 = { 1.0f, 1.0f, 1.0f };

#define UFBXI_PI ((ufbx_real)3.14159265358979323846)
#define UFBXI_DPI (3.14159265358979323846)
#define UFBXI_DEG_TO_RAD ((ufbx_real)(UFBXI_PI / 180.0))
#define UFBXI_RAD_TO_DEG ((ufbx_real)(180.0 / UFBXI_PI))
#define UFBXI_DEG_TO_RAD_DOUBLE (UFBXI_DPI / 180.0)
#define UFBXI_RAD_TO_DEG_DOUBLE (180.0 / UFBXI_DPI)
#define UFBXI_MM_TO_INCH ((ufbx_real)0.0393700787)

ufbx_inline ufbx_vec3 ufbxi_add3(ufbx_vec3 a, ufbx_vec3 b) {
	ufbx_vec3 v = { a.x + b.x, a.y + b.y, a.z + b.z };
	return v;
}

ufbx_inline ufbx_vec3 ufbxi_sub3(ufbx_vec3 a, ufbx_vec3 b) {
	ufbx_vec3 v = { a.x - b.x, a.y - b.y, a.z - b.z };
	return v;
}

ufbx_inline ufbx_vec3 ufbxi_mul3(ufbx_vec3 a, ufbx_real b) {
	ufbx_vec3 v = { a.x * b, a.y * b, a.z * b };
	return v;
}

ufbx_inline ufbx_vec3 ufbxi_lerp3(ufbx_vec3 a, ufbx_vec3 b, ufbx_real t) {
	ufbx_real u = 1.0f - t;
	ufbx_vec3 v = { a.x*u + b.x*t, a.y*u + b.y*t, a.z*u + b.z*t };
	return v;
}

ufbx_inline ufbx_real ufbxi_dot3(ufbx_vec3 a, ufbx_vec3 b) {
	return a.x*b.x + a.y*b.y + a.z*b.z;
}

ufbx_inline ufbx_real ufbxi_length3(ufbx_vec3 v)
{
	return (ufbx_real)ufbx_sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
}

ufbx_inline ufbx_real ufbxi_min3(ufbx_vec3 v)
{
	return ufbxi_min_real(ufbxi_min_real(v.x, v.y), v.z);
}

ufbx_inline ufbx_vec3 ufbxi_cross3(ufbx_vec3 a, ufbx_vec3 b) {
	ufbx_vec3 v = { a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x };
	return v;
}

ufbx_inline ufbx_vec3 ufbxi_normalize3(ufbx_vec3 a) {
	ufbx_real len = (ufbx_real)ufbx_sqrt(ufbxi_dot3(a, a));
	if (len > UFBX_EPSILON) {
		return ufbxi_mul3(a, (ufbx_real)1.0 / len);
	} else {
		ufbx_vec3 zero = { (ufbx_real)0 };
		return zero;
	}
}

ufbx_inline ufbx_real ufbxi_distsq2(ufbx_vec2 a, ufbx_vec2 b) {
	ufbx_real dx = a.x - b.x, dy = a.y - b.y;
	return dx*dx + dy*dy;
}

static ufbxi_noinline ufbx_vec3 ufbxi_slow_normalize3(const ufbx_vec3 *a) {
	return ufbxi_normalize3(*a);
}

static ufbxi_noinline ufbx_vec3 ufbxi_slow_normalized_cross3(const ufbx_vec3 *a, const ufbx_vec3 *b) {
	return ufbxi_normalize3(ufbxi_cross3(*a, *b));
}

// -- Threading

typedef struct ufbxi_task ufbxi_task;
typedef struct ufbxi_thread ufbxi_thread;
typedef struct ufbxi_thread_pool ufbxi_thread_pool;

typedef bool ufbxi_task_fn(ufbxi_task *task);

struct ufbxi_task {
	void *data;
	const char *error;
};

typedef struct {
	ufbxi_task task;
	ufbxi_task_fn *fn;
} ufbxi_task_imp;

typedef struct {
	uint32_t max_index;
	uint32_t wait_index;
} ufbxi_task_group;

struct ufbxi_thread_pool {
	ufbx_thread_opts opts;
	ufbxi_allocator *ator;
	ufbx_error *error;
	void *user_ptr;

	bool enabled;
	bool failed;
	const char *error_desc;

	uint32_t start_index;
	uint32_t execute_index;
	uint32_t wait_index;

	ufbxi_task_group groups[UFBX_THREAD_GROUP_COUNT];
	uint32_t group;

	uint32_t num_tasks;
	ufbxi_task_imp *tasks;
};

static void ufbxi_thread_pool_execute(ufbxi_thread_pool *pool, uint32_t index)
{
	ufbxi_task_imp *imp = &pool->tasks[index % pool->num_tasks];
	if (imp->fn(&imp->task)) {
		imp->task.error = NULL;
	} else if (!imp->task.error) {
		imp->task.error = "";
	}
}

ufbxi_noinline static void ufbxi_thread_pool_update_finished(ufbxi_thread_pool *pool, uint32_t max_index)
{
	while (pool->wait_index < max_index) {
		ufbxi_task_imp *task = &pool->tasks[pool->wait_index % pool->num_tasks];
		if (!pool->failed && task->task.error) {
			pool->failed = true;
			pool->error_desc = task->task.error;
		}
		pool->wait_index += 1;
	}
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_thread_pool_wait_imp(ufbxi_thread_pool *pool, uint32_t group, bool can_fail)
{
	uint32_t max_index = pool->groups[group].max_index;

	if (pool->groups[group].wait_index < max_index) {
		pool->opts.pool.wait_fn(pool->opts.pool.user, (ufbx_thread_pool_context)pool, group, max_index);
		pool->groups[group].wait_index = max_index;
	}
	ufbxi_thread_pool_update_finished(pool, max_index);

	if (pool->failed && can_fail) {
		ufbx_error *error = pool->error;
		if (pool->error_desc) {
			error->description.data = pool->error_desc;
			error->description.length = strlen(pool->error_desc);
		}
		ufbxi_fail_err(error, "Task failed");
	}
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_thread_pool_wait_group(ufbxi_thread_pool *pool)
{
	ufbxi_check_err(pool->error, ufbxi_thread_pool_wait_imp(pool, pool->group, true));
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_thread_pool_wait_all(ufbxi_thread_pool *pool)
{
	for (uint32_t i = 0; i < UFBX_THREAD_GROUP_COUNT; i++) {
		ufbxi_check_err(pool->error, ufbxi_thread_pool_wait_imp(pool, pool->group, true));
		pool->group = (pool->group + 1) % UFBX_THREAD_GROUP_COUNT;
	}
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_thread_pool_init(ufbxi_thread_pool *pool, ufbx_error *error, ufbxi_allocator *ator, const ufbx_thread_opts *opts)
{
	if (!(opts->pool.run_fn && opts->pool.wait_fn)) return 1;
	pool->enabled = true;

	uint32_t num_tasks = (uint32_t)ufbxi_min_sz(opts->num_tasks, INT32_MAX);
	if (num_tasks == 0) {
		num_tasks = 2048;
	}

	pool->opts = *opts;
	if (pool->opts.pool.init_fn) {
		ufbx_thread_pool_info info; // ufbxi_uninit
		info.max_concurrent_tasks = num_tasks;
		ufbxi_check_err(error, pool->opts.pool.init_fn(pool->opts.pool.user, (ufbx_thread_pool_context)pool, &info));
	}
	pool->ator = ator;
	pool->error = error;

	pool->num_tasks = num_tasks;
	pool->tasks = ufbxi_alloc(ator, ufbxi_task_imp, num_tasks);
	ufbxi_check_err(error, pool->tasks);

	return 1;
}

ufbxi_noinline static void ufbxi_thread_pool_free(ufbxi_thread_pool *pool)
{
	if (!pool->enabled) return;

	// Wait for all pending tasks
	for (uint32_t i = 0; i < UFBX_THREAD_GROUP_COUNT; i++) {
		pool->group = (pool->group + 1) % UFBX_THREAD_GROUP_COUNT;
		ufbxi_ignore(ufbxi_thread_pool_wait_imp(pool, pool->group, false));
	}

	if (pool->opts.pool.free_fn) {
		pool->opts.pool.free_fn(pool->opts.pool.user, (ufbx_thread_pool_context)pool);
	}

	ufbxi_free(pool->ator, ufbxi_task_imp, pool->tasks, pool->num_tasks);
}

ufbxi_nodiscard ufbxi_noinline static uint32_t ufbxi_thread_pool_available_tasks(ufbxi_thread_pool *pool)
{
	return pool->num_tasks - (pool->start_index - pool->wait_index);
}

ufbxi_noinline static void ufbxi_thread_pool_flush_group(ufbxi_thread_pool *pool)
{
	uint32_t group = pool->group;
	uint32_t start_index = pool->execute_index;
	uint32_t count = pool->start_index - start_index;
	if (count > 0) {
		if (pool->opts.pool.run_fn) {
			pool->opts.pool.run_fn(pool->opts.pool.user, (ufbx_thread_pool_context)pool, group, start_index, count);
		}
		pool->groups[group].max_index = start_index + count;
		pool->execute_index = start_index + count;
	}
	pool->group = (group + 1) % UFBX_THREAD_GROUP_COUNT;
}

ufbxi_nodiscard ufbxi_noinline static ufbxi_task *ufbxi_thread_pool_create_task(ufbxi_thread_pool *pool, ufbxi_task_fn *fn)
{
	uint32_t index = pool->start_index;
	if (index - pool->wait_index >= pool->num_tasks) {
		if (index - pool->wait_index >= pool->num_tasks) {
			// No space left
			return NULL;
		}
	} else if (index == INT32_MAX) {
		// TODO: Expand to 64 bits if possible?
		return NULL;
	}

	ufbxi_task_imp *imp = &pool->tasks[index % pool->num_tasks];
	if (index < pool->num_tasks) {
		memset(imp, 0, sizeof(ufbxi_task_imp));
	}

	imp->fn = fn;

	return &imp->task;
}

static void ufbxi_thread_pool_run_task(ufbxi_thread_pool *pool, ufbxi_task *task)
{
	(void)task;
	uint32_t index = pool->start_index;
	ufbx_assert(task == &pool->tasks[index % pool->num_tasks].task);
	pool->start_index = index + 1;
}

// -- Type definitions

typedef struct ufbxi_node ufbxi_node;

typedef enum {
	UFBXI_VALUE_NONE,
	UFBXI_VALUE_NUMBER,
	UFBXI_VALUE_STRING,
	UFBXI_VALUE_ARRAY,
} ufbxi_value_type;

typedef union {
	struct { double f; int64_t i; }; // < if `UFBXI_PROP_NUMBER`
	ufbxi_sanitized_string s;        // < if `UFBXI_PROP_STRING`
} ufbxi_value;

typedef struct {
	void *data;  // < Pointer to `size` bool/int32_t/int64_t/float/double elements
	size_t size; // < Number of elements
	char type;   // < FBX type code: b/i/l/f/d
} ufbxi_value_array;

struct ufbxi_node {
	const char *name;      // < Name of the node (pooled, compare with == to ufbxi_* strings)
	uint32_t num_children; // < Number of child nodes
	uint8_t name_len;      // < Length of `name` in bytes

	// If `value_type_mask == UFBXI_PROP_ARRAY` then the node is an array
	// (`array` field is valid) otherwise the node has N values in `vals`
	// where the type of each value is stored in 2 bits per value from LSB.
	// ie. `vals[ix]` type is `(value_type_mask >> (ix*2)) & 0x3`
	uint16_t value_type_mask;

	ufbxi_node *children;
	union {
		ufbxi_value_array *array; // if `prop_type_mask == UFBXI_PROP_ARRAY`
		ufbxi_value *vals;        // otherwise
	};
};

typedef struct ufbxi_refcount ufbxi_refcount;

struct ufbxi_refcount {
	ufbxi_refcount *parent;
	void *align_0;
	uint32_t self_magic;
	uint32_t type_magic;
	ufbxi_buf buf;
	ufbxi_allocator ator;
	uint64_t zero_pad_pre[8];
	ufbxi_atomic_counter refcount;
	uint64_t zero_pad_post[8];
};

static ufbxi_noinline void ufbxi_init_ref(ufbxi_refcount *refcount, uint32_t magic, ufbxi_refcount *parent);
static ufbxi_noinline void ufbxi_retain_ref(ufbxi_refcount *refcount);

#define ufbxi_get_imp(type, ptr) ((type*)((char*)ptr - sizeof(ufbxi_refcount)))

typedef struct {
	ufbxi_refcount refcount;
	ufbx_scene scene;
	uint32_t magic;

	ufbxi_buf string_buf;
} ufbxi_scene_imp;

ufbx_static_assert(scene_imp_offset, offsetof(ufbxi_scene_imp, scene) == sizeof(ufbxi_refcount));

typedef struct {
	ufbxi_refcount refcount;
	ufbx_mesh mesh;
	uint32_t magic;
} ufbxi_mesh_imp;

ufbx_static_assert(mesh_imp_offset, offsetof(ufbxi_mesh_imp, mesh) == sizeof(ufbxi_refcount));

typedef struct {
	// Semantic string data and length eg. for a string token
	// this string doesn't include the quotes.
	char *str_data;
	size_t str_len;
	size_t str_cap;

	// Type of the token, either single character such as '{' or ':'
	// or one of UFBXI_ASCII_* defines.
	char type;

	// Sign for integer if negative.
	bool negative;

	// Parsed semantic value
	union {
		double f64;
		int64_t i64;
		size_t name_len;
	} value;
} ufbxi_ascii_token;

typedef struct {
	size_t max_token_length;

	const char *src;
	const char *src_yield;
	const char *src_end;

	bool read_first_comment;
	bool found_version;
	bool parse_as_f32;
	bool src_is_retained;

	ufbxi_buf *retain_buf;
	ufbxi_buf *src_buf;

	ufbxi_ascii_token prev_token;
	ufbxi_ascii_token token;
} ufbxi_ascii;

typedef struct {
	const char *type;
	ufbx_string sub_type;
	ufbx_props props;
} ufbxi_template;

typedef struct {
	uint64_t fbx_id;
	uint32_t element_id;
	uint32_t user_id;
} ufbxi_fbx_id_entry;

typedef struct {
	uint64_t node_fbx_id;
	uint64_t attr_fbx_id;
} ufbxi_fbx_attr_entry;

// Temporary connection before we resolve the element pointers
typedef struct {
	uint64_t src, dst;
	ufbx_string src_prop;
	ufbx_string dst_prop;
} ufbxi_tmp_connection;

typedef struct {
	uint64_t fbx_id;
	ufbx_string name;
	ufbx_props props;
	ufbx_dom_node *dom_node;
} ufbxi_element_info;

typedef struct {
	uint64_t bone_fbx_id;
	ufbx_matrix bone_to_world;
} ufbxi_tmp_bone_pose;

typedef struct {
	ufbx_string prop_name;
	uint32_t *face_texture;
	size_t num_faces;
	bool all_same;
} ufbxi_tmp_mesh_texture;

typedef struct {
	ufbxi_tmp_mesh_texture *texture_arr;
	size_t texture_count;
} ufbxi_mesh_extra;

typedef struct {
	int32_t material_id;
	int32_t texture_id;
	ufbx_string prop_name;
} ufbxi_tmp_material_texture;

typedef struct {
	int32_t *blend_modes;
	size_t num_blend_modes;

	ufbx_real *alphas;
	size_t num_alphas;
} ufbxi_texture_extra;

typedef enum {
	UFBXI_OBJ_ATTRIB_POSITION,
	UFBXI_OBJ_ATTRIB_UV,
	UFBXI_OBJ_ATTRIB_NORMAL,
	UFBXI_OBJ_ATTRIB_COLOR,
} ufbxi_obj_attrib;

#define UFBXI_OBJ_NUM_ATTRIBS 3
#define UFBXI_OBJ_NUM_ATTRIBS_EXT 4

typedef struct {
	uint64_t min_ix, max_ix;
} ufbxi_obj_index_range;

typedef struct {
	size_t num_faces;
	size_t num_indices;
	ufbxi_obj_index_range vertex_range[UFBXI_OBJ_NUM_ATTRIBS];

	ufbx_node *fbx_node;
	ufbx_mesh *fbx_mesh;

	uint64_t fbx_node_id;
	uint64_t fbx_mesh_id;

	uint32_t usemtl_base;

	uint32_t num_groups;
} ufbxi_obj_mesh;

typedef struct {
	const char *name;
	uint32_t local_id;
	uint32_t mesh_id;
} ufbxi_obj_group_entry;

typedef struct {
	uint64_t *indices;
	size_t num_left;
} ufbxi_obj_fast_indices;

// Temporary pointer to a `ufbx_anim_stack` by name used to patch start/stop
// time from "Takes" if necessary.
typedef struct {
	const char *name;
	ufbx_anim_stack *stack;
} ufbxi_tmp_anim_stack;

typedef struct {
	ufbx_string absolute_filename;
	ufbx_blob content;
} ufbxi_file_content;

typedef struct {

	// Current line and tokens.
	// NOTE: `line` and `tokens` are not NULL-terminated nor UTF-8!
	// `line` is guaranteed to be terminated by a `\n`
	ufbx_string line;
	ufbx_string *tokens;
	size_t tokens_cap;
	size_t num_tokens;

	ufbxi_obj_fast_indices fast_indices[UFBXI_OBJ_NUM_ATTRIBS];

	size_t vertex_count[UFBXI_OBJ_NUM_ATTRIBS_EXT];
	ufbxi_buf tmp_vertices[UFBXI_OBJ_NUM_ATTRIBS_EXT];
	ufbxi_buf tmp_indices[UFBXI_OBJ_NUM_ATTRIBS_EXT];
	ufbxi_buf tmp_color_valid;
	ufbxi_buf tmp_faces;
	ufbxi_buf tmp_face_smoothing;
	ufbxi_buf tmp_face_group;
	ufbxi_buf tmp_face_group_infos;
	ufbxi_buf tmp_face_material;
	ufbxi_buf tmp_meshes;
	ufbxi_buf tmp_props;

	ufbxi_map group_map;

	size_t read_progress;

	ufbxi_obj_mesh *mesh;

	uint64_t usemtl_fbx_id;
	uint32_t usemtl_index;
	ufbx_string usemtl_name;

	uint32_t face_material;

	uint32_t face_group;
	bool has_face_group;

	bool face_smoothing;
	bool has_face_smoothing;

	bool has_vertex_color;
	size_t mrgb_vertex_count;

	bool eof;
	bool initialized;

	ufbx_blob mtllib_relative_path;

	ufbx_material **tmp_materials;
	size_t tmp_materials_cap;

	ufbx_string object;
	ufbx_string group;
	bool material_dirty;
	bool object_dirty;
	bool group_dirty;
	bool face_group_dirty;

} ufbxi_obj_context;

typedef struct {

	ufbx_error error;
	uint32_t version;
	ufbx_exporter exporter;
	uint32_t exporter_version;
	bool from_ascii;
	bool local_big_endian;
	bool file_big_endian;
	bool sure_fbx;
	bool retain_mesh_parts;
	bool read_legacy_settings;
	uint32_t double_parse_flags;

	ufbx_load_opts opts;

	// IO
	uint64_t data_offset;
	ufbx_read_fn *read_fn;
	ufbx_skip_fn *skip_fn;
	void *read_user;

	char *read_buffer;
	size_t read_buffer_size;

	const char *data_begin;
	const char *data;
	size_t yield_size;
	size_t data_size;

	// Allocators
	ufbxi_allocator ator_result;
	ufbxi_allocator ator_tmp;

	// Temporary maps
	ufbxi_map prop_type_map;    // < `ufbxi_prop_type_name` Property type to enum
	ufbxi_map fbx_id_map;       // < `ufbxi_fbx_id_entry` FBX ID to local ID
	ufbxi_map texture_file_map; // < `ufbxi_texture_file_entry` absolute raw filename to element ID
	ufbxi_map anim_stack_map;   // < `ufbxi_tmp_anim_stack` anim stacks by name before finalization

	// 6x00 specific maps
	ufbxi_map fbx_attr_map;  // < `ufbxi_fbx_attr_entry` Node ID to attrib ID
	ufbxi_map node_prop_set; // < `const char*` Node property names

	// DOM nodes
	ufbxi_map dom_node_map; // < `const char*` Node property names

	// Temporary array
	char *tmp_arr;
	size_t tmp_arr_size;
	char *swap_arr;
	size_t swap_arr_size;

	// Generated index buffers
	size_t max_zero_indices;
	size_t max_consecutive_indices;

	// Temporary buffers
	ufbxi_buf tmp;
	ufbxi_buf tmp_parse;
	ufbxi_buf tmp_stack;
	ufbxi_buf tmp_connections;
	ufbxi_buf tmp_node_ids;
	ufbxi_buf tmp_elements;
	ufbxi_buf tmp_element_offsets;
	ufbxi_buf tmp_element_fbx_ids;
	ufbxi_buf tmp_element_ptrs;
	ufbxi_buf tmp_typed_element_offsets[UFBX_ELEMENT_TYPE_COUNT];
	ufbxi_buf tmp_mesh_textures;
	ufbxi_buf tmp_full_weights;
	ufbxi_buf tmp_dom_nodes;
	ufbxi_buf tmp_element_id;
	ufbxi_buf tmp_ascii_spans;
	ufbxi_buf tmp_thread_parse[UFBX_THREAD_GROUP_COUNT];
	size_t tmp_element_byte_offset;

	ufbxi_template *templates;
	size_t num_templates;

	ufbx_dom_node *dom_parse_toplevel;
	size_t dom_parse_num_children;

	uint32_t *p_element_id;

	// String pool
	ufbxi_string_pool string_pool;

	// Result buffers, these are retained in `ufbx_scene` returned to user.
	ufbxi_buf result;

	// Top-level state
	ufbxi_node *top_nodes;
	size_t top_nodes_len, top_nodes_cap;
	bool parsed_to_end;

	// "Focused" top-level node and child index, if `top_child_index == SIZE_MAX`
	// the children are parsed on demand.
	ufbxi_node *top_node;
	size_t top_child_index;
	ufbxi_node top_child;
	bool has_next_child;

	// Shared consecutive and all-zero index buffers
	uint32_t *zero_indices;
	uint32_t *consecutive_indices;

	// Call progress function periodically
	ptrdiff_t progress_timer;
	uint64_t progress_bytes_total;
	uint64_t latest_progress_bytes;
	size_t progress_interval;

	// Extra data on the side of elements
	void **element_extra_arr;
	size_t element_extra_cap;

	// Temporary per-element flags
	uint8_t *tmp_element_flag;

	// IO (cold)
	ufbx_close_fn *close_fn;
	ufbx_size_fn *size_fn;

	ufbxi_ascii ascii;

	bool has_geometry_transform_nodes;
	bool has_scale_helper_nodes;
	bool retain_vertex_w;

	ufbx_mirror_axis mirror_axis;

	ufbxi_node root;

	ufbx_scene scene;
	ufbxi_scene_imp *scene_imp;

	ufbx_inflate_retain *inflate_retain;

	// Per-mesh consecutive indices used by `ufbxi_flip_winding()`.
	uint32_t *tmp_mesh_consecutive_indices;

	uint64_t root_id;
	uint32_t num_elements;

	ufbxi_node legacy_node;
	uint64_t legacy_implicit_anim_layer_id;

	ufbxi_file_content *file_content;
	size_t num_file_content;

	int64_t ktime_sec;
	double ktime_sec_double;

	bool eof;
	ufbxi_obj_context obj;

	ufbx_matrix axis_matrix;
	ufbx_real unit_scale;

	ufbxi_warnings warnings;

	bool deferred_failure;
	bool deferred_load;

	const char *load_filename;
	size_t load_filename_len;

	bool parse_threaded;
	ufbxi_thread_pool thread_pool;

} ufbxi_context;

static ufbxi_noinline int ufbxi_fail_imp(ufbxi_context *uc, const char *cond, const char *func, uint32_t line)
{
	return ufbxi_fail_imp_err(&uc->error, cond, func, line);
}

#define ufbxi_check(cond) if (ufbxi_unlikely(!ufbxi_trace(cond))) return ufbxi_fail_imp(uc, ufbxi_cond_str(cond), ufbxi_function, ufbxi_line)
#define ufbxi_check_return(cond, ret) do { if (ufbxi_unlikely(!ufbxi_trace(cond))) { ufbxi_fail_imp(uc, ufbxi_cond_str(cond), ufbxi_function, ufbxi_line); return ret; } } while (0)
#define ufbxi_fail(desc) return ufbxi_fail_imp(uc, desc, ufbxi_function, ufbxi_line)
#define ufbxi_fail_return(desc, ret) do { ufbxi_fail_imp(uc, desc, ufbxi_function, ufbxi_line); return ret; } while (0)

#define ufbxi_check_msg(cond, msg) if (ufbxi_unlikely(!ufbxi_trace(cond))) return ufbxi_fail_imp(uc, ufbxi_error_msg(ufbxi_cond_str(cond), msg), ufbxi_function, ufbxi_line)
#define ufbxi_check_return_msg(cond, ret, msg) do { if (ufbxi_unlikely(!ufbxi_trace(cond))) { ufbxi_fail_imp(uc, ufbxi_error_msg(ufbxi_cond_str(cond), msg), ufbxi_function, ufbxi_line); return ret; } } while (0)
#define ufbxi_fail_msg(desc, msg) return ufbxi_fail_imp(uc, ufbxi_error_msg(desc, msg), ufbxi_function, ufbxi_line)

#define ufbxi_warnf(type, ...) ufbxi_warnf_imp(&uc->warnings, type, ~0u, __VA_ARGS__)
#define ufbxi_warnf_tag(type, element_id, ...) ufbxi_warnf_imp(&uc->warnings, type, (element_id), __VA_ARGS__)

// -- Progress

static ufbxi_forceinline uint64_t ufbxi_get_read_offset(ufbxi_context *uc)
{
	return uc->data_offset + ufbxi_to_size(uc->data - uc->data_begin);
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_report_progress(ufbxi_context *uc)
{
	if (!uc->opts.progress_cb.fn) return 1;

	uint64_t read_offset = ufbxi_get_read_offset(uc);
	uc->latest_progress_bytes = read_offset;

	ufbx_progress progress = { 0 };
	progress.bytes_read = read_offset;
	progress.bytes_total = uc->progress_bytes_total;
	if (progress.bytes_total < progress.bytes_read) {
		progress.bytes_total = progress.bytes_read;
	}

	uc->progress_timer = 1024;
	uint32_t result = (uint32_t)uc->opts.progress_cb.fn(uc->opts.progress_cb.user, &progress);
	ufbx_assert(result == UFBX_PROGRESS_CONTINUE || result == UFBX_PROGRESS_CANCEL);
	ufbxi_check_msg(result != UFBX_PROGRESS_CANCEL, "Cancelled");
	return 1;
}

// TODO: Remove `ufbxi_unused` when it's not needed anymore
ufbxi_unused ufbxi_nodiscard static ufbxi_forceinline int ufbxi_progress(ufbxi_context *uc, size_t work_units)
{
	if (!uc->opts.progress_cb.fn) return 1;
	ptrdiff_t left = uc->progress_timer - (ptrdiff_t)work_units;
	uc->progress_timer = left;
	if (left > 0) return 1;
	return ufbxi_report_progress(uc);
}

// -- IO

static ufbxi_noinline const char *ufbxi_refill(ufbxi_context *uc, size_t size, bool require_size)
{
	ufbx_assert(uc->data_size < size);
	ufbxi_check_return(!uc->eof, NULL);
	if (require_size) {
		ufbxi_check_return_msg(uc->read_fn || uc->data_size > 0, NULL, "Empty file");
		ufbxi_check_return_msg(uc->read_fn, NULL, "Truncated file");
	} else if (!uc->read_fn) {
		uc->eof = true;
		return uc->data;
	}

	void *data_to_free = NULL;
	size_t size_to_free = 0;

	// Grow the read buffer if necessary, data is copied over below with the
	// usual path so the free is deferred (`size_to_free`, `data_to_free`)
	if (size > uc->read_buffer_size) {
		size_t new_size = ufbxi_max_sz(size, uc->opts.read_buffer_size);
		new_size = ufbxi_max_sz(new_size, uc->read_buffer_size * 2);
		size_to_free = uc->read_buffer_size;
		data_to_free = uc->read_buffer;
		char *new_buffer = ufbxi_alloc(&uc->ator_tmp, char, new_size);
		ufbxi_check_return(new_buffer, NULL);
		uc->read_buffer = new_buffer;
		uc->read_buffer_size = new_size;
	}

	// Copy the remains of the previous buffer to the beginning of the new one
	size_t data_size = uc->data_size;
	if (data_size > 0) {
		ufbx_assert(uc->read_buffer != NULL && uc->data != NULL);
		memmove(uc->read_buffer, uc->data, data_size);
	}

	if (size_to_free) {
		ufbxi_free(&uc->ator_tmp, char, data_to_free, size_to_free);
	}

	// Fill the rest of the buffer with user data
	size_t data_capacity = uc->read_buffer_size;
	while (data_size < data_capacity) {
		size_t to_read = data_capacity - data_size;
		size_t read_result = uc->read_fn(uc->read_user, uc->read_buffer + data_size, to_read);
		ufbxi_check_return_msg(read_result != SIZE_MAX, NULL, "IO error");
		ufbxi_check_return(read_result <= to_read, NULL);
		data_size += read_result;
		if (read_result == 0) {
			uc->eof = true;
			break;
		}
	}

	if (require_size) {
		if (uc->data_offset == 0) {
			ufbxi_check_return_msg(data_size > 0, NULL, "Empty file");
		}
		ufbxi_check_return_msg(data_size >= size, NULL, "Truncated file");
	}

	uc->data_offset += ufbxi_to_size(uc->data - uc->data_begin);
	uc->data_begin = uc->data = uc->read_buffer;
	uc->data_size = data_size;

	return uc->read_buffer;
}

static ufbxi_forceinline void ufbxi_pause_progress(ufbxi_context *uc)
{
	uc->data_size += uc->yield_size;
	uc->yield_size = 0;
}

static ufbxi_noinline int ufbxi_resume_progress(ufbxi_context *uc)
{
	uc->yield_size = ufbxi_min_sz(uc->data_size, uc->progress_interval);
	uc->data_size -= uc->yield_size;

	if (ufbxi_get_read_offset(uc) - uc->latest_progress_bytes >= uc->progress_interval) {
		ufbxi_check(ufbxi_report_progress(uc));
	}

	return 1;
}

static ufbxi_noinline const char *ufbxi_yield(ufbxi_context *uc, size_t size)
{
	const char *ret;
	uc->data_size += uc->yield_size;
	if (uc->data_size >= size) {
		ret = uc->data;
	} else {
		ret = ufbxi_refill(uc, size, true);
	}
	uc->yield_size = ufbxi_min_sz(uc->data_size, ufbxi_max_sz(size, uc->progress_interval));
	uc->data_size -= uc->yield_size;

	ufbxi_check_return(ufbxi_report_progress(uc), NULL);
	return ret;
}

static ufbxi_forceinline const char *ufbxi_peek_bytes(ufbxi_context *uc, size_t size)
{
	if (uc->yield_size >= size) {
		return uc->data;
	} else {
		return ufbxi_yield(uc, size);
	}
}

static ufbxi_forceinline const char *ufbxi_read_bytes(ufbxi_context *uc, size_t size)
{
	// Refill the current buffer if necessary
	const char *ret;
	if (uc->yield_size >= size) {
		ret = uc->data;
	} else {
		ret = ufbxi_yield(uc, size);
		if (!ret) return NULL;
	}

	// Advance the read position inside the current buffer
	uc->yield_size -= size;
	uc->data = ret + size;
	return ret;
}

static ufbxi_forceinline void ufbxi_consume_bytes(ufbxi_context *uc, size_t size)
{
	// Bytes must have been checked first with `ufbxi_peek_bytes()`
	ufbx_assert(size <= uc->yield_size);
	uc->yield_size -= size;
	uc->data += size;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_skip_bytes(ufbxi_context *uc, uint64_t size)
{
	if (uc->skip_fn) {
		ufbxi_pause_progress(uc);

		if (size > uc->data_size) {
			size -= uc->data_size;
			uc->data += uc->data_size;
			uc->data_size = 0;

			uc->data_offset += size;
			while (size >= UFBXI_MAX_SKIP_SIZE) {
				size -= UFBXI_MAX_SKIP_SIZE;
				ufbxi_check_msg(uc->skip_fn(uc->read_user, UFBXI_MAX_SKIP_SIZE - 1), "Truncated file");

				// Check that we can read at least one byte in case the file is broken
				// and causes us to seek indefinitely forwards as `fseek()` does not
				// report if we hit EOF...
				char single_byte[1]; // ufbxi_uninit
				size_t num_read = uc->read_fn(uc->read_user, single_byte, 1);
				ufbxi_check_msg(num_read <= 1, "IO error");
				ufbxi_check_msg(num_read == 1, "Truncated file");
			}

			if (size > 0) {
				ufbxi_check_msg(uc->skip_fn(uc->read_user, (size_t)size), "Truncated file");
			}

		} else {
			uc->data += (size_t)size;
			uc->data_size -= (size_t)size;
		}

		ufbxi_check(ufbxi_resume_progress(uc));
	} else {
		// Read and discard bytes in reasonable chunks
		uint64_t skip_size = ufbxi_max64(uc->read_buffer_size, uc->opts.read_buffer_size);
		while (size > 0) {
			uint64_t to_skip = ufbxi_min64(size, skip_size);
			ufbxi_check(ufbxi_read_bytes(uc, (size_t)to_skip));
			size -= to_skip;
		}
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_to(ufbxi_context *uc, void *dst, size_t size)
{
	char *ptr = (char*)dst;

	ufbxi_pause_progress(uc);

	// Copy data from the current buffer first
	size_t len = ufbxi_min_sz(uc->data_size, size);
	memcpy(ptr, uc->data, len);
	uc->data += len;
	uc->data_size -= len;
	ptr += len;
	size -= len;

	// If there's data left to copy try to read from user IO
	if (size > 0) {
		uc->data_offset += ufbxi_to_size(uc->data - uc->data_begin);

		uc->data_begin = uc->data = NULL;
		uc->data_size = 0;
		ufbxi_check(uc->read_fn);

		while (size > 0) {
			size_t read_result = uc->read_fn(uc->read_user, ptr, size);
			ufbxi_check_msg(read_result != SIZE_MAX, "IO error");
			ufbxi_check(read_result != 0);

			ptr += read_result;
			size -= read_result;
			uc->data_offset += read_result;
		}
	}

	ufbxi_check(ufbxi_resume_progress(uc));

	return 1;
}

static ufbxi_noinline void ufbxi_init_ator(ufbx_error *error, ufbxi_allocator *ator, const ufbx_allocator_opts *opts, const char *name)
{
	ufbx_allocator_opts zero_opts;
	if (!opts) {
		memset(&zero_opts, 0, sizeof(zero_opts));
		opts = &zero_opts;
	}

	// `opts` is either passed in or `zero_opts`.
	// cppcheck-suppress uninitvar
	ator->ator = *opts;
	ator->error = error;
	ator->max_size = opts->memory_limit ? opts->memory_limit : SIZE_MAX;
	ator->max_allocs = opts->allocation_limit ? opts->allocation_limit : SIZE_MAX;
	ator->huge_size = opts->huge_threshold ? opts->huge_threshold : 0x100000;
	ator->chunk_max = opts->max_chunk_size ? opts->max_chunk_size : 0x1000000;
	ator->name = name;
}

typedef struct {
	ufbx_error error;

	ufbxi_allocator *parent_ator;
	ufbxi_allocator ator;
} ufbxi_file_context;

static ufbxi_noinline void ufbxi_begin_file_context(ufbxi_file_context *fc, ufbx_open_file_context ctx, const ufbx_allocator_opts *ator_opts)
{
	memset(fc, 0, sizeof(ufbxi_file_context));
	if (ctx) {
		fc->parent_ator = (ufbxi_allocator*)ctx;
		fc->ator = *fc->parent_ator;
		fc->ator.error = &fc->error;
	} else {
		ufbxi_init_ator(&fc->error, &fc->ator, ator_opts, "file");
	}
}

static ufbxi_noinline void ufbxi_end_file_context(ufbxi_file_context *fc, ufbx_error *error, bool ok)
{
	if (fc->parent_ator) {
		fc->ator.error = fc->parent_ator->error;
		*fc->parent_ator = fc->ator;
	} else {
		ufbxi_free_ator(&fc->ator);
	}
	if (error) {
		if (!ok) {
			ufbxi_fix_error_type(&fc->error, "Failed to open file", error);
		} else {
			ufbxi_clear_error(error);
		}
	}
}

// -- File IO

#if !defined(UFBX_NO_STDIO) && !defined(UFBX_EXTERNAL_STDIO)

static ufbxi_noinline FILE *ufbxi_fopen(ufbxi_file_context *fc, const char *path, size_t path_len, bool null_terminated)
{
	FILE *file = NULL;
#if !defined(UFBX_STANDARD_C) && defined(_WIN32)
	(void)null_terminated;
	wchar_t wpath_buf[256], *wpath = NULL; // ufbxi_uninit
	if (path_len < ufbxi_arraycount(wpath_buf) - 1) {
		wpath = wpath_buf;
	} else {
		wpath = ufbxi_alloc(&fc->ator, wchar_t, path_len + 1);
		if (!wpath) return NULL;
	}

	// Convert UTF-8 to UTF-16 but allow stray surrogate pairs as the Windows
	// file system encoding allows them as well..
	size_t wlen = 0;
	for (size_t i = 0; i < path_len; ) {
		uint32_t code = UINT32_MAX;
		char c = path[i++];
		if ((c & 0x80) == 0) {
			code = (uint32_t)c;
		} else if ((c & 0xe0) == 0xc0) {
			code = (uint32_t)(c & 0x1f);
			if (i < path_len) code = code << 6 | (uint32_t)(path[i++] & 0x3f);
		} else if ((c & 0xf0) == 0xe0) {
			code = (uint32_t)(c & 0x0f);
			if (i < path_len) code = code << 6 | (uint32_t)(path[i++] & 0x3f);
			if (i < path_len) code = code << 6 | (uint32_t)(path[i++] & 0x3f);
		} else if ((c & 0xf8) == 0xf0) {
			code = (uint32_t)(c & 0x07);
			if (i < path_len) code = code << 6 | (uint32_t)(path[i++] & 0x3f);
			if (i < path_len) code = code << 6 | (uint32_t)(path[i++] & 0x3f);
			if (i < path_len) code = code << 6 | (uint32_t)(path[i++] & 0x3f);
		}
		if (code < 0x10000) {
			wpath[wlen++] = (wchar_t)code;
		} else {
			code -= 0x10000;
			wpath[wlen++] = (wchar_t)(0xd800 + (code >> 10));
			wpath[wlen++] = (wchar_t)(0xdc00 + (code & 0x3ff));
		}
	}
	wpath[wlen] = 0;

	#if UFBXI_MSC_VER >= 1400
		if (_wfopen_s(&file, wpath, L"rb") != 0) file = NULL;
	#else
		file = _wfopen(wpath, L"rb");
	#endif
	if (wpath != wpath_buf) {
		ufbxi_free(&fc->ator, wchar_t, wpath, path_len + 1);
	}
#else
	char copy_buf[256], *copy = NULL; // ufbxi_uninit
	if (null_terminated) {
		copy = (char*)path;
	} else {
		if (path_len < ufbxi_arraycount(copy_buf) - 1) {
			copy = copy_buf;
		} else {
			copy = ufbxi_alloc(&fc->ator, char, path_len + 1);
			if (!copy) return NULL;
		}
		memcpy(copy, path, path_len);
		copy[path_len] = '\0';
	}
	file = fopen(copy, "rb");
	if (!null_terminated && copy != copy_buf) {
		ufbxi_free(&fc->ator, char, copy, path_len + 1);
	}
#endif
	if (!file) {
		ufbxi_set_err_info(&fc->error, path, path_len);
		ufbxi_report_err_msg(&fc->error, "file", "File not found");
	}
	return file;
}

static uint64_t ufbxi_ftell(FILE *file)
{
#if !defined(UFBX_STANDARD_C) && defined(UFBX_HAS_FTELLO)
	off_t result = ftello(file);
	if (result >= 0) return (uint64_t)result;
#elif !defined(UFBX_STANDARD_C) && defined(_MSC_VER)
	int64_t result = _ftelli64(file);
	if (result >= 0) return (uint64_t)result;
#else
	int64_t result = ftell(file);
	if (result >= 0) return (uint64_t)result;
#endif
	return UINT64_MAX;
}

static size_t ufbxi_stdio_read(void *user, void *data, size_t max_size)
{
	FILE *file = (FILE*)user;
	if (ferror(file)) return SIZE_MAX;
	return fread(data, 1, max_size, file);
}

static bool ufbxi_stdio_skip(void *user, size_t size)
{
	FILE *file = (FILE*)user;
	ufbx_assert(size <= UFBXI_MAX_SKIP_SIZE);
	if (fseek(file, (long)size, SEEK_CUR) != 0) return false;
	if (ferror(file)) return false;
	return true;
}

static uint64_t ufbxi_stdio_size(void *user)
{
	FILE *file = (FILE*)user;
	uint64_t result = 0;
	uint64_t begin = ufbxi_ftell(file);
	if (begin < UINT64_MAX) {
		fpos_t pos; // ufbxi_uninit
		if (fgetpos(file, &pos) == 0) {
			if (fseek(file, 0, SEEK_END) == 0) {
				uint64_t end = ufbxi_ftell(file);
				if (end != UINT64_MAX && begin < end) {
					result = end - begin;
				}
				// Both `rewind()` and `fsetpos()` to reset error and EOF
				rewind(file);
				fsetpos(file, &pos);
			}
		}
	}
	return result;
}

static void ufbxi_stdio_close(void *user)
{
	FILE *file = (FILE*)user;
	fclose(file);
}

static ufbxi_noinline void ufbxi_stdio_init(ufbx_stream *stream, void *file, bool close)
{
	stream->read_fn = &ufbxi_stdio_read;
	stream->skip_fn = &ufbxi_stdio_skip;
	stream->size_fn = &ufbxi_stdio_size;
	stream->close_fn = close ? &ufbxi_stdio_close : NULL;
	stream->user = file;
}

static ufbxi_noinline bool ufbxi_stdio_open(ufbxi_file_context *fc, ufbx_stream *stream, const char *path, size_t path_len, bool null_terminated)
{
	FILE *file = ufbxi_fopen(fc, path, path_len, null_terminated);
	if (!file) return false;
	ufbxi_stdio_init(stream, file, true);
	return true;
}

#elif defined(UFBX_EXTERNAL_STDIO)

static ufbxi_noinline void ufbxi_stdio_init(ufbx_stream *stream, void *file, bool close)
{
	stream->read_fn = &ufbx_stdio_read;
	stream->skip_fn = &ufbx_stdio_skip;
	stream->size_fn = &ufbx_stdio_size;
	stream->close_fn = close ? &ufbx_stdio_close : NULL;
	stream->user = file;
}

static ufbxi_noinline bool ufbxi_stdio_open(ufbxi_file_context *fc, ufbx_stream *stream, const char *path, size_t path_len, bool null_terminated)
{
	char copy_buf[256], *copy = NULL; // ufbxi_uninit
	if (null_terminated) {
		copy = (char*)path;
	} else {
		if (path_len < ufbxi_arraycount(copy_buf) - 1) {
			copy = copy_buf;
		} else {
			copy = ufbxi_alloc(&fc->ator, char, path_len + 1);
			if (!copy) return false;
		}
		memcpy(copy, path, path_len);
		copy[path_len] = '\0';
	}
	void *file = ufbx_stdio_open(copy, path_len);
	if (!null_terminated && copy != copy_buf) {
		ufbxi_free(&fc->ator, char, copy, path_len + 1);
	}
	if (!file) {
		ufbxi_set_err_info(&fc->error, path, path_len);
		ufbxi_report_err_msg(&fc->error, "file", "File not found");
		return false;
	}
	ufbxi_stdio_init(stream, file, true);
	return true;
}

#endif

// -- Memory IO

typedef struct {
	const void *data;
	size_t size;
	size_t position;
	ufbx_close_memory_cb close_cb;

	// Own allocation information
	size_t self_size;
	ufbxi_allocator *parent_ator;
	ufbxi_allocator local_ator;
	ufbx_error error;
	char data_copy[];
} ufbxi_memory_stream;

static size_t ufbxi_memory_read(void *user, void *data, size_t max_size)
{
	ufbxi_memory_stream *stream = (ufbxi_memory_stream*)user;
	size_t to_read = ufbxi_min_sz(stream->size - stream->position, max_size);
	memcpy(data, (const char*)stream->data + stream->position, to_read);
	stream->position += to_read;
	return to_read;
}

static bool ufbxi_memory_skip(void *user, size_t size)
{
	ufbxi_memory_stream *stream = (ufbxi_memory_stream*)user;
	if (stream->size - stream->position < size) return false;
	stream->position += size;
	return true;
}

static uint64_t ufbxi_memory_size(void *user)
{
	ufbxi_memory_stream *stream = (ufbxi_memory_stream*)user;
	return stream->size;
}

static void ufbxi_memory_close(void *user)
{
	ufbxi_memory_stream *stream = (ufbxi_memory_stream*)user;
	if (stream->close_cb.fn) {
		stream->close_cb.fn(stream->close_cb.user, (void*)stream->data, stream->size);
	}

	if (stream->parent_ator) {
		ufbxi_free(stream->parent_ator, char, stream, stream->self_size);
	} else {
		ufbxi_allocator ator = stream->local_ator;
		ufbxi_free(&ator, char, stream, stream->self_size);
		ufbxi_free_ator(&ator);
	}
}

// -- XML

#if UFBXI_FEATURE_XML

typedef struct ufbxi_xml_tag ufbxi_xml_tag;
typedef struct ufbxi_xml_attrib ufbxi_xml_attrib;
typedef struct ufbxi_xml_document ufbxi_xml_document;

struct ufbxi_xml_attrib {
	ufbx_string name;
	ufbx_string value;
};

struct ufbxi_xml_tag {
	ufbx_string name;
	ufbx_string text;

	ufbxi_xml_attrib *attribs;
	size_t num_attribs;

	ufbxi_xml_tag *children;
	size_t num_children;
};

struct ufbxi_xml_document {
	ufbxi_xml_tag *root;
	ufbxi_buf buf;
};

typedef struct {
	ufbx_error error;

	ufbxi_allocator *ator;

	ufbxi_buf tmp_stack;
	ufbxi_buf result;

	ufbxi_xml_document *doc;

	ufbx_read_fn *read_fn;
	void *read_user;

	char *tok;
	size_t tok_cap;
	size_t tok_len;

	const char *pos, *pos_end;
	char data[4096];

	bool io_error;
} ufbxi_xml_context;

enum {
	UFBXI_XML_CTYPE_WHITESPACE = 0x1,
	UFBXI_XML_CTYPE_SINGLE_QUOTE = 0x2,
	UFBXI_XML_CTYPE_DOUBLE_QUOTE = 0x4,
	UFBXI_XML_CTYPE_NAME_END = 0x8,
	UFBXI_XML_CTYPE_TAG_START = 0x10,
	UFBXI_XML_CTYPE_END_OF_FILE = 0x20,
};

// Generated by `misc/gen_xml_ctype.py`
static const uint8_t ufbxi_xml_ctype[256] = {
	32,0,0,0,0,0,0,0,0,9,9,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	9,0,12,0,0,0,0,10,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,16,8,8,8,
};

static ufbxi_noinline void ufbxi_xml_refill(ufbxi_xml_context *xc)
{
	size_t num = xc->read_fn(xc->read_user, xc->data, sizeof(xc->data));
	if (num == SIZE_MAX || num < sizeof(xc->data)) xc->io_error = true;
	if (num < sizeof(xc->data)) {
		xc->data[num++] = '\0';
	}
	xc->pos = xc->data;
	xc->pos_end = xc->data + num;
}

static ufbxi_forceinline void ufbxi_xml_advance(ufbxi_xml_context *xc)
{
	if (++xc->pos == xc->pos_end) ufbxi_xml_refill(xc);
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_xml_push_token_char(ufbxi_xml_context *xc, char c)
{
	if (xc->tok_len == xc->tok_cap || UFBXI_IS_REGRESSION) {
		ufbxi_check_err(&xc->error, ufbxi_grow_array(xc->ator, &xc->tok, &xc->tok_cap, xc->tok_len + 1));
	}
	xc->tok[xc->tok_len++] = c;
	return 1;
}

static ufbxi_noinline int ufbxi_xml_accept(ufbxi_xml_context *xc, char ch)
{
	if (*xc->pos == ch) {
		ufbxi_xml_advance(xc);
		return 1;
	} else {
		return 0;
	}
}

static ufbxi_noinline void ufbxi_xml_skip_while(ufbxi_xml_context *xc, uint32_t ctypes)
{
	while (ufbxi_xml_ctype[(uint8_t)*xc->pos] & ctypes) {
		ufbxi_xml_advance(xc);
	}
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_xml_skip_until_string(ufbxi_xml_context *xc, ufbx_string *dst, const char *suffix)
{
	xc->tok_len = 0;
	size_t match_len = 0, ix = 0, suffix_len = strlen(suffix);
	char buf[16] = { 0 };
	size_t wrap_mask = sizeof(buf) - 1;
	ufbx_assert(suffix_len < sizeof(buf));
	for (;;) {
		char c = *xc->pos;
		ufbxi_check_err_msg(&xc->error, c != 0, "Truncated file");
		ufbxi_xml_advance(xc);
		if (ix >= suffix_len) {
			ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, buf[(ix - suffix_len) & wrap_mask]));
		}

		buf[ix++ & wrap_mask] = c;
		for (match_len = 0; match_len < suffix_len; match_len++) {
			if (buf[(ix - suffix_len + match_len) & wrap_mask] != suffix[match_len]) {
				break;
			}
		}
		if (match_len == suffix_len) break;
	}

	ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, '\0'));
	if (dst) {
		dst->length = xc->tok_len - 1;
		dst->data = ufbxi_push_copy(&xc->result, char, xc->tok_len, xc->tok);
		ufbxi_check_err(&xc->error, dst->data);
	}

	return 1;
}

static ufbxi_noinline int ufbxi_xml_read_until(ufbxi_xml_context *xc, ufbx_string *dst, uint32_t ctypes)
{
	xc->tok_len = 0;
	for (;;) {
		char c = *xc->pos;

		if (c == '&') {
			size_t entity_begin = xc->tok_len;
			for (;;) {
				ufbxi_xml_advance(xc);
				c = *xc->pos;
				ufbxi_check_err(&xc->error, c != '\0');
				if (c == ';') break;
				ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, c));
			}
			ufbxi_xml_advance(xc);
			ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, '\0'));

			char *entity = xc->tok + entity_begin;
			xc->tok_len = entity_begin;

			if (entity[0] == '#') {
				unsigned long code = 0;
				if (entity[1] == 'x') {
					code = ufbxi_parse_uint32_radix(entity + 2, 16);
				} else {
					code = ufbxi_parse_uint32_radix(entity + 1, 10);
				}

				char bytes[5] = { 0 };
				if (code < 0x80) {
					bytes[0] = (char)code;
				} else if (code < 0x800) {
					bytes[0] = (char)(0xc0 | (code>>6));
					bytes[1] = (char)(0x80 | (code & 0x3f));
				} else if (code < 0x10000) {
					bytes[0] = (char)(0xe0 | (code>>12));
					bytes[1] = (char)(0x80 | ((code>>6) & 0x3f));
					bytes[2] = (char)(0x80 | (code & 0x3f));
				} else {
					bytes[0] = (char)(0xf0 | (code>>18));
					bytes[1] = (char)(0x80 | ((code>>12) & 0x3f));
					bytes[2] = (char)(0x80 | ((code>>6) & 0x3f));
					bytes[3] = (char)(0x80 | (code & 0x3f));
				}
				for (char *b = bytes; *b; b++) {
					ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, *b));
				}
			} else {
				char ch = '\0';
				if (!strcmp(entity, "lt")) ch = '<';
				else if (!strcmp(entity, "quot")) ch = '"';
				else if (!strcmp(entity, "amp")) ch = '&';
				else if (!strcmp(entity, "apos")) ch = '\'';
				else if (!strcmp(entity, "gt")) ch = '>';
				if (ch) {
					ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, ch));
				}
			}
		} else {
			if ((ufbxi_xml_ctype[(uint8_t)c] & ctypes) != 0) break;
			ufbxi_check_err_msg(&xc->error, c != 0, "Truncated file");
			ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, c));
			ufbxi_xml_advance(xc);
		}
	}

	ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, '\0'));
	if (dst) {
		dst->length = xc->tok_len - 1;
		dst->data = ufbxi_push_copy(&xc->result, char, xc->tok_len, xc->tok);
		ufbxi_check_err(&xc->error, dst->data);
	}

	return 1;
}

// Recursion limited by check at the start
static ufbxi_noinline int ufbxi_xml_parse_tag(ufbxi_xml_context *xc, size_t depth, bool *p_closing, const char *opening)
	ufbxi_recursive_function(int, ufbxi_xml_parse_tag, (xc, depth, p_closing, opening), UFBXI_MAX_XML_DEPTH + 1,
		(ufbxi_xml_context *xc, size_t depth, bool *p_closing, const char *opening))
{
	ufbxi_check_err(&xc->error, depth < UFBXI_MAX_XML_DEPTH);

	if (!ufbxi_xml_accept(xc, '<')) {
		if (*xc->pos == '\0') {
			*p_closing = true;
		} else {
			ufbxi_check_err(&xc->error, ufbxi_xml_read_until(xc, NULL, UFBXI_XML_CTYPE_TAG_START | UFBXI_XML_CTYPE_END_OF_FILE));
			bool has_text = false;
			for (size_t i = 0; i < xc->tok_len; i++) {
				if ((ufbxi_xml_ctype[(uint8_t)xc->tok[i]] & UFBXI_XML_CTYPE_WHITESPACE) == 0) {
					has_text = true;
					break;
				}
			}

			if (has_text) {
				ufbxi_xml_tag *tag = ufbxi_push_zero(&xc->tmp_stack, ufbxi_xml_tag, 1);
				ufbxi_check_err(&xc->error, tag);
				tag->name.data = ufbxi_empty_char;

				tag->text.length = xc->tok_len - 1;
				tag->text.data = ufbxi_push_copy(&xc->result, char, xc->tok_len, xc->tok);
				ufbxi_check_err(&xc->error, tag->text.data);
			}
		}
		return 1;
	}

	if (ufbxi_xml_accept(xc, '/')) {
		ufbxi_check_err(&xc->error, ufbxi_xml_read_until(xc, NULL, UFBXI_XML_CTYPE_NAME_END));
		ufbxi_check_err(&xc->error, opening && !strcmp(xc->tok, opening));
		ufbxi_xml_skip_while(xc, UFBXI_XML_CTYPE_WHITESPACE);
		if (!ufbxi_xml_accept(xc, '>')) return 0;
		*p_closing = true;
		return 1;
	} else if (ufbxi_xml_accept(xc, '!')) {
		if (ufbxi_xml_accept(xc, '[')) {
			for (const char *ch = "CDATA["; *ch; ch++) {
				if (!ufbxi_xml_accept(xc, *ch)) return 0;
			}

			ufbxi_xml_tag *tag = ufbxi_push_zero(&xc->tmp_stack, ufbxi_xml_tag, 1);
			ufbxi_check_err(&xc->error, tag);
			ufbxi_check_err(&xc->error, ufbxi_xml_skip_until_string(xc, &tag->text, "]]>"));
			tag->name.data = ufbxi_empty_char;

		} else if (ufbxi_xml_accept(xc, '-')) {
			if (!ufbxi_xml_accept(xc, '-')) return 0;
			ufbxi_check_err(&xc->error, ufbxi_xml_skip_until_string(xc, NULL, "-->"));
		} else {
			// TODO: !DOCTYPE
			ufbxi_check_err(&xc->error, ufbxi_xml_skip_until_string(xc, NULL, ">"));
		}
		return 1;
	} else if (ufbxi_xml_accept(xc, '?')) {
		ufbxi_check_err(&xc->error, ufbxi_xml_skip_until_string(xc, NULL, "?>"));
		return 1;
	}

	ufbxi_xml_tag *tag = ufbxi_push_zero(&xc->tmp_stack, ufbxi_xml_tag, 1);
	ufbxi_check_err(&xc->error, tag);
	ufbxi_check_err(&xc->error, ufbxi_xml_read_until(xc, &tag->name, UFBXI_XML_CTYPE_NAME_END));
	tag->text.data = ufbxi_empty_char;

	bool has_children = false;

	size_t num_attribs = 0;
	for (;;) {
		ufbxi_xml_skip_while(xc, UFBXI_XML_CTYPE_WHITESPACE);
		if (ufbxi_xml_accept(xc, '/')) {
			if (!ufbxi_xml_accept(xc, '>')) return 0;
			break;
		} else if (ufbxi_xml_accept(xc, '>')) {
			has_children = true;
			break;
		} else {
			ufbxi_xml_attrib *attrib = ufbxi_push_zero(&xc->tmp_stack, ufbxi_xml_attrib, 1);
			ufbxi_check_err(&xc->error, attrib);
			ufbxi_check_err(&xc->error, ufbxi_xml_read_until(xc, &attrib->name, UFBXI_XML_CTYPE_NAME_END));
			ufbxi_xml_skip_while(xc, UFBXI_XML_CTYPE_WHITESPACE);
			if (!ufbxi_xml_accept(xc, '=')) return 0;
			ufbxi_xml_skip_while(xc, UFBXI_XML_CTYPE_WHITESPACE);
			uint32_t quote_ctype = 0;
			if (ufbxi_xml_accept(xc, '"')) {
				quote_ctype = UFBXI_XML_CTYPE_DOUBLE_QUOTE;
			} else if (ufbxi_xml_accept(xc, '\'')) {
				quote_ctype = UFBXI_XML_CTYPE_SINGLE_QUOTE;
			} else {
				ufbxi_fail_err(&xc->error, "Bad attrib value");
			}
			ufbxi_check_err(&xc->error, ufbxi_xml_read_until(xc, &attrib->value, quote_ctype));
			ufbxi_xml_advance(xc);
			num_attribs++;
		}
	}

	tag->num_attribs = num_attribs;
	tag->attribs = ufbxi_push_pop(&xc->result, &xc->tmp_stack, ufbxi_xml_attrib, num_attribs);
	ufbxi_check_err(&xc->error, tag->attribs);

	if (has_children) {
		size_t children_begin = xc->tmp_stack.num_items;
		for (;;) {
			bool closing = false;
			ufbxi_check_err(&xc->error, ufbxi_xml_parse_tag(xc, depth + 1, &closing, tag->name.data));
			if (closing) break;
		}

		tag->num_children = xc->tmp_stack.num_items - children_begin;
		tag->children = ufbxi_push_pop(&xc->result, &xc->tmp_stack, ufbxi_xml_tag, tag->num_children);
		ufbxi_check_err(&xc->error, tag->children);
	}

	return 1;
}

static ufbxi_noinline int ufbxi_xml_parse_root(ufbxi_xml_context *xc)
{
	ufbxi_xml_tag *tag = ufbxi_push_zero(&xc->result, ufbxi_xml_tag, 1);
	ufbxi_check_err(&xc->error, tag);
	tag->name.data = ufbxi_empty_char;
	tag->text.data = ufbxi_empty_char;

	for (;;) {
		bool closing = false;
		ufbxi_check_err(&xc->error, ufbxi_xml_parse_tag(xc, 0, &closing, NULL));
		if (closing) break;
	}

	tag->num_children = xc->tmp_stack.num_items;
	tag->children = ufbxi_push_pop(&xc->result, &xc->tmp_stack, ufbxi_xml_tag, tag->num_children);
	ufbxi_check_err(&xc->error, tag->children);

	xc->doc = ufbxi_push(&xc->result, ufbxi_xml_document, 1);
	ufbxi_check_err(&xc->error, xc->doc);

	xc->doc->root = tag;
	xc->doc->buf = xc->result;

	return 1;
}

typedef struct {
	ufbxi_allocator *ator;
	ufbx_read_fn *read_fn;
	void *read_user;
	const char *prefix;
	size_t prefix_length;
} ufbxi_xml_load_opts;

static ufbxi_noinline ufbxi_xml_document *ufbxi_load_xml(ufbxi_xml_load_opts *opts, ufbx_error *error)
{
	ufbxi_xml_context xc = { UFBX_ERROR_NONE };
	xc.ator = opts->ator;
	xc.read_fn = opts->read_fn;
	xc.read_user = opts->read_user;

	xc.tmp_stack.ator = xc.ator;
	xc.result.ator = xc.ator;

	xc.result.unordered = true;

	if (opts->prefix_length > 0) {
		xc.pos = opts->prefix;
		xc.pos_end = opts->prefix + opts->prefix_length;
	} else {
		ufbxi_xml_refill(&xc);
	}

	int ok = ufbxi_xml_parse_root(&xc);

	ufbxi_buf_free(&xc.tmp_stack);
	ufbxi_free(xc.ator, char, xc.tok, xc.tok_cap);

	if (ok) {
		return xc.doc;
	} else {
		ufbxi_buf_free(&xc.result);
		if (error) {
			*error = xc.error;
		}

		return NULL;
	}
}

static ufbxi_noinline void ufbxi_free_xml(ufbxi_xml_document *doc)
{
	ufbxi_buf buf = doc->buf;
	ufbxi_buf_free(&buf);
}

static ufbxi_noinline ufbxi_xml_tag *ufbxi_xml_find_child(ufbxi_xml_tag *tag, const char *name)
{
	ufbxi_for(ufbxi_xml_tag, child, tag->children, tag->num_children) {
		if (!strcmp(child->name.data, name)) {
			return child;
		}
	}
	return NULL;
}

static ufbxi_noinline ufbxi_xml_attrib *ufbxi_xml_find_attrib(ufbxi_xml_tag *tag, const char *name)
{
	ufbxi_for(ufbxi_xml_attrib, attrib, tag->attribs, tag->num_attribs) {
		if (!strcmp(attrib->name.data, name)) {
			return attrib;
		}
	}
	return NULL;
}

#endif

// -- FBX value type information

static char ufbxi_normalize_array_type(char type, char bool_type) {
	switch (type) {
	case 'r': return sizeof(ufbx_real) == sizeof(float) ? 'f' : 'd';
	case 'b': return bool_type;
	default: return type;
	}
}

static ufbxi_noinline size_t ufbxi_array_type_size(char type)
{
	switch (type) {
	case 'r': return sizeof(ufbx_real);
	case 'b': return sizeof(bool);
	case 'c': return sizeof(uint8_t);
	case 'i': return sizeof(int32_t);
	case 'l': return sizeof(int64_t);
	case 'f': return sizeof(float);
	case 'd': return sizeof(double);
	case 's': return sizeof(ufbx_string);
	case 'S': return sizeof(ufbx_string);
	case 'C': return sizeof(ufbx_string);
	default: return 1;
	}
}

// -- Node operations

static ufbxi_noinline ufbxi_node *ufbxi_find_child(ufbxi_node *node, const char *name)
{
	ufbxi_for(ufbxi_node, c, node->children, node->num_children) {
		if (c->name == name) return c;
	}
	return NULL;
}

// Retrieve values from nodes with type codes:
// Any: '_' (ignore)
// NUMBER: 'I' int32_t 'L' int64_t 'F' float 'D' double 'R' ufbxi_real 'B' bool 'Z' size_t
// STRING: 'S' ufbx_string 'C' const char* (checked) 's' ufbx_string 'c' const char * (unchecked) 'b' ufbx_blob

ufbxi_nodiscard ufbxi_forceinline static int ufbxi_get_val_at(ufbxi_node *node, size_t ix, char fmt, void *v)
{
	ufbxi_dev_assert(ix < UFBXI_MAX_NON_ARRAY_VALUES);
	ufbxi_value_type type = (ufbxi_value_type)((node->value_type_mask >> (ix*2)) & 0x3);
	switch (fmt) {
	case '_': return 1;
	case 'I': if (type == UFBXI_VALUE_NUMBER) { *(int32_t*)v = (int32_t)node->vals[ix].i; return 1; } else return 0;
	case 'L': if (type == UFBXI_VALUE_NUMBER) { *(int64_t*)v = (int64_t)node->vals[ix].i; return 1; } else return 0;
	case 'F': if (type == UFBXI_VALUE_NUMBER) { *(float*)v = (float)node->vals[ix].f; return 1; } else return 0;
	case 'D': if (type == UFBXI_VALUE_NUMBER) { *(double*)v = (double)node->vals[ix].f; return 1; } else return 0;
	case 'R': if (type == UFBXI_VALUE_NUMBER) { *(ufbx_real*)v = (ufbx_real)node->vals[ix].f; return 1; } else return 0;
	case 'B': if (type == UFBXI_VALUE_NUMBER) { *(bool*)v = node->vals[ix].i != 0; return 1; } else return 0;
	case 'Z': if (type == UFBXI_VALUE_NUMBER) { if (node->vals[ix].i < 0) return 0; *(size_t*)v = (size_t)node->vals[ix].i; return 1; } else return 0;
	case 'S': if (type == UFBXI_VALUE_STRING) {
		ufbxi_sanitized_string src = node->vals[ix].s;
		ufbx_string *dst = (ufbx_string*)v;
		if (src.utf8_length > 0) {
			if (src.utf8_length == UINT32_MAX) return 0;
			dst->data = src.raw_data + src.raw_length + 1;
			dst->length = src.utf8_length;
		} else {
			dst->data = src.raw_data;
			dst->length = src.raw_length;
		}
		return 1;
	} else return 0;
	case 's': if (type == UFBXI_VALUE_STRING) {
		ufbxi_sanitized_string src = node->vals[ix].s;
		ufbx_string *dst = (ufbx_string*)v;
		dst->data = src.raw_data;
		dst->length = src.raw_length;
		return 1;
	} else return 0;
	case 'C': if (type == UFBXI_VALUE_STRING) {
		ufbxi_sanitized_string src = node->vals[ix].s;
		const char **dst = (const char **)v;
		if (src.utf8_length > 0) {
			if (src.utf8_length == UINT32_MAX) return 0;
			*dst = src.raw_data + src.raw_length + 1;
		} else {
			*dst = src.raw_data;
		}
		return 1;
	} else return 0;
	case 'c': if (type == UFBXI_VALUE_STRING) {
		ufbxi_sanitized_string src = node->vals[ix].s;
		const char **dst = (const char **)v;
		*dst = src.raw_data;
		return 1;
	} else return 0;
	case 'b': if (type == UFBXI_VALUE_STRING) {
		ufbxi_sanitized_string src = node->vals[ix].s;
		ufbx_blob *dst = (ufbx_blob*)v;
		dst->data = src.raw_data;
		dst->size = src.raw_length;
		return 1;
	} else return 0;
	default:
		ufbxi_unreachable("Bad format char");
		return 0;
	}
}

ufbxi_nodiscard ufbxi_noinline static ufbxi_value_array *ufbxi_get_array(ufbxi_node *node, char fmt)
{
	if (node->value_type_mask != UFBXI_VALUE_ARRAY) return NULL;
	ufbxi_value_array *array = node->array;
	if (fmt != '?') {
		fmt = ufbxi_normalize_array_type(fmt, 'b');
		if (array->type != fmt) return NULL;
	}
	return array;
}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val1(ufbxi_node *node, const char *fmt, void *v0)
{
	if (!ufbxi_get_val_at(node, 0, fmt[0], v0)) return 0;
	return 1;
}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val2(ufbxi_node *node, const char *fmt, void *v0, void *v1)
{
	if (!ufbxi_get_val_at(node, 0, fmt[0], v0)) return 0;
	if (!ufbxi_get_val_at(node, 1, fmt[1], v1)) return 0;
	return 1;
}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val3(ufbxi_node *node, const char *fmt, void *v0, void *v1, void *v2)
{
	if (!ufbxi_get_val_at(node, 0, fmt[0], v0)) return 0;
	if (!ufbxi_get_val_at(node, 1, fmt[1], v1)) return 0;
	if (!ufbxi_get_val_at(node, 2, fmt[2], v2)) return 0;
	return 1;
}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val4(ufbxi_node *node, const char *fmt, void *v0, void *v1, void *v2, void *v3)
{
	if (!ufbxi_get_val_at(node, 0, fmt[0], v0)) return 0;
	if (!ufbxi_get_val_at(node, 1, fmt[1], v1)) return 0;
	if (!ufbxi_get_val_at(node, 2, fmt[2], v2)) return 0;
	if (!ufbxi_get_val_at(node, 3, fmt[3], v3)) return 0;
	return 1;
}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val5(ufbxi_node *node, const char *fmt, void *v0, void *v1, void *v2, void *v3, void *v4)
{
	if (!ufbxi_get_val_at(node, 0, fmt[0], v0)) return 0;
	if (!ufbxi_get_val_at(node, 1, fmt[1], v1)) return 0;
	if (!ufbxi_get_val_at(node, 2, fmt[2], v2)) return 0;
	if (!ufbxi_get_val_at(node, 3, fmt[3], v3)) return 0;
	if (!ufbxi_get_val_at(node, 4, fmt[4], v4)) return 0;
	return 1;
}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_find_val1(ufbxi_node *node, const char *name, const char *fmt, void *v0)
{
	ufbxi_node *child = ufbxi_find_child(node, name);
	if (!child) return 0;
	if (!ufbxi_get_val_at(child, 0, fmt[0], v0)) return 0;
	return 1;
}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_find_val2(ufbxi_node *node, const char *name, const char *fmt, void *v0, void *v1)
{
	ufbxi_node *child = ufbxi_find_child(node, name);
	if (!child) return 0;
	if (!ufbxi_get_val_at(child, 0, fmt[0], v0)) return 0;
	if (!ufbxi_get_val_at(child, 1, fmt[1], v1)) return 0;
	return 1;
}

ufbxi_nodiscard static ufbxi_noinline ufbxi_value_array *ufbxi_find_array(ufbxi_node *node, const char *name, char fmt)
{
	ufbxi_node *child = ufbxi_find_child(node, name);
	if (!child) return NULL;
	return ufbxi_get_array(child, fmt);
}

static ufbxi_node *ufbxi_find_child_strcmp(ufbxi_node *node, const char *name)
{
	char leading = name[0];
	ufbxi_for(ufbxi_node, c, node->children, node->num_children) {
		if (c->name[0] != leading) continue;
		if (!strcmp(c->name, name)) return c;
	}
	return NULL;
}

// -- Element extra data allocation

ufbxi_nodiscard static ufbxi_noinline void *ufbxi_push_element_extra_size(ufbxi_context *uc, uint32_t id, size_t size)
{
	if (uc->element_extra_cap <= id) {
		size_t old_cap = uc->element_extra_cap;
		ufbxi_check_return(ufbxi_grow_array(&uc->ator_tmp, &uc->element_extra_arr, &uc->element_extra_cap, id + 1), NULL);
		memset(uc->element_extra_arr + old_cap, 0, (uc->element_extra_cap - old_cap) * sizeof(void*));
	}

	if (uc->element_extra_arr[id]) return uc->element_extra_arr[id];

	void *extra = ufbxi_push_size_zero(&uc->tmp, size, 1);
	ufbxi_check_return(extra, NULL);
	uc->element_extra_arr[id] = extra;

	return extra;
}

static ufbxi_noinline void *ufbxi_get_element_extra(ufbxi_context *uc, uint32_t id)
{
	if (id < uc->element_extra_cap) {
		return uc->element_extra_arr[id];
	} else {
		return NULL;
	}
}

#define ufbxi_push_element_extra(uc, id, type) (type*)ufbxi_push_element_extra_size((uc), (id), sizeof(type))

// -- Parsing state machine
//
// When reading the file we maintain a coarse representation of the structure so
// that we can resolve array info (type, included in result, etc). Using this info
// we can often read/decompress the contents directly into the right memory area.

typedef enum {
	UFBXI_PARSE_ROOT,
	UFBXI_PARSE_FBX_HEADER_EXTENSION,
	UFBXI_PARSE_SCENE_INFO,
	UFBXI_PARSE_THUMBNAIL,
	UFBXI_PARSE_DEFINITIONS,
	UFBXI_PARSE_OBJECTS,
	UFBXI_PARSE_CONNECTIONS,
	UFBXI_PARSE_RELATIONS,
	UFBXI_PARSE_TAKES,
	UFBXI_PARSE_FBX_VERSION,
	UFBXI_PARSE_MODEL,
	UFBXI_PARSE_GEOMETRY,
	UFBXI_PARSE_NODE_ATTRIBUTE,
	UFBXI_PARSE_LEGACY_MODEL,
	UFBXI_PARSE_LEGACY_MEDIA,
	UFBXI_PARSE_LEGACY_VIDEO,
	UFBXI_PARSE_LEGACY_SWITCHER,
	UFBXI_PARSE_LEGACY_SCENE_PERSISTENCE,
	UFBXI_PARSE_REFERENCES,
	UFBXI_PARSE_REFERENCE,
	UFBXI_PARSE_ANIMATION_CURVE,
	UFBXI_PARSE_DEFORMER,
	UFBXI_PARSE_ASSOCIATE_MODEL,
	UFBXI_PARSE_LEGACY_LINK,
	UFBXI_PARSE_POSE,
	UFBXI_PARSE_POSE_NODE,
	UFBXI_PARSE_TEXTURE,
	UFBXI_PARSE_VIDEO,
	UFBXI_PARSE_LAYERED_TEXTURE,
	UFBXI_PARSE_SELECTION_NODE,
	UFBXI_PARSE_COLLECTION,
	UFBXI_PARSE_AUDIO,
	UFBXI_PARSE_UNKNOWN_OBJECT,
	UFBXI_PARSE_LAYER_ELEMENT_NORMAL,
	UFBXI_PARSE_LAYER_ELEMENT_BINORMAL,
	UFBXI_PARSE_LAYER_ELEMENT_TANGENT,
	UFBXI_PARSE_LAYER_ELEMENT_UV,
	UFBXI_PARSE_LAYER_ELEMENT_COLOR,
	UFBXI_PARSE_LAYER_ELEMENT_VERTEX_CREASE,
	UFBXI_PARSE_LAYER_ELEMENT_EDGE_CREASE,
	UFBXI_PARSE_LAYER_ELEMENT_SMOOTHING,
	UFBXI_PARSE_LAYER_ELEMENT_VISIBILITY,
	UFBXI_PARSE_LAYER_ELEMENT_POLYGON_GROUP,
	UFBXI_PARSE_LAYER_ELEMENT_HOLE,
	UFBXI_PARSE_LAYER_ELEMENT_MATERIAL,
	UFBXI_PARSE_LAYER_ELEMENT_OTHER,
	UFBXI_PARSE_GEOMETRY_UV_INFO,
	UFBXI_PARSE_SHAPE,
	UFBXI_PARSE_TAKE,
	UFBXI_PARSE_TAKE_OBJECT,
	UFBXI_PARSE_CHANNEL,
	UFBXI_PARSE_UNKNOWN,
} ufbxi_parse_state;

typedef enum {
	UFBXI_ARRAY_FLAG_RESULT       = 0x1, // < Allocate the array from the result buffer
	UFBXI_ARRAY_FLAG_TMP_BUF      = 0x2, // < Allocate the array from the result buffer
	UFBXI_ARRAY_FLAG_PAD_BEGIN    = 0x4, // < Pad the begin of the array with 4 zero elements to guard from invalid -1 index accesses
	UFBXI_ARRAY_FLAG_ACCURATE_F32 = 0x8, // < Must be parsed as bit-accurate 32-bit floats
} ufbxi_array_flags;

typedef struct {
	char type;      // < FBX type code of the array: b,i,l,f,d (or 'r' meaning ufbx_real '-' ignore, 's'/'S' for strings, 'C' for content)
	uint8_t flags;  // < Combination of `ufbxi_array_flags`
} ufbxi_array_info;

static ufbxi_noinline ufbxi_parse_state ufbxi_update_parse_state(ufbxi_parse_state parent, const char *name)
{
	switch (parent) {

	case UFBXI_PARSE_ROOT:
		if (name == ufbxi_FBXHeaderExtension) return UFBXI_PARSE_FBX_HEADER_EXTENSION;
		if (name == ufbxi_Definitions) return UFBXI_PARSE_DEFINITIONS;
		if (name == ufbxi_Objects) return UFBXI_PARSE_OBJECTS;
		if (name == ufbxi_Connections) return UFBXI_PARSE_CONNECTIONS;
		if (name == ufbxi_Takes) return UFBXI_PARSE_TAKES;
		if (name == ufbxi_Model) return UFBXI_PARSE_LEGACY_MODEL;
		if (!strcmp(name, "References")) return UFBXI_PARSE_REFERENCES;
		if (!strcmp(name, "Relations")) return UFBXI_PARSE_RELATIONS;
		if (name == ufbxi_Media) return UFBXI_PARSE_LEGACY_MEDIA;
		if (!strcmp(name, "Switcher")) return UFBXI_PARSE_LEGACY_SWITCHER;
		if (!strcmp(name, "SceneGenericPersistence")) return UFBXI_PARSE_LEGACY_SCENE_PERSISTENCE;
		break;

	case UFBXI_PARSE_FBX_HEADER_EXTENSION:
		if (name == ufbxi_FBXVersion) return UFBXI_PARSE_FBX_VERSION;
		if (name == ufbxi_SceneInfo) return UFBXI_PARSE_SCENE_INFO;
		break;

	case UFBXI_PARSE_SCENE_INFO:
		if (name == ufbxi_Thumbnail) return UFBXI_PARSE_THUMBNAIL;
		break;

	case UFBXI_PARSE_OBJECTS:
		if (name == ufbxi_Model) return UFBXI_PARSE_MODEL;
		if (name == ufbxi_Geometry) return UFBXI_PARSE_GEOMETRY;
		if (name == ufbxi_NodeAttribute) return UFBXI_PARSE_NODE_ATTRIBUTE;
		if (name == ufbxi_AnimationCurve) return UFBXI_PARSE_ANIMATION_CURVE;
		if (name == ufbxi_Deformer) return UFBXI_PARSE_DEFORMER;
		if (name == ufbxi_Pose) return UFBXI_PARSE_POSE;
		if (name == ufbxi_Texture) return UFBXI_PARSE_TEXTURE;
		if (name == ufbxi_Video) return UFBXI_PARSE_VIDEO;
		if (name == ufbxi_LayeredTexture) return UFBXI_PARSE_LAYERED_TEXTURE;
		if (name == ufbxi_SelectionNode) return UFBXI_PARSE_SELECTION_NODE;
		if (name == ufbxi_Collection) return UFBXI_PARSE_COLLECTION;
		if (name == ufbxi_Audio) return UFBXI_PARSE_AUDIO;
		return UFBXI_PARSE_UNKNOWN_OBJECT;

	case UFBXI_PARSE_MODEL:
	case UFBXI_PARSE_GEOMETRY:
		if (name[0] == 'L') {
			if (name == ufbxi_LayerElementNormal) return UFBXI_PARSE_LAYER_ELEMENT_NORMAL;
			if (name == ufbxi_LayerElementBinormal) return UFBXI_PARSE_LAYER_ELEMENT_BINORMAL;
			if (name == ufbxi_LayerElementTangent) return UFBXI_PARSE_LAYER_ELEMENT_TANGENT;
			if (name == ufbxi_LayerElementUV) return UFBXI_PARSE_LAYER_ELEMENT_UV;
			if (name == ufbxi_LayerElementColor) return UFBXI_PARSE_LAYER_ELEMENT_COLOR;
			if (name == ufbxi_LayerElementVertexCrease) return UFBXI_PARSE_LAYER_ELEMENT_VERTEX_CREASE;
			if (name == ufbxi_LayerElementEdgeCrease) return UFBXI_PARSE_LAYER_ELEMENT_EDGE_CREASE;
			if (name == ufbxi_LayerElementSmoothing) return UFBXI_PARSE_LAYER_ELEMENT_SMOOTHING;
			if (name == ufbxi_LayerElementVisibility) return UFBXI_PARSE_LAYER_ELEMENT_VISIBILITY;
			if (name == ufbxi_LayerElementPolygonGroup) return UFBXI_PARSE_LAYER_ELEMENT_POLYGON_GROUP;
			if (name == ufbxi_LayerElementHole) return UFBXI_PARSE_LAYER_ELEMENT_HOLE;
			if (name == ufbxi_LayerElementMaterial) return UFBXI_PARSE_LAYER_ELEMENT_MATERIAL;
			if (!strncmp(name, "LayerElement", 12)) return UFBXI_PARSE_LAYER_ELEMENT_OTHER;
		}
		if (name == ufbxi_Shape) return UFBXI_PARSE_SHAPE;
		break;

	case UFBXI_PARSE_DEFORMER:
		if (!strcmp(name, "AssociateModel")) return UFBXI_PARSE_ASSOCIATE_MODEL;
		break;

	case UFBXI_PARSE_LEGACY_MEDIA:
		if (name == ufbxi_Video) return UFBXI_PARSE_LEGACY_VIDEO;
		break;

	case UFBXI_PARSE_LEGACY_VIDEO:
		return UFBXI_PARSE_VIDEO;

	case UFBXI_PARSE_LEGACY_MODEL:
		if (name == ufbxi_GeometryUVInfo) return UFBXI_PARSE_GEOMETRY_UV_INFO;
		if (name == ufbxi_Link) return UFBXI_PARSE_LEGACY_LINK;
		if (name == ufbxi_Channel) return UFBXI_PARSE_CHANNEL;
		if (name == ufbxi_Shape) return UFBXI_PARSE_SHAPE;
		break;

	case UFBXI_PARSE_POSE:
		if (name == ufbxi_PoseNode) return UFBXI_PARSE_POSE_NODE;
		break;

	case UFBXI_PARSE_TAKES:
		if (name == ufbxi_Take) return UFBXI_PARSE_TAKE;
		break;

	case UFBXI_PARSE_TAKE:
		return UFBXI_PARSE_TAKE_OBJECT;

	case UFBXI_PARSE_TAKE_OBJECT:
		if (name == ufbxi_Channel) return UFBXI_PARSE_CHANNEL;
		break;

	case UFBXI_PARSE_CHANNEL:
		if (name == ufbxi_Channel) return UFBXI_PARSE_CHANNEL;
		break;

	case UFBXI_PARSE_REFERENCES:
		return UFBXI_PARSE_REFERENCE;

	default:
		break;

	}

	return UFBXI_PARSE_UNKNOWN;
}

static bool ufbxi_is_array_node(ufbxi_context *uc, ufbxi_parse_state parent, const char *name, ufbxi_array_info *info)
{
	info->flags = 0;

	// Retain all arrays if user wants the DOM representation
	if (uc->opts.retain_dom) {
		info->flags |= UFBXI_ARRAY_FLAG_RESULT;
	}

	switch (parent) {

	case UFBXI_PARSE_THUMBNAIL:
		if (name == ufbxi_ImageData) {
			info->type = 'c';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		}
		break;

	case UFBXI_PARSE_GEOMETRY:
	case UFBXI_PARSE_MODEL:
		if (name == ufbxi_Vertices) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		} else if (name == ufbxi_PolygonVertexIndex) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_Edges) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			return true;
		} else if (name == ufbxi_Indexes) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_Points) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_KnotVector) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_KnotVectorU) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_KnotVectorV) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_PointsIndex) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_Normals) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		}
		break;

	case UFBXI_PARSE_LEGACY_MODEL:
		if (name == ufbxi_Vertices) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		} else if (name == ufbxi_Normals) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		} else if (name == ufbxi_Materials) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_PolygonVertexIndex) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_Children) {
			info->type = 's';
			return true;
		}
		break;

	case UFBXI_PARSE_ANIMATION_CURVE:
		if (name == ufbxi_KeyTime) {
			info->type = uc->opts.ignore_animation ? '-' : 'l';
			return true;
		} else if (name == ufbxi_KeyValueFloat) {
			info->type = uc->opts.ignore_animation ? '-' : 'r';
			return true;
		} else if (name == ufbxi_KeyAttrFlags) {
			info->type = uc->opts.ignore_animation ? '-' : 'i';
			return true;
		} else if (name == ufbxi_KeyAttrDataFloat) {
			// The float data in a keyframe attribute array is represented as integers
			// in versions >= 7200 as some of the elements aren't actually floats (!)
			info->type = uc->from_ascii && uc->version >= 7200 ? 'i' : 'f';
			if (uc->opts.ignore_animation) info->type = '-';
			if (uc->from_ascii && uc->version >= 7200) {
				info->flags |= UFBXI_ARRAY_FLAG_ACCURATE_F32;
			}
			return true;
		} else if (name == ufbxi_KeyAttrRefCount) {
			info->type = uc->opts.ignore_animation ? '-' : 'i';
			return true;
		}
		break;

	case UFBXI_PARSE_TEXTURE:
		if (!strcmp(name, "ModelUVTranslation") || !strcmp(name, "ModelUVScaling") || !strcmp(name, "Cropping")) {
			info->type = uc->opts.retain_dom ? 'r' : '-';
			return true;
		}
		break;

	case UFBXI_PARSE_VIDEO:
		if (name == ufbxi_Content) {
			info->type = uc->opts.ignore_embedded ? '-' : 'C';
			return true;
		}
		break;

	case UFBXI_PARSE_LAYERED_TEXTURE:
		if (name == ufbxi_BlendModes) {
			info->type = 'i';
			info->flags |= UFBXI_ARRAY_FLAG_TMP_BUF;
			return true;
		} else if (name == ufbxi_Alphas) {
			info->type = 'r';
			info->flags |= UFBXI_ARRAY_FLAG_TMP_BUF;
			return true;
		}
		break;

	case UFBXI_PARSE_SELECTION_NODE:
		if (name == ufbxi_VertexIndexArray) {
			info->type = 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_EdgeIndexArray) {
			info->type = 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_PolygonIndexArray) {
			info->type = 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		}
		break;

	case UFBXI_PARSE_LAYER_ELEMENT_NORMAL:
		if (name == ufbxi_Normals) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		} else if (name == ufbxi_NormalsIndex) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_NormalsW) {
			info->type = uc->retain_vertex_w ? 'r' : '-';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		}
		break;

	case UFBXI_PARSE_LAYER_ELEMENT_BINORMAL:
		if (name == ufbxi_Binormals) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		} else if (name == ufbxi_BinormalsIndex) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_BinormalsW) {
			info->type = uc->retain_vertex_w ? 'r' : '-';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		}
		break;

	case UFBXI_PARSE_LAYER_ELEMENT_TANGENT:
		if (name == ufbxi_Tangents) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		} else if (name == ufbxi_TangentsIndex) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_TangentsW) {
			info->type = uc->retain_vertex_w ? 'r' : '-';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		}
		break;

	case UFBXI_PARSE_LAYER_ELEMENT_UV:
		if (name == ufbxi_UV) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		} else if (name == ufbxi_UVIndex) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		}
		break;

	case UFBXI_PARSE_LAYER_ELEMENT_COLOR:
		if (name == ufbxi_Colors) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		} else if (name == ufbxi_ColorIndex) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		}
		break;

	case UFBXI_PARSE_LAYER_ELEMENT_VERTEX_CREASE:
		if (name == ufbxi_VertexCrease) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		} else if (name == ufbxi_VertexCreaseIndex) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		}
		break;

	case UFBXI_PARSE_LAYER_ELEMENT_EDGE_CREASE:
		if (name == ufbxi_EdgeCrease) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		}
		break;

	case UFBXI_PARSE_LAYER_ELEMENT_SMOOTHING:
		if (name == ufbxi_Smoothing) {
			info->type = uc->opts.ignore_geometry ? '-' : 'b';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		}
		break;

	case UFBXI_PARSE_LAYER_ELEMENT_VISIBILITY:
		if (name == ufbxi_Visibility) {
			info->type = uc->opts.ignore_geometry ? '-' : 'b';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		}
		break;

	case UFBXI_PARSE_LAYER_ELEMENT_POLYGON_GROUP:
		if (name == ufbxi_PolygonGroup) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		}
		break;

	case UFBXI_PARSE_LAYER_ELEMENT_HOLE:
		if (name == ufbxi_Hole) {
			info->type = uc->opts.ignore_geometry ? '-' : 'b';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		}
		break;

	case UFBXI_PARSE_LAYER_ELEMENT_MATERIAL:
		if (name == ufbxi_Materials) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		}
		break;

	case UFBXI_PARSE_LAYER_ELEMENT_OTHER:
		if (name == ufbxi_TextureId) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags |= UFBXI_ARRAY_FLAG_TMP_BUF;
			return true;
		} else if (name == ufbxi_UV) {
			info->type = uc->opts.retain_dom ? 'r' : '-';
			return true;
		} else if (name == ufbxi_UVIndex) {
			info->type = uc->opts.retain_dom ? 'i' : '-';
			return true;
		}
		break;

	case UFBXI_PARSE_GEOMETRY_UV_INFO:
		if (name == ufbxi_TextureUV) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		} else if (name == ufbxi_TextureUVVerticeIndex) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		}
		break;

	case UFBXI_PARSE_SHAPE:
		if (name == ufbxi_Indexes) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		}
		if (name == ufbxi_Vertices) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		}
		if (name == ufbxi_Normals) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN;
			return true;
		}
		break;

	case UFBXI_PARSE_DEFORMER:
		if (name == ufbxi_Transform) {
			info->type = 'r';
			return true;
		} else if (name == ufbxi_TransformLink) {
			info->type = 'r';
			return true;
		} else if (name == ufbxi_Indexes) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_Weights) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_BlendWeights) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_FullWeights) {
			// Ignore blend shape FullWeights as it's used in Blender for vertex groups
			// which we don't currently handle. https://developer.blender.org/T90382
			// TODO: Should we present this to users anyway somehow?
			info->type = 'r';
			if (!uc->opts.disable_quirks && uc->exporter == UFBX_EXPORTER_BLENDER_BINARY) {
				info->type = '-';
			}
			info->flags |= UFBXI_ARRAY_FLAG_TMP_BUF;
			return true;
		} else if (!strcmp(name, "TransformAssociateModel")) {
			info->type = uc->opts.retain_dom ? 'r' : '-';
			return true;
		}
		break;

	case UFBXI_PARSE_ASSOCIATE_MODEL:
		if (name == ufbxi_Transform) {
			info->type = uc->opts.retain_dom ? 'r' : '-';
			return true;
		}
		break;

	case UFBXI_PARSE_LEGACY_LINK:
		if (name == ufbxi_Transform) {
			info->type = 'r';
			return true;
		} else if (name == ufbxi_TransformLink) {
			info->type = 'r';
			return true;
		} else if (name == ufbxi_Indexes) {
			info->type = uc->opts.ignore_geometry ? '-' : 'i';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		} else if (name == ufbxi_Weights) {
			info->type = uc->opts.ignore_geometry ? '-' : 'r';
			info->flags = UFBXI_ARRAY_FLAG_RESULT;
			return true;
		}
		break;

	case UFBXI_PARSE_POSE_NODE:
		if (name == ufbxi_Matrix) {
			info->type = 'r';
			return true;
		}
		break;

	case UFBXI_PARSE_CHANNEL:
		if (name == ufbxi_Key) {
			info->type = uc->opts.ignore_animation ? '-' : 'd';
			return true;
		}
		break;

	case UFBXI_PARSE_AUDIO:
		if (name == ufbxi_Content) {
			info->type = uc->opts.ignore_embedded ? '-' : 'C';
			return true;
		}
		break;

	default:
		if (name == ufbxi_BinaryData) {
			info->type = uc->opts.ignore_embedded ? '-' : 'C';
			return true;
		}
		break;

	}

	return false;
}

static ufbxi_noinline bool ufbxi_is_raw_string(ufbxi_context *uc, ufbxi_parse_state parent, const char *name, size_t index)
{
	(void)index;

	switch (parent) {

	case UFBXI_PARSE_ROOT:
		if (name == ufbxi_Model) return true;
		if (!strcmp(name, "FileId")) return true;
		break;

	case UFBXI_PARSE_FBX_HEADER_EXTENSION:
		if (name == ufbxi_SceneInfo) return true;
		break;

	case UFBXI_PARSE_OBJECTS:
		return true;

	case UFBXI_PARSE_CONNECTIONS:
	case UFBXI_PARSE_RELATIONS:
		// Pre-7000 needs raw strings for "Name\x00\x01Type" pairs, post-7000 uses it only
		// for properties that are non-raw by default.
		return uc->version < 7000;

	case UFBXI_PARSE_MODEL:
		if (name == ufbxi_NodeAttributeName) return true;
		if (name == ufbxi_Name) return true;
		break;

	case UFBXI_PARSE_VIDEO:
		if (name == ufbxi_Content) return true;
		break;

	case UFBXI_PARSE_TEXTURE:
		if (!strcmp(name, "TextureName")) return true;
		if (name == ufbxi_Media) return true;
		break;

	case UFBXI_PARSE_GEOMETRY:
		if (name == ufbxi_NodeAttributeName) return true;
		if (name == ufbxi_Name) return true;
		break;

	case UFBXI_PARSE_NODE_ATTRIBUTE:
		if (name == ufbxi_NodeAttributeName) return true;
		if (name == ufbxi_Name) return true;
		break;

	case UFBXI_PARSE_POSE_NODE:
		if (name == ufbxi_Node) return true;
		break;

	case UFBXI_PARSE_SELECTION_NODE:
		if (name == ufbxi_Node) return true;
		break;

	case UFBXI_PARSE_UNKNOWN_OBJECT:
		if (name == ufbxi_NodeAttributeName) return true;
		if (name == ufbxi_Name) return true;
		break;

	case UFBXI_PARSE_COLLECTION:
		if (!strcmp(name, "Member")) return true;
		break;

	case UFBXI_PARSE_AUDIO:
		if (name == ufbxi_Content) return true;
		break;

	case UFBXI_PARSE_LEGACY_MODEL:
		if (name == ufbxi_Material) return true;
		if (name == ufbxi_Link) return true;
		if (name == ufbxi_Name) return true;
		break;

	case UFBXI_PARSE_LEGACY_SWITCHER:
		if (!strcmp(name, "CameraIndexName")) return true;
		break;

	case UFBXI_PARSE_LEGACY_SCENE_PERSISTENCE:
		if (name == ufbxi_SceneInfo) return true;
		break;

	case UFBXI_PARSE_REFERENCE:
		if (!strcmp(name, "Object")) return true;
		break;

	case UFBXI_PARSE_TAKE:
		if (name == ufbxi_Model) return true;
		break;

	default:
		break;

	}

	return false;
}

// -- Binary parsing

ufbxi_nodiscard static ufbxi_noinline char *ufbxi_swap_endian(ufbxi_context *uc, const void *src, size_t count, size_t elem_size)
{
	ufbxi_dev_assert(elem_size > 1);
	size_t total_size = count * elem_size;
	ufbxi_check_return(!ufbxi_does_overflow(total_size, count, elem_size), NULL);
	if (uc->swap_arr_size < total_size) {
		ufbxi_check_return(ufbxi_grow_array(&uc->ator_tmp, &uc->swap_arr, &uc->swap_arr_size, total_size), NULL);
	}
	char *dst = uc->swap_arr, *d = dst;

	const char *s = (const char*)src;
	switch (elem_size) {
	case 2:
		ufbxi_nounroll for (size_t i = 0; i < count; i++) {
			d[0] = s[1]; d[1] = s[0];
			d += 2; s += 2;
		}
		break;
	case 4:
		ufbxi_nounroll for (size_t i = 0; i < count; i++) {
			d[0] = s[3]; d[1] = s[2]; d[2] = s[1]; d[3] = s[0];
			d += 4; s += 4;
		}
		break;
	case 8:
		ufbxi_nounroll for (size_t i = 0; i < count; i++) {
			d[0] = s[7]; d[1] = s[6]; d[2] = s[5]; d[3] = s[4];
			d[4] = s[3]; d[5] = s[2]; d[6] = s[1]; d[7] = s[0];
			d += 8; s += 8;
		}
		break;
	default:
		ufbxi_unreachable("Bad endian swap size");
	}

	return dst;
}

// Swap the endianness of an array typed with a lowercase letter
ufbxi_nodiscard static ufbxi_noinline const char *ufbxi_swap_endian_array(ufbxi_context *uc, const void *src, size_t count, char type)
{
	switch (type) {
	case 'i': case 'f': return ufbxi_swap_endian(uc, src, count, 4);
	case 'l': case 'd': return ufbxi_swap_endian(uc, src, count, 8);
	default: return (const char*)src;
	}
}

// Swap the endianness of a single value (shallow, swaps string/array header words)
ufbxi_nodiscard static ufbxi_noinline const char *ufbxi_swap_endian_value(ufbxi_context *uc, const void *src, char type)
{
	switch (type) {
	case 'Y': return ufbxi_swap_endian(uc, src, 1, 2);
	case 'I': case 'F': return ufbxi_swap_endian(uc, src, 1, 4);
	case 'L': case 'D': return ufbxi_swap_endian(uc, src, 1, 8);
	case 'S': case 'R': return ufbxi_swap_endian(uc, src, 1, 4);
	case 'i': case 'l': case 'f': case 'd': case 'b': return ufbxi_swap_endian(uc, src, 3, 4);
	default: return (const char*)src;
	}
}

// Read and convert a post-7000 FBX data array into a different format. `src_type` may be equal to `dst_type`
// if the platform is not binary compatible with the FBX data representation.
ufbxi_nodiscard static ufbxi_noinline int ufbxi_binary_convert_array(ufbxi_context *maybe_uc, char src_type, char dst_type, const void *src, void *dst, size_t size)
{
	// TODO: We might want to use the slow path if the machine float/double doesn't match IEEE 754!
	// Convert commented out lines under some `#if UFBX_NON_IEE754` define or something.
	if (src_type == dst_type) {
		ufbx_assert(maybe_uc && maybe_uc->file_big_endian != maybe_uc->local_big_endian);
		src = ufbxi_swap_endian_array(maybe_uc, src, size, src_type);
		ufbxi_check_err(&maybe_uc->error, src);
		memcpy(dst, src, size * ufbxi_array_type_size(dst_type));
		return 1;
	}

	if (maybe_uc && maybe_uc->file_big_endian) {
		src = ufbxi_swap_endian_array(maybe_uc, src, size, src_type);
		ufbxi_check_err(&maybe_uc->error, src);
	}

	switch (dst_type)
	{

	#define ufbxi_convert_loop_fast(m_dst, m_cast, m_size, m_expr) do { \
		const char *val = (const char*)src, *val_end = val + size*m_size; \
		m_dst *d = (m_dst*)dst; \
		while (val != val_end) { *d++ = m_cast(m_expr); val += m_size; } \
	} while (0)

	#define ufbxi_convert_loop_slow(m_dst, m_cast, m_size, m_expr) do { \
		const char *val = (const char*)src, *val_end = val + size*m_size; \
		m_dst *d = (m_dst*)dst; \
		ufbxi_nounroll while (val != val_end) { *d++ = m_cast(m_expr); val += m_size; } \
	} while (0)

	case 'c':
		switch (src_type) {
		// case 'c': ufbxi_convert_loop_fast(char, (char), 1, *val != 0); break;
		case 'i': ufbxi_convert_loop_slow(uint8_t, (uint8_t), 4, (uint8_t)ufbxi_read_i32(val)); break;
		case 'l': ufbxi_convert_loop_slow(uint8_t, (uint8_t), 8, (uint8_t)ufbxi_read_i64(val)); break;
		case 'f': ufbxi_convert_loop_slow(uint8_t, (uint8_t), 4, (uint8_t)ufbxi_read_f32(val)); break;
		case 'd': ufbxi_convert_loop_slow(uint8_t, (uint8_t), 8, (uint8_t)ufbxi_read_f64(val)); break;
		default: if (maybe_uc) ufbxi_fail_err(&maybe_uc->error, "Bad array source type"); return 0;
		}
		break;

	case 'i':
		switch (src_type) {
		case 'c': ufbxi_convert_loop_slow(int32_t, (int32_t), 1, *val); break;
		// case 'i': ufbxi_convert_loop_slow(int32_t, (int32_t), 4, ufbxi_read_i32(val)); break;
		case 'l': ufbxi_convert_loop_slow(int32_t, (int32_t), 8, ufbxi_read_i64(val)); break;
		case 'f': ufbxi_convert_loop_slow(int32_t, ufbxi_f64_to_i32, 4, ufbxi_read_f32(val)); break;
		case 'd': ufbxi_convert_loop_slow(int32_t, ufbxi_f64_to_i32, 8, ufbxi_read_f64(val)); break;
		default: if (maybe_uc) ufbxi_fail_err(&maybe_uc->error, "Bad array source type"); return 0;
		}
		break;

	case 'l':
		switch (src_type) {
		case 'c': ufbxi_convert_loop_slow(int64_t, (int64_t), 1, *val); break;
		case 'i': ufbxi_convert_loop_slow(int64_t, (int64_t), 4, ufbxi_read_i32(val)); break;
		// case 'l': ufbxi_convert_loop_slow(int64_t, (int64_t), 8, ufbxi_read_i64(val)); break;
		case 'f': ufbxi_convert_loop_slow(int64_t, ufbxi_f64_to_i64, 4, ufbxi_read_f32(val)); break;
		case 'd': ufbxi_convert_loop_slow(int64_t, ufbxi_f64_to_i64, 8, ufbxi_read_f64(val)); break;
		default: if (maybe_uc) ufbxi_fail_err(&maybe_uc->error, "Bad array source type"); return 0;
		}
		break;

	case 'f':
		switch (src_type) {
		case 'c': ufbxi_convert_loop_slow(float, (float), 1, *val); break;
		case 'i': ufbxi_convert_loop_slow(float, (float), 4, ufbxi_read_i32(val)); break;
		case 'l': ufbxi_convert_loop_slow(float, (float), 8, ufbxi_read_i64(val)); break;
		// case 'f': ufbxi_convert_loop_slow(float, (float), 4, ufbxi_read_f32(val)); break;
		case 'd': ufbxi_convert_loop_fast(float, (float), 8, ufbxi_read_f64(val)); break;
		default: if (maybe_uc) ufbxi_fail_err(&maybe_uc->error, "Bad array source type"); return 0;
		}
		break;

	case 'd':
		switch (src_type) {
		case 'c': ufbxi_convert_loop_slow(double, (double), 1, *val); break;
		case 'i': ufbxi_convert_loop_slow(double, (double), 4, ufbxi_read_i32(val)); break;
		case 'l': ufbxi_convert_loop_slow(double, (double), 8, ufbxi_read_i64(val)); break;
		case 'f': ufbxi_convert_loop_fast(double, (double), 4, ufbxi_read_f32(val)); break;
		// case 'd': ufbxi_convert_loop_slow(double, (double), 8, ufbxi_read_f64(val)); break;
		default: if (maybe_uc) ufbxi_fail_err(&maybe_uc->error, "Bad array source type"); return 0;
		}
		break;

	default: return 0;

	}

	return 1;
}

// Read pre-7000 separate properties as an array.
ufbxi_nodiscard static ufbxi_noinline int ufbxi_binary_parse_multivalue_array(ufbxi_context *uc, char dst_type, void *dst, size_t size, ufbxi_buf *tmp_buf)
{
	if (size == 0) return 1;
	const char *val;
	size_t val_size;

	bool file_big_endian = uc->file_big_endian;

	#define ufbxi_convert_parse_fast(m_dst, m_type, m_expr) do { \
		m_dst *d = (m_dst*)dst; \
		for (; base < size; base++) { \
			val = ufbxi_peek_bytes(uc, 13); \
			ufbxi_check(val); \
			if (*val != m_type) break; \
			val++; \
			*d++ = (m_dst)(m_expr); \
			ufbxi_consume_bytes(uc, 1 + sizeof(m_dst)); \
		} \
	} while (0)

	// String array special case
	if (dst_type == 's' || dst_type == 'S' || dst_type == 'C') {
		bool raw = dst_type == 's';
		ufbx_string *d = (ufbx_string*)dst;
		for (size_t i = 0; i < size; i++) {
			val = ufbxi_peek_bytes(uc, 13);
			ufbxi_check(val);
			char type = *val++;
			ufbxi_check(type == 'S' || type == 'R');
			if (file_big_endian) {
				val = ufbxi_swap_endian_value(uc, val, type);
				ufbxi_check(val);
			}
			size_t len = ufbxi_read_u32(val);
			ufbxi_consume_bytes(uc, 5);
			d->data = ufbxi_read_bytes(uc, len);
			d->length = len;
			ufbxi_check(d->data);
			if (dst_type == 'C') {
				ufbxi_buf *buf = size == 1 || uc->opts.retain_dom ? &uc->result : tmp_buf;
				d->data = ufbxi_push_copy(buf, char, len, d->data);
				ufbxi_check(d->data);
			} else {
				ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, d, raw));
			}
			d++;
		}
		return 1;
	}

	// Optimize a couple of common cases
	size_t base = 0;
	if (!file_big_endian) {
		switch (dst_type) {
		case 'i': ufbxi_convert_parse_fast(int32_t, 'I', ufbxi_read_i32(val)); break;
		case 'l': ufbxi_convert_parse_fast(int64_t, 'L', ufbxi_read_i64(val)); break;
		case 'f': ufbxi_convert_parse_fast(float, 'F', ufbxi_read_f32(val)); break;
		case 'd': ufbxi_convert_parse_fast(double, 'D', ufbxi_read_f64(val)); break;
		default: break; // Fallthrough to rest
		}

		// Early return if we handled everything
		if (base == size) return 1;
	}

	switch (dst_type)
	{

	#define ufbxi_convert_parse(m_cast, m_size, m_expr) \
		*d++ = m_cast(m_expr); val_size = m_size + 1; \

	#define ufbxi_convert_parse_switch(m_dst, m_cast_int, m_cast_float) do { \
		m_dst *d = (m_dst*)dst + base; \
		for (size_t i = base; i < size; i++) { \
			val = ufbxi_peek_bytes(uc, 13); \
			ufbxi_check(val); \
			char type = *val++; \
			if (file_big_endian) { \
				val = ufbxi_swap_endian_value(uc, val, type); \
				ufbxi_check(val); \
			} \
			switch (type) { \
				case 'C': \
				case 'B': ufbxi_convert_parse(m_cast_int, 1, *val); break; \
				case 'Y': ufbxi_convert_parse(m_cast_int, 2, ufbxi_read_i16(val)); break; \
				case 'I': ufbxi_convert_parse(m_cast_int, 4, ufbxi_read_i32(val)); break; \
				case 'L': ufbxi_convert_parse(m_cast_int, 8, ufbxi_read_i64(val)); break; \
				case 'F': ufbxi_convert_parse(m_cast_float, 4, ufbxi_read_f32(val)); break; \
				case 'D': ufbxi_convert_parse(m_cast_float, 8, ufbxi_read_f64(val)); break; \
				default: ufbxi_fail("Bad multivalue array type"); \
			} \
			ufbxi_consume_bytes(uc, val_size); \
		} \
	} while (0)

	case 'c': ufbxi_convert_parse_switch(uint8_t, (uint8_t), (uint8_t)); break;
	case 'i': ufbxi_convert_parse_switch(int32_t, (int32_t), ufbxi_f64_to_i32); break;
	case 'l': ufbxi_convert_parse_switch(int64_t, (int64_t), ufbxi_f64_to_i64); break;
	case 'f': ufbxi_convert_parse_switch(float, (float), (float)); break;
	case 'd': ufbxi_convert_parse_switch(double, (double), (double)); break;

	default: return 0;

	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static void *ufbxi_push_array_data(ufbxi_context *uc, const ufbxi_array_info *info, size_t size, ufbxi_buf *tmp_buf)
{
	size_t elem_size = ufbxi_array_type_size(info->type);
	uint32_t flags = info->flags;
	if (flags & UFBXI_ARRAY_FLAG_PAD_BEGIN) size += 4;

	// The array may be pushed either to the result or temporary buffer depending
	// if it's already in the right format
	ufbxi_buf *arr_buf = tmp_buf;
	if (flags & UFBXI_ARRAY_FLAG_RESULT) arr_buf = &uc->result;
	else if (flags & UFBXI_ARRAY_FLAG_TMP_BUF) arr_buf = &uc->tmp;
	char *data = (char*)ufbxi_push_size(arr_buf, elem_size, size);
	ufbxi_check_return(data, NULL);

	if (flags & UFBXI_ARRAY_FLAG_PAD_BEGIN) {
		memset(data, 0, elem_size * 4);
		data += elem_size * 4;
	}

	return data;
}

ufbxi_noinline static void ufbxi_postprocess_bool_array(char *data, size_t size)
{
	ufbxi_for(char, b, (char*)data, size) {
		*b = (char)(*b != 0);
	}
}

typedef struct {
	size_t encoded_size;
	size_t src_elem_size;
	size_t array_size;
	char src_type;
	char dst_type;
	char arr_type;
	const void *encoded_data;
	void *decoded_data;
	void *dst_data;
	ufbx_inflate_retain *inflate_retain;
} ufbxi_deflate_task;

static bool ufbxi_deflate_task_fn(ufbxi_task *task)
{
	ufbxi_deflate_task *t = (ufbxi_deflate_task*)task->data;

	ufbx_inflate_input input; // ufbxi_uninit
	input.total_size = t->encoded_size;
	input.data = t->encoded_data;
	input.data_size = t->encoded_size;
	input.no_header = false;
	input.no_checksum = false;
	input.internal_fast_bits = 0;
	input.progress_cb.fn = NULL;
	input.progress_cb.user = NULL;
	input.progress_size_before = 0;
	input.progress_size_after = 0;
	input.progress_interval_hint = 0;
	input.buffer = NULL;
	input.buffer_size = 0;
	input.read_fn = NULL;
	input.read_user = NULL;

	size_t decoded_data_size = t->src_elem_size * t->array_size;
	ptrdiff_t res = ufbx_inflate(t->decoded_data, decoded_data_size, &input, t->inflate_retain);
	if (res == -28) {
		task->error = "Cancelled";
		return false;
	} else if (res != (ptrdiff_t)decoded_data_size) {
		task->error = "Bad DEFLATE data";
		return false;
	}

	if (t->decoded_data != t->dst_data) {
		int ok = ufbxi_binary_convert_array(NULL, t->src_type, t->dst_type, t->decoded_data, t->dst_data, t->array_size);
		if (!ok) {
			task->error = "Failed to convert array";
			return false;
		}
	}

	if (t->arr_type == 'b') {
		ufbxi_postprocess_bool_array((char*)t->dst_data, t->array_size);
	}

	return true;
}

// Recursion limited by check at the start
ufbxi_nodiscard ufbxi_noinline static int ufbxi_binary_parse_node(ufbxi_context *uc, uint32_t depth, ufbxi_parse_state parent_state, bool *p_end, ufbxi_buf *tmp_buf, bool recursive)
	ufbxi_recursive_function(int, ufbxi_binary_parse_node, (uc, depth, parent_state, p_end, tmp_buf, recursive), UFBXI_MAX_NODE_DEPTH + 1,
		(ufbxi_context *uc, uint32_t depth, ufbxi_parse_state parent_state, bool *p_end, ufbxi_buf *tmp_buf, bool recursive))
{
	// https://code.blender.org/2013/08/fbx-binary-file-format-specification
	// Parse an FBX document node in the binary format
	ufbxi_check(depth < UFBXI_MAX_NODE_DEPTH);

	// Parse the node header, post-7500 versions use 64-bit values for most
	// header fields.
	uint64_t end_offset, num_values64, values_len;
	uint8_t name_len;
	size_t header_size = (uc->version >= 7500) ? 25 : 13;
	const char *header = ufbxi_read_bytes(uc, header_size), *header_words = header;
	ufbxi_check(header);
	if (uc->version >= 7500) {
		if (uc->file_big_endian) {
			header_words = ufbxi_swap_endian(uc, header_words, 3, 8);
			ufbxi_check(header_words);
		}
		end_offset = ufbxi_read_u64(header_words + 0);
		num_values64 = ufbxi_read_u64(header_words + 8);
		values_len = ufbxi_read_u64(header_words + 16);
		name_len = ufbxi_read_u8(header + 24);
	} else {
		if (uc->file_big_endian) {
			header_words = ufbxi_swap_endian(uc, header_words, 3, 4);
			ufbxi_check(header_words);
		}
		end_offset = ufbxi_read_u32(header_words + 0);
		num_values64 = ufbxi_read_u32(header_words + 4);
		values_len = ufbxi_read_u32(header_words + 8);
		name_len = ufbxi_read_u8(header + 12);
	}

	ufbxi_check(num_values64 <= UINT32_MAX);
	uint32_t num_values = (uint32_t)num_values64;

	// If `end_offset` and `name_len` is zero we treat as the node as a NULL-sentinel
	// that terminates a node list.
	if (end_offset == 0 && name_len == 0) {
		*p_end = true;
		return 1;
	}

	// Update estimated end offset if possible
	if (end_offset > uc->progress_bytes_total) {
		uc->progress_bytes_total = end_offset;
	}

	// Push the parsed node into the `tmp_stack` buffer, the nodes will be popped by
	// calling code after its done parsing all of it's children.
	ufbxi_node *node = ufbxi_push_zero(&uc->tmp_stack, ufbxi_node, 1);
	ufbxi_check(node);

	// Parse and intern the name to the string pool.
	const char *name = ufbxi_read_bytes(uc, name_len);
	ufbxi_check(name);
	name = ufbxi_push_string(&uc->string_pool, name, name_len, NULL, true);
	ufbxi_check(name);
	node->name_len = name_len;
	node->name = name;

	uint64_t values_end_offset = ufbxi_get_read_offset(uc) + values_len;

	// Check if the values of the node we're parsing currently should be
	// treated as an array.
	ufbxi_array_info arr_info;
	if (ufbxi_is_array_node(uc, parent_state, name, &arr_info)) {

		// Normalize the array type (eg. 'r' to 'f'/'d' depending on the build)
		// and get the per-element size of the array.
		// Boolean arrays 'b' are normalized to 'c' as they are postprocessed
		// below based on `arr_info.type`.
		char dst_type = ufbxi_normalize_array_type(arr_info.type, 'c');

		ufbxi_value_array *arr = ufbxi_push(tmp_buf, ufbxi_value_array, 1);
		ufbxi_check(arr);

		node->value_type_mask = UFBXI_VALUE_ARRAY;
		node->array = arr;
		arr->type = ufbxi_normalize_array_type(arr_info.type, 'b');

		// Peek the first bytes of the array. We can always look at least 13 bytes
		// ahead safely as valid FBX files must end in a 13/25 byte NULL record.
		const char *data = ufbxi_peek_bytes(uc, 13);
		ufbxi_check(data);

		// Check if the data type is one of the explicit array types (post-7000).
		// Otherwise we form the array by concatenating all the normal values of the
		// node (pre-7000)
		char c = data[0];

		// HACK: Override the "type" if either the array is empty or we want to
		// specifically ignore the contents.
		if (num_values == 0) c = '0';
		if (dst_type == '-') c = '-';

		bool deferred = false;

		if (c=='c' || c=='b' || c=='i' || c=='l' || c =='f' || c=='d') {

			const char *arr_words = data + 1;
			if (uc->file_big_endian) {
				arr_words = ufbxi_swap_endian(uc, arr_words, 3, 4);
				ufbxi_check(arr_words);
			}

			// Parse the array header from the prefix we already peeked above.
			char src_type = data[0];
			uint32_t size = ufbxi_read_u32(arr_words + 0);
			uint32_t encoding = ufbxi_read_u32(arr_words + 4);
			uint32_t encoded_size = ufbxi_read_u32(arr_words + 8);
			ufbxi_consume_bytes(uc, 13);

			// Normalize the source type as well, but don't convert UFBX-specific
			// 'r' to 'f'/'d', but fail later instead.
			if (src_type != 'r') src_type = ufbxi_normalize_array_type(src_type, 'c');
			size_t src_elem_size = ufbxi_array_type_size(src_type);
			size_t decoded_data_size = src_elem_size * size;

			// Allocate `size` elements for the array.
			char *arr_data = (char*)ufbxi_push_array_data(uc, &arr_info, size, tmp_buf);
			ufbxi_check(arr_data);

			uint64_t arr_begin = ufbxi_get_read_offset(uc);
			ufbxi_check(UINT64_MAX - encoded_size > arr_begin);
			uint64_t arr_end = arr_begin + encoded_size;
			if (arr_end > uc->progress_bytes_total) {
				uc->progress_bytes_total = arr_end;
			}

			// Threading
			if (uc->parse_threaded && encoding == 1 && encoded_size >= UFBXI_MIN_THREADED_DEFLATE_BYTES && !uc->file_big_endian && !uc->local_big_endian) {
				ufbxi_task *task = ufbxi_thread_pool_create_task(&uc->thread_pool, &ufbxi_deflate_task_fn);
				if (task) {
					ufbxi_deflate_task *t = ufbxi_push_zero(tmp_buf, ufbxi_deflate_task, 1);
					ufbxi_check(t);

					ufbxi_inflate_init_retain(uc->inflate_retain);

					t->src_elem_size = src_elem_size;
					t->encoded_size = encoded_size;
					t->array_size = size;
					t->src_type = src_type;
					t->dst_type = dst_type;
					t->arr_type = arr->type;
					t->dst_data = arr_data;
					t->inflate_retain = uc->inflate_retain;

					if (!uc->read_fn) {
						// From memory, no need to copy
						t->encoded_data = uc->data;
					} else {
						void *encoded_data = ufbxi_push(tmp_buf, char, encoded_size);
						ufbxi_check(encoded_data);
						ufbxi_check(ufbxi_read_to(uc, encoded_data, encoded_size));
						t->encoded_data = encoded_data;
					}

					if (src_type != dst_type) {
						t->decoded_data = ufbxi_push_size(tmp_buf, src_elem_size, size);
						ufbxi_check(t->decoded_data);
					} else {
						t->decoded_data = arr_data;
					}

					task->data = t;
					ufbxi_thread_pool_run_task(&uc->thread_pool, task);
					deferred = true;
				}
			}

			// If the source and destination types are equal and our build is binary-compatible
			// with the FBX format we can read the decoded data directly into the array buffer.
			// Otherwise we need a temporary buffer to decode the array into before conversion.
			void *decoded_data = arr_data;
			if (!deferred && (src_type != dst_type || uc->local_big_endian != uc->file_big_endian)) {
				ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, decoded_data_size));
				decoded_data = uc->tmp_arr;
			}

			if (deferred) {
				// Nop
			} else if (encoding == 0) {
				// Encoding 0: Plain binary data.
				ufbxi_check(encoded_size == decoded_data_size);

				// If the array is contained in the current read buffer and we need to convert
				// the data anyway we can use the read buffer as the decoded array source, otherwise
				// do a plain byte copy to the array/conversion buffer.
				if (uc->yield_size + uc->data_size >= encoded_size && decoded_data != arr_data) {
					// Yield right after this if we crossed the yield threshold
					if (encoded_size > uc->yield_size) {
						uc->data_size += uc->yield_size;
						uc->yield_size = encoded_size;
						uc->data_size -= uc->yield_size;
					}

					decoded_data = (void*)uc->data;
					ufbxi_consume_bytes(uc, encoded_size);
				} else {
					ufbxi_check(ufbxi_read_to(uc, decoded_data, encoded_size));
				}
			} else if (encoding == 1) {
				// Encoding 1: DEFLATE

				ufbxi_pause_progress(uc);

				// Inflate the data from the user-provided IO buffer / read callbacks
				ufbx_inflate_input input;
				input.total_size = encoded_size;
				input.data = uc->data;
				input.data_size = uc->data_size;
				input.no_header = false;
				input.no_checksum = false;
				input.internal_fast_bits = 0;

				if (uc->opts.progress_cb.fn) {
					input.progress_cb = uc->opts.progress_cb;
					input.progress_size_before = arr_begin;
					input.progress_size_after = uc->progress_bytes_total - arr_end;
					input.progress_interval_hint = uc->progress_interval;
				} else {
					input.progress_cb.fn = NULL;
					input.progress_cb.user = NULL;
					input.progress_size_before = 0;
					input.progress_size_after = 0;
					input.progress_interval_hint = 0;
				}

				// If the encoded array is larger than the data we have currently buffered
				// we need to allow `ufbx_inflate()` to read from the IO callback. We can
				// let `ufbx_inflate()` freely clobber our `read_buffer` as all the data
				// in the buffer will be consumed. `ufbx_inflate()` always reads exactly
				// the amount of bytes needed so we can continue reading from `read_fn` as
				// usual (given that we clear the `uc->data/_size` buffer below).
				// NOTE: We _cannot_ share `read_buffer` if we plan to read later from it
				// as `ufbx_inflate()` overwrites parts of it with zeroes.
				if (encoded_size > input.data_size) {
					input.buffer = uc->read_buffer;
					input.buffer_size = uc->read_buffer_size;
					input.read_fn = uc->read_fn;
					input.read_user = uc->read_user;
					uc->data_offset += encoded_size - input.data_size;
					uc->data += input.data_size;
					uc->data_size = 0;
				} else {
					input.buffer = NULL;
					input.buffer_size = 0;
					input.read_fn = NULL;
					input.read_user = NULL;
					uc->data += encoded_size;
					uc->data_size -= encoded_size;
					ufbxi_check(ufbxi_resume_progress(uc));
				}

				ptrdiff_t res = ufbx_inflate(decoded_data, decoded_data_size, &input, uc->inflate_retain);
				ufbxi_check_msg(res != -28, "Cancelled");
				ufbxi_check_msg(res == (ptrdiff_t)decoded_data_size, "Bad DEFLATE data");

			} else {
				ufbxi_fail("Bad array encoding");
			}

			// Convert the decoded array if necessary.
			if (!deferred && decoded_data != arr_data) {
				ufbxi_check(ufbxi_binary_convert_array(uc, src_type, dst_type, decoded_data, arr_data, size));
			}

			arr->data = arr_data;
			arr->size = size;

		} else if (c == '0' || c == '-') {
			// Ignore the array
			arr->type = c == '-' ? '-' : dst_type;
			arr->data = (char*)ufbxi_zero_size_buffer + 32;
			arr->size = 0;
		} else {
			// Allocate `num_values` elements for the array and parse single values into it.
			char *arr_data = (char*)ufbxi_push_array_data(uc, &arr_info, num_values, tmp_buf);
			ufbxi_check(arr_data);
			ufbxi_check(ufbxi_binary_parse_multivalue_array(uc, dst_type, arr_data, num_values, tmp_buf));
			arr->data = arr_data;
			arr->size = num_values;
		}

		// Post-process boolean arrays
		if (!deferred && arr_info.type == 'b') {
			ufbxi_postprocess_bool_array((char*)arr->data, arr->size);
		}

	} else {
		// Parse up to UFBXI_MAX_NON_ARRAY_VALUES as plain values
		num_values = ufbxi_min32(num_values, UFBXI_MAX_NON_ARRAY_VALUES);
		ufbxi_value *vals = ufbxi_push(tmp_buf, ufbxi_value, num_values);
		ufbxi_check(vals);
		node->vals = vals;

		uint32_t type_mask = 0;
		for (size_t i = 0; i < (size_t)num_values; i++) {
			// The file must end in a 13/25 byte NULL record, so we can peek
			// up to 13 bytes safely here.
			const char *data = ufbxi_peek_bytes(uc, 13);
			ufbxi_check(data);

			const char *value = data + 1;

			char type = data[0];
			if (uc->file_big_endian) {
				value = ufbxi_swap_endian_value(uc, value, type);
				ufbxi_check(value);
			}

			switch (type) {

			case 'C': case 'B': case 'Z':
				type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (i*2);
				vals[i].f = (double)(vals[i].i = (int64_t)(uint8_t)value[0]);
				ufbxi_consume_bytes(uc, 2);
				break;

			case 'Y':
				type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (i*2);
				vals[i].f = (double)(vals[i].i = ufbxi_read_i16(value));
				ufbxi_consume_bytes(uc, 3);
				break;

			case 'I':
				type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (i*2);
				vals[i].f = (double)(vals[i].i = ufbxi_read_i32(value));
				ufbxi_consume_bytes(uc, 5);
				break;

			case 'L':
				type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (i*2);
				vals[i].f = (double)(vals[i].i = ufbxi_read_i64(value));
				ufbxi_consume_bytes(uc, 9);
				break;

			case 'F':
				type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (i*2);
				vals[i].i = ufbxi_f64_to_i64(vals[i].f = ufbxi_read_f32(value));
				ufbxi_consume_bytes(uc, 5);
				break;

			case 'D':
				type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (i*2);
				vals[i].i = ufbxi_f64_to_i64(vals[i].f = ufbxi_read_f64(value));
				ufbxi_consume_bytes(uc, 9);
				break;

			case 'S': case 'R':
			{
				uint32_t length = ufbxi_read_u32(value);
				ufbxi_consume_bytes(uc, 5);
				const char *str = ufbxi_read_bytes(uc, length);
				ufbxi_check(str);

				if (length == 0) {
					vals[i].s.raw_data = ufbxi_empty_char;
					vals[i].s.raw_length = 0;
					vals[i].s.utf8_length = 0;
				} else {
					bool non_ascii = false;
					uint32_t hash = ufbxi_hash_string_check_ascii(str, length, &non_ascii);
					bool raw = !non_ascii || ufbxi_is_raw_string(uc, parent_state, name, i);
					ufbxi_check(ufbxi_push_sanitized_string(&uc->string_pool, &vals[i].s, str, length, hash, raw));

					// Mark the data as invalid UTF-8
					if (non_ascii && raw) vals[i].s.utf8_length = UINT32_MAX;
				}

				type_mask |= (uint32_t)UFBXI_VALUE_STRING << (i*2);
			}
			break;

			// Treat arrays as non-values and skip them
			case 'c': case 'b': case 'i': case 'l': case 'f': case 'd':
			{
				uint32_t encoded_size = ufbxi_read_u32(value + 8);
				ufbxi_consume_bytes(uc, 13);
				ufbxi_check(ufbxi_skip_bytes(uc, encoded_size));
			}
			break;

			default:
				ufbxi_fail("Bad value type");

			}
		}

		node->value_type_mask = (uint16_t)type_mask;
	}

	// Skip over remaining values if necessary if we for example truncated
	// the list of values or if there are values after an array
	uint64_t offset = ufbxi_get_read_offset(uc);
	ufbxi_check(offset <= values_end_offset);
	if (offset < values_end_offset) {
		ufbxi_check(ufbxi_skip_bytes(uc, values_end_offset - offset));
	}

	if (recursive) {
		// Recursively parse the children of this node. Update the parse state
		// to provide context for child node parsing.
		ufbxi_parse_state parse_state = ufbxi_update_parse_state(parent_state, node->name);
		uint32_t num_children = 0;
		for (;;) {
			// Stop at end offset
			uint64_t current_offset = ufbxi_get_read_offset(uc);
			if (current_offset >= end_offset) {
				ufbxi_check(current_offset == end_offset || end_offset == 0);
				break;
			}

			bool end = false;
			ufbxi_check(ufbxi_binary_parse_node(uc, depth + 1, parse_state, &end, tmp_buf, true));
			if (end) break;
			num_children++;
		}

		// Pop children from `tmp_stack` to a contiguous array
		node->num_children = num_children;
		if (num_children > 0) {
			node->children = ufbxi_push_pop(tmp_buf, &uc->tmp_stack, ufbxi_node, num_children);
			ufbxi_check(node->children);
		}
	} else {
		uint64_t current_offset = ufbxi_get_read_offset(uc);
		uc->has_next_child = (current_offset < end_offset);
	}

	return 1;
}

#define UFBXI_BINARY_MAGIC_SIZE 22
#define UFBXI_BINARY_HEADER_SIZE 27
static const char ufbxi_binary_magic[] = "Kaydara FBX Binary  \x00\x1a";

// -- ASCII parsing

#define UFBXI_ASCII_END '\0'
#define UFBXI_ASCII_NAME 'N'
#define UFBXI_ASCII_BARE_WORD 'B'
#define UFBXI_ASCII_INT 'I'
#define UFBXI_ASCII_FLOAT 'F'
#define UFBXI_ASCII_STRING 'S'

static ufbxi_noinline char ufbxi_ascii_refill(ufbxi_context *uc)
{
	ufbxi_ascii *ua = &uc->ascii;
	uc->data_offset += ufbxi_to_size(ua->src - uc->data_begin);
	if (uc->read_fn) {
		char *dst_buffer = NULL;
		size_t dst_size = 0;

		if (ua->retain_buf != NULL) {
			dst_size = uc->opts.read_buffer_size;
			dst_buffer = ufbxi_push(ua->retain_buf, char, dst_size);
			ufbxi_check_return(dst_buffer, '\0');
			ua->src_is_retained = true;
			ua->src_buf = ua->retain_buf;
		} else {
			// Grow the read buffer if necessary
			if (uc->read_buffer_size < uc->opts.read_buffer_size) {
				size_t new_size = uc->opts.read_buffer_size;
				ufbxi_check_return(ufbxi_grow_array(&uc->ator_tmp, &uc->read_buffer, &uc->read_buffer_size, new_size), '\0');
			}
			dst_buffer = uc->read_buffer;
			dst_size = uc->read_buffer_size;
			ua->src_is_retained = false;
			ua->src_buf = NULL;
		}

		// Read user data, return '\0' on EOF
		// TODO: Very unoptimal for non-full-size reads in some cases
		size_t num_read = uc->read_fn(uc->read_user, dst_buffer, dst_size);
		ufbxi_check_return_msg(num_read != SIZE_MAX, '\0', "IO error");
		ufbxi_check_return(num_read <= uc->read_buffer_size, '\0');
		if (num_read == 0) return '\0';

		uc->data = uc->data_begin = ua->src = dst_buffer;
		ua->src_end = dst_buffer + num_read;
		return *ua->src;
	} else {
		// If the user didn't specify a `read_fn()` treat anything
		// past the initial data buffer as EOF.
		uc->data = uc->data_begin = ua->src = "";
		ua->src_end = ua->src + 1;
		return '\0';
	}
}

static ufbxi_noinline char ufbxi_ascii_yield(ufbxi_context *uc)
{
	ufbxi_ascii *ua = &uc->ascii;

	char ret;
	if (ua->src == ua->src_end) {
		ret = ufbxi_ascii_refill(uc);
	} else {
		ret = *ua->src;
	}

	if (ufbxi_to_size(ua->src_end - ua->src) < uc->progress_interval) {
		ua->src_yield = ua->src_end;
	} else {
		ua->src_yield = ua->src + uc->progress_interval;
	}

	// TODO: Unify these properly
	uc->data = ua->src;
	ufbxi_check_return(ufbxi_report_progress(uc), '\0');
	return ret;
}

static ufbxi_forceinline char ufbxi_ascii_peek(ufbxi_context *uc)
{
	ufbxi_ascii *ua = &uc->ascii;
	if (ua->src == ua->src_yield) return ufbxi_ascii_yield(uc);
	return *ua->src;
}

static ufbxi_forceinline char ufbxi_ascii_next(ufbxi_context *uc)
{
	ufbxi_ascii *ua = &uc->ascii;
	if (ua->src == ua->src_yield) return ufbxi_ascii_yield(uc);
	ua->src++;
	if (ua->src == ua->src_yield) return ufbxi_ascii_yield(uc);
	return *ua->src;
}

static ufbxi_noinline uint32_t ufbxi_ascii_parse_version(ufbxi_context *uc)
{
	uint8_t digits[3];
	uint32_t num_digits = 0;

	char c = ufbxi_ascii_next(uc);

	const char fmt[] = " FBX ?.?.?";
	uint32_t ix = 0;
	while (num_digits < 3) {
		char ref = fmt[ix++];
		switch (ref) {

		// Digit
		case '?':
			if (c < '0' || c > '9') return 0;
			digits[num_digits++] = (uint8_t)(c - '0');
			c = ufbxi_ascii_next(uc);
			break;

		// Whitespace
		case ' ':
			while (c == ' ' || c == '\t') {
				c = ufbxi_ascii_next(uc);
			}
			break;

		// Literal character
		default:
			if (c != ref) return 0;
			c = ufbxi_ascii_next(uc);
			break;
		}
	}

	if (num_digits != 3) return 0;
	return 1000u*(uint32_t)digits[0] + 100u*(uint32_t)digits[1] + 10u*(uint32_t)digits[2];
}

static const uint32_t ufbxi_space_mask =
	(1u << ((uint32_t)' '  - 1)) |
	(1u << ((uint32_t)'\t' - 1)) |
	(1u << ((uint32_t)'\r' - 1)) |
	(1u << ((uint32_t)'\n' - 1)) ;

ufbx_static_assert(space_codepoint,
	(uint32_t)' '  <= 32u && (uint32_t)'\t' <= 32u &&
	(uint32_t)'\r' <= 32u && (uint32_t)'\n' <= 32u);

static ufbxi_forceinline bool ufbxi_is_space(char c)
{
	uint32_t v = (uint32_t)(uint8_t)c - 1;
	return v < 32 && ((ufbxi_space_mask >> v) & 0x1) != 0;
}

static ufbxi_noinline char ufbxi_ascii_skip_whitespace(ufbxi_context *uc)
{
	ufbxi_ascii *ua = &uc->ascii;

	// Ignore whitespace
	char c = ufbxi_ascii_peek(uc);
	for (;;) {
		while (ufbxi_is_space(c)) {
			c = ufbxi_ascii_next(uc);
		}

		// Line comment
		if (c == ';') {

			bool read_magic = false;
			// FBX ASCII files begin with a magic comment of form "; FBX 7.7.0 project file"
			// Try to extract the version number from the magic comment
			if (!ua->read_first_comment) {
				ua->read_first_comment = true;
				uint32_t version = ufbxi_ascii_parse_version(uc);
				if (version) {
					uc->version = version;
					ua->found_version = true;
					read_magic = true;
				}
			}

			c = ufbxi_ascii_next(uc);
			while (c != '\n' && c != '\0') {
				c = ufbxi_ascii_next(uc);
			}
			c = ufbxi_ascii_next(uc);

			// Try to determine if this is a Blender 6100 ASCII file
			if (read_magic) {
				if (c == ';') {
					char line[32];
					size_t line_len = 0;

					c = ufbxi_ascii_next(uc);
					while (c != '\n' && c != '\0') {
						if (line_len < sizeof(line)) {
							line[line_len++] = c;
						}
						c = ufbxi_ascii_next(uc);
					}

					if (line_len >= 19 && !memcmp(line, " Created by Blender", 19)) {
						uc->exporter = UFBX_EXPORTER_BLENDER_ASCII;
					}
				}
			}

		} else {
			break;
		}
	}
	return c;
}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_ascii_push_token_char(ufbxi_context *uc, ufbxi_ascii_token *token, char c)
{
	// Grow the string data buffer if necessary
	if (token->str_len == token->str_cap) {
		size_t len = ufbxi_max_sz(token->str_len + 1, 256);
		ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &token->str_data, &token->str_cap, len));
	}

	token->str_data[token->str_len++] = c;

	return 1;
}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_ascii_push_token_string(ufbxi_context *uc, ufbxi_ascii_token *token, const char *data, size_t length)
{
	// Grow the string data buffer if necessary
	if (token->str_len + length >= token->str_cap) {
		size_t len = ufbxi_max_sz(token->str_len + length, 256);
		ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &token->str_data, &token->str_cap, len));
	}

	memcpy(token->str_data + token->str_len, data, length);
	token->str_len += length;

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_skip_until(ufbxi_context *uc, char dst)
{
	ufbxi_ascii *ua = &uc->ascii;

	for (;;) {
		size_t buffered = ufbxi_to_size(ua->src_yield - ua->src);
		const char *match = (const char*)memchr(ua->src, dst, buffered);
		if (match) {
			ua->src = match;
			break;
		} else {
			ua->src += buffered;
		}
		if (buffered == 0) {
			char c = ufbxi_ascii_yield(uc);
			ufbxi_check(c != '\0');
		}
	}

	return 1;
}

typedef struct {
	const char *source;
	size_t length;
} ufbxi_ascii_span;

ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_store_array(ufbxi_context *uc, ufbxi_buf *tmp_buf)
{
	ufbxi_ascii *ua = &uc->ascii;

	ua->retain_buf = tmp_buf;

	for (;;) {
		size_t buffered = ufbxi_to_size(ua->src_yield - ua->src);
		if (buffered == 0) {
			char c = ufbxi_ascii_yield(uc);
			ufbxi_check(c != '\0');
			continue;
		}

		const char *begin = ua->src, *end;
		const char *match = (const char*)memchr(begin, '}', buffered);
		if (match) {
			end = match;
		} else {
			end = begin + buffered;
		}
		ua->src = end;

		size_t length = ufbxi_to_size(end - begin);
		ufbxi_ascii_span *span = ufbxi_push(&uc->tmp_ascii_spans, ufbxi_ascii_span, 1);
		ufbxi_check(span);
		// Store the trailing '}' for parsing
		if (match) length += 1;
		span->length = length;
		if (ua->src_is_retained || !uc->read_fn) {
			span->source = begin;
		} else {
			span->source = ufbxi_push_copy(tmp_buf, char, length, begin);
			ufbxi_check(span->source);
		}

		if (match) break;
	}

	ua->retain_buf = NULL;

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_try_ignore_string(ufbxi_context *uc, ufbxi_ascii_token *token)
{
	ufbxi_ascii *ua = &uc->ascii;

	char c = ufbxi_ascii_skip_whitespace(uc);
	token->str_len = 0;

	if (c == '"') {
		// Replace `prev_token` with `token` but swap the buffers so `token` uses
		// the now-unused string buffer of the old `prev_token`.
		char *swap_data = ua->prev_token.str_data;
		size_t swap_cap = ua->prev_token.str_cap;
		ua->prev_token = ua->token;
		ua->token.str_data = swap_data;
		ua->token.str_cap = swap_cap;

		token->type = UFBXI_ASCII_STRING;
		// Skip opening quote
		ufbxi_ascii_next(uc);
		ufbxi_check(ufbxi_ascii_skip_until(uc, '"'));
		// Skip closing quote
		ufbxi_ascii_next(uc);
		return true;
	}

	return false;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_next_token(ufbxi_context *uc, ufbxi_ascii_token *token)
{
	ufbxi_ascii *ua = &uc->ascii;

	// Replace `prev_token` with `token` but swap the buffers so `token` uses
	// the now-unused string buffer of the old `prev_token`.
	char *swap_data = ua->prev_token.str_data;
	size_t swap_cap = ua->prev_token.str_cap;
	ua->prev_token = ua->token;
	ua->token.str_data = swap_data;
	ua->token.str_cap = swap_cap;

	char c = ufbxi_ascii_skip_whitespace(uc);
	token->str_len = 0;

	if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_') {
		token->type = UFBXI_ASCII_BARE_WORD;
		while ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
			|| (c >= '0' && c <= '9') || c == '_') {
			ufbxi_check(ufbxi_ascii_push_token_char(uc, token, c));
			c = ufbxi_ascii_next(uc);
		}

		// Skip whitespace to find if there's a following ':'
		c = ufbxi_ascii_skip_whitespace(uc);
		if (c == ':') {
			token->value.name_len = token->str_len;
			token->type = UFBXI_ASCII_NAME;
			ufbxi_ascii_next(uc);
		}
	} else if ((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.') {
		token->type = UFBXI_ASCII_INT;

		token->negative = c == '-';
		while ((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E') {
			if (c == '.' || c == 'e' || c == 'E') {
				token->type = UFBXI_ASCII_FLOAT;
			}
			ufbxi_check(ufbxi_ascii_push_token_char(uc, token, c));
			c = ufbxi_ascii_next(uc);
		}

		if (c == '#') {
			ufbxi_check(token->type == UFBXI_ASCII_FLOAT);
			ufbxi_check(ufbxi_ascii_push_token_char(uc, token, c));
			c = ufbxi_ascii_next(uc);

			bool is_inf = c == 'I' || c == 'i';
			while ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
				ufbxi_check(ufbxi_ascii_push_token_char(uc, token, c));
				c = ufbxi_ascii_next(uc);
			}
			ufbxi_check(ufbxi_ascii_push_token_char(uc, token, '\0'));

			if (is_inf) {
				token->value.f64 = token->str_data[0] == '-' ? -UFBX_INFINITY : UFBX_INFINITY;
			} else {
				token->value.f64 = UFBX_NAN;
			}

		} else {
			ufbxi_check(ufbxi_ascii_push_token_char(uc, token, '\0'));

			char *end;
			if (token->type == UFBXI_ASCII_INT) {
				token->value.i64 = ufbxi_parse_int64(token->str_data, &end);
				ufbxi_check(end == token->str_data + token->str_len - 1);
			} else if (token->type == UFBXI_ASCII_FLOAT) {
				uint32_t flags = uc->double_parse_flags;
				if (ua->parse_as_f32) flags = UFBXI_PARSE_DOUBLE_AS_BINARY32;
				token->value.f64 = ufbxi_parse_double(token->str_data, token->str_len, &end, flags);
				ufbxi_check(end == token->str_data + token->str_len - 1);
			}
		}
	} else if (c == '"') {
		token->type = UFBXI_ASCII_STRING;
		c = ufbxi_ascii_next(uc);
		while (c != '"') {

			// Optimized string parsing for non-special characters
			if (ua->src + 1 < ua->src_yield) {
				const char *begin = ua->src;
				const char *end = ua->src_yield;
				const char *quot = (const char*)memchr(begin, '"', ufbxi_to_size(end - begin));
				if (quot) end = quot;
				const char *esc = (const char*)memchr(begin, '&', ufbxi_to_size(end - begin));
				if (esc) end = esc;

				if (begin < end) {
					ufbxi_check(ufbxi_ascii_push_token_string(uc, token, begin, ufbxi_to_size(end - begin)));
					ua->src = end;
					c = ufbxi_ascii_peek(uc);
					continue;
				}
			}

			// Escape XML-like elements, funny enough there is no way to escape '&' itself, there is no `&amp`.
			// '&quot;' -> '"'
			// '&cr;' -> '\r'
			// '&lf;' -> '\n'
			if (c == '&') {
				const char *entity = NULL;
				char replacement = '\0';

				c = ufbxi_ascii_next(uc);
				switch (c) {
				case 'q':
					entity = "&quot;";
					replacement = '"';
					break;
				case 'c':
					entity = "&cr;";
					replacement = '\r';
					break;
				case 'l':
					entity = "&lf;";
					replacement = '\n';
					break;
				default:
					// As '&' is not escaped in any way just map '&' -> '&'
					entity = "&";
					replacement = '&';
					break;
				}

				size_t step = 1;

				ufbxi_dev_assert(entity && *entity);
				// `entity` is a NULL terminated string longer than a single character
				// cppcheck-suppress arrayIndexOutOfBounds
				for (; entity[step]; step++) {
					if (c != entity[step]) break;
					c = ufbxi_ascii_next(uc);
				}

				if (entity[step] == '\0') {
					// Full match: Push the replacement character
					ufbxi_check(ufbxi_ascii_push_token_char(uc, token, replacement));
				} else {
					// Partial match: Push the prefix we have skipped already
					for (size_t i = 0; i < step; i++) {
						ufbxi_check(ufbxi_ascii_push_token_char(uc, token, entity[i]));
					}
				}
				continue;
			}

			ufbxi_check(c != '\0');
			ufbxi_check(ufbxi_ascii_push_token_char(uc, token, c));
			c = ufbxi_ascii_next(uc);
		}
		// Skip closing quote
		ufbxi_ascii_next(uc);
	} else {
		// Single character token
		token->type = c;
		ufbxi_ascii_next(uc);
	}

	return 1;
}

ufbxi_nodiscard static int ufbxi_ascii_accept(ufbxi_context *uc, char type)
{
	ufbxi_ascii *ua = &uc->ascii;

	if (ua->token.type == type) {
		ufbxi_check(ufbxi_ascii_next_token(uc, &ua->token));
		return 1;
	} else {
		return 0;
	}
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_ascii_read_int_array(ufbxi_context *uc, char type, size_t *p_num_read)
{
	ufbxi_ascii *ua = &uc->ascii;
	if (ua->parse_as_f32) return 1;
	size_t initial_items = uc->tmp_stack.num_items;

	int64_t val;
	if (ua->token.type == UFBXI_ASCII_INT) {
		val = ua->token.value.i64;
	} else {
		return 1;
	}

	const char *src = ua->src;
	const char *end = ua->src_yield;
	const char *src_scan = src;

	for (;;) {

		// Skip '\s*,\s*' between array elements. If we don't find a comma after an element
		// don't push it as we can't be 100% certain whether it's a part of the array.
		while (src_scan != end && ufbxi_is_space(*src_scan)) src_scan++;
		if (src_scan == end || *src_scan != ',') break;
		src_scan++;
		while (src_scan != end && ufbxi_is_space(*src_scan)) src_scan++;

		// Found comma, commit to the position and push the previous value to the array
		src = src_scan;
		if (type == 'i') {
			int32_t *v = ufbxi_push_fast(&uc->tmp_stack, int32_t, 1);
			ufbxi_check(v);
			*v = (int32_t)val;
		} else if (type == 'l') {
			int64_t *v = ufbxi_push_fast(&uc->tmp_stack, int64_t, 1);
			ufbxi_check(v);
			*v = (int64_t)val;
		}

		// Try to parse the next value, we don't commit this until we find a comma after it above.
		size_t left = ufbxi_to_size(end - src_scan);
		if (left < 32) break;

		val = ufbxi_parse_int64(src_scan, (char**)&src_scan);
		if (!src_scan) break;
	}

	// Resume conventional parsing if we moved `src`.
	if (src != ua->src) {
		ua->src = src;
		ufbxi_check(ufbxi_ascii_next_token(uc, &ua->token));
	}

	*p_num_read = uc->tmp_stack.num_items - initial_items;
	return 1;
}

typedef struct {
	void *arr_data;
	char arr_type;
	size_t arr_size;
	const ufbxi_ascii_span *spans;
	size_t num_spans;
	size_t offset;
} ufbxi_ascii_array_task;

ufbxi_noinline static const char *ufbxi_ascii_array_task_parse_floats(ufbxi_ascii_array_task *t, const char *src, const char *src_end, uint32_t parse_flags)
{
	size_t offset = t->offset;
	float *dst_float = t->arr_type == 'f' ? (float*)t->arr_data + offset : NULL;
	double *dst_double = t->arr_type == 'd' ? (double*)t->arr_data + offset : NULL;
	ufbx_assert(dst_float || dst_double);
	const char *src_begin = src;

	while (src != src_end) {
		while (ufbxi_is_space(*src)) src++;

		// Try to parse the next value, we don't commit this until we find a comma after it above.
		char *num_end = NULL;
		double val = ufbxi_parse_double(src, ufbxi_to_size(src_end - src), &num_end, parse_flags);
		if (!num_end) return src_begin;
		src = num_end;

		while (ufbxi_is_space(*src)) src++;
		if (*src != ',') break;
		src++;
		src_begin = src;

		if (offset >= t->arr_size) return NULL;
		if (dst_double) {
			*dst_double++ = val;
		} else {
			*dst_float++ = (float)val;
		}
		offset++;
	}

	t->offset = offset;
	return src_begin;
}

ufbxi_noinline static const char *ufbxi_ascii_array_task_parse_ints(ufbxi_ascii_array_task *t, const char *src, const char *src_end)
{
	size_t offset = t->offset;
	int32_t *dst32 = t->arr_type == 'i' ? (int32_t*)t->arr_data + offset : NULL;
	int64_t *dst64 = t->arr_type == 'l' ? (int64_t*)t->arr_data + offset : NULL;
	ufbx_assert(dst32 || dst64);
	const char *src_begin = src;

	while (src != src_end) {
		while (ufbxi_is_space(*src)) src++;

		int64_t val = ufbxi_parse_int64(src, (char**)&src);
		if (!src) return NULL;

		while (ufbxi_is_space(*src)) src++;
		if (*src != ',') break;
		src++;
		src_begin = src;

		if (offset >= t->arr_size) return NULL;
		if (dst32) {
			*dst32++ = (int32_t)val;
		} else {
			*dst64++ = val;
		}
		offset++;
	}

	t->offset = offset;
	return src_begin;
}

ufbxi_noinline static const char *ufbxi_ascii_array_task_parse(ufbxi_ascii_array_task *t, const char *src, const char *src_end)
{
	if (t->arr_type == 'f' || t->arr_type == 'd') {
		uint32_t flags = ufbxi_parse_double_init_flags();
		return ufbxi_ascii_array_task_parse_floats(t, src, src_end, flags);
	} else {
		return ufbxi_ascii_array_task_parse_ints(t, src, src_end);
	}
}

typedef enum {
	UFBXI_ASCII_SCAN_STATE_VALUE,
	UFBXI_ASCII_SCAN_STATE_WHITESPACE,
	UFBXI_ASCII_SCAN_STATE_COMMENT,
	UFBXI_ASCII_SCAN_STATE_COMMA,
} ufbxi_ascii_scan_state;

ufbxi_noinline static bool ufbxi_ascii_array_task_imp(ufbxi_ascii_array_task *t)
{
	// Temporary buffer for parsing between spans
	char buffer[128]; // ufbxi_uninit
	size_t buffer_len = 0;
	bool buffer_value = false;

	ufbxi_ascii_scan_state state = UFBXI_ASCII_SCAN_STATE_WHITESPACE;
	ufbxi_for(const ufbxi_ascii_span, span, t->spans, t->num_spans) {
		const char *src = span->source;
		const char *end = src + span->length;

		while (src != end) {

			// State machine for skipping whitespace and comments, potentially
			// between multiple spans.
			while (src != end) {
				char c = *src;
				if (state == UFBXI_ASCII_SCAN_STATE_VALUE) {
					if (buffer_len >= sizeof(buffer) - 1) return false;
					if (c == '"') {
						return false;
					} else if (c == ';' || ufbxi_is_space(c)) {
						state = UFBXI_ASCII_SCAN_STATE_WHITESPACE;
						buffer[buffer_len] = ' ';
						buffer_len++;
					} else if (c == ',' || c == '}') {
						state = UFBXI_ASCII_SCAN_STATE_COMMA;
						buffer[buffer_len] = ',';
						buffer_len++;
						src++;
						break;
					} else {
						buffer_value = true;
						buffer[buffer_len] = c;
						buffer_len++;
						src++;
					}
				} else if (state == UFBXI_ASCII_SCAN_STATE_WHITESPACE) {
					if (c == ';') {
						state = UFBXI_ASCII_SCAN_STATE_COMMENT;
					} else if (ufbxi_is_space(c)) {
						src++;
					} else {
						state = UFBXI_ASCII_SCAN_STATE_VALUE;
					}
				} else if (state == UFBXI_ASCII_SCAN_STATE_COMMENT) {
					if (c == '\n') {
						state = UFBXI_ASCII_SCAN_STATE_WHITESPACE;
					} else {
						src++;
					}
				} else if (state == UFBXI_ASCII_SCAN_STATE_COMMA) {
					state = UFBXI_ASCII_SCAN_STATE_WHITESPACE;
				}
			}

			if (state == UFBXI_ASCII_SCAN_STATE_COMMA) {
				// Parse a value from the buffer
				if (buffer_value) {
					const char *buffer_end = ufbxi_ascii_array_task_parse(t, buffer, buffer + buffer_len);
					if (buffer_end == NULL || buffer_end == buffer) {
						return false;
					}
				}

				// If not at end, we are past the last comma, so try to find a
				// safe range to parse.
				if (src != end) {
					const char *parse_end = end;
					while (parse_end > src) {
						if (parse_end[-1] == ',') break;
						parse_end--;
					}
					if (src < parse_end) {
						src = ufbxi_ascii_array_task_parse(t, src, parse_end);
						if (src == NULL) return false;
					}
				}

				buffer_len = 0;
				buffer_value = false;
			}
		}
	}

	if (t->offset != t->arr_size) return false;

	return true;
}

ufbxi_noinline static bool ufbxi_ascii_array_task_fn(ufbxi_task *task)
{
	ufbxi_ascii_array_task *t = (ufbxi_ascii_array_task *)task->data;
	if (!ufbxi_ascii_array_task_imp(t)) {
		task->error = "Threaded ASCII parse error";
		return false;
	}
	return true;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_ascii_read_float_array(ufbxi_context *uc, char type, size_t *p_num_read)
{
	ufbxi_ascii *ua = &uc->ascii;
	if (ua->parse_as_f32) return 1;

	double val;
	if (ua->token.type == UFBXI_ASCII_FLOAT) {
		val = ua->token.value.f64;
	} else if (ua->token.type == UFBXI_ASCII_INT) {
		double fsign = !ua->token.value.i64 && ua->token.negative ? -1.0 : 1.0;
		val = (double)ua->token.value.i64 * fsign;
	} else {
		return 1;
	}

	const char *src = ua->src;
	const char *end = ua->src_yield;

	uint32_t parse_flags = uc->double_parse_flags | UFBXI_PARSE_DOUBLE_VERIFY_LENGTH;

	size_t initial_items = uc->tmp_stack.num_items;
	const char *src_scan = src;
	for (;;) {

		// Skip '\s*,\s*' between array elements. If we don't find a comma after an element
		// don't push it as we can't be 100% certain whether it's a part of the array.
		while (src_scan != end && ufbxi_is_space(*src_scan)) src_scan++;
		if (src_scan == end || *src_scan != ',') break;
		src_scan++;
		while (src_scan != end && ufbxi_is_space(*src_scan)) src_scan++;

		// Found comma, commit to the position and push the previous value to the array
		src = src_scan;
		if (type == 'd') {
			double *v = ufbxi_push_fast(&uc->tmp_stack, double, 1);
			ufbxi_check(v);
			*v = (double)val;
		} else if (type == 'f') {
			float *v = ufbxi_push_fast(&uc->tmp_stack, float, 1);
			ufbxi_check(v);
			*v = (float)val;
		}

		// Try to parse the next value, we don't commit this until we find a comma after it above.
		char *num_end = NULL;
		size_t left = ufbxi_to_size(end - src_scan);
		if (left < 64) break;
		val = ufbxi_parse_double(src_scan, left - 2, &num_end, parse_flags);
		if (!num_end || num_end == src_scan) {
			break;
		}

		src_scan = num_end;
	}

	// Resume conventional parsing if we moved `src`.
	if (src != ua->src) {
		ua->src = src;
		ufbxi_check(ufbxi_ascii_next_token(uc, &ua->token));
	}

	*p_num_read = uc->tmp_stack.num_items - initial_items;
	return 1;
}

// Recursion limited by check at the start
ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_parse_node(ufbxi_context *uc, uint32_t depth, ufbxi_parse_state parent_state, bool *p_end, ufbxi_buf *tmp_buf, bool recursive)
	ufbxi_recursive_function(int, ufbxi_ascii_parse_node, (uc, depth, parent_state, p_end, tmp_buf, recursive), UFBXI_MAX_NODE_DEPTH + 1,
		(ufbxi_context *uc, uint32_t depth, ufbxi_parse_state parent_state, bool *p_end, ufbxi_buf *tmp_buf, bool recursive))
{
	ufbxi_ascii *ua = &uc->ascii;

	if (ua->token.type == '}') {
		ufbxi_check(ufbxi_ascii_next_token(uc, &ua->token));
		*p_end = true;
		return 1;
	}

	if (ua->token.type == UFBXI_ASCII_END) {
		ufbxi_check_msg(depth == 0, "Truncated file");
		*p_end = true;
		return 1;
	}

	// Parse the name eg. "Node:" token and intern the name
	ufbxi_check(depth < UFBXI_MAX_NODE_DEPTH);
	if (!uc->sure_fbx && depth == 0 && ua->token.type != UFBXI_ASCII_NAME) {
		ufbxi_fail_msg("Expected a 'Name:' token", "Not an FBX file");
	}
	ufbxi_check(ufbxi_ascii_accept(uc, UFBXI_ASCII_NAME));
	size_t name_len = ua->prev_token.value.name_len;
	ufbxi_check(name_len <= 0xff);
	const char *name = ufbxi_push_string(&uc->string_pool, ua->prev_token.str_data, ua->prev_token.str_len, NULL, true);
	ufbxi_check(name);

	// Push the parsed node into the `tmp_stack` buffer, the nodes will be popped by
	// calling code after its done parsing all of it's children.
	ufbxi_node *node = ufbxi_push_zero(&uc->tmp_stack, ufbxi_node, 1);
	ufbxi_check(node);
	node->name = name;
	node->name_len = (uint8_t)name_len;

	bool in_ascii_array = false;

	uint32_t num_values = 0;
	uint32_t type_mask = 0;

	int arr_type = 0;
	ufbxi_buf *arr_buf = NULL;
	size_t arr_elem_size = 0;

	// Check if the values of the node we're parsing currently should be
	// treated as an array.
	ufbxi_array_info arr_info;
	if (ufbxi_is_array_node(uc, parent_state, name, &arr_info)) {
		uint32_t flags = arr_info.flags;
		arr_type = ufbxi_normalize_array_type(arr_info.type, 'b');
		arr_buf = tmp_buf;
		if (flags & UFBXI_ARRAY_FLAG_RESULT) arr_buf = &uc->result;
		else if (flags & UFBXI_ARRAY_FLAG_TMP_BUF) arr_buf = &uc->tmp;

		ufbxi_value_array *arr = ufbxi_push(tmp_buf, ufbxi_value_array, 1);
		ufbxi_check(arr);
		node->value_type_mask = UFBXI_VALUE_ARRAY;
		node->array = arr;
		arr->type = (char)arr_type;

		// Parse array values using strtof() if the array destination is 32-bit float
		// since KeyAttrDataFloat packs integer data (!) into floating point values so we
		// should try to be as exact as possible.
		if (arr_info.flags & UFBXI_ARRAY_FLAG_ACCURATE_F32) {
			ua->parse_as_f32 = true;
		}

		arr_elem_size = ufbxi_array_type_size((char)arr_type);

		if (arr_type != '-') {
			// Force alignment for array contents: This allows us to use `ufbxi_push_fast()`
			// in fast parsing functions.
			ufbxi_check(ufbxi_push_size_zero(&uc->tmp_stack, 8, 1));

			// Pad with 4 zero elements to make indexing with `-1` safe.
			if ((flags & UFBXI_ARRAY_FLAG_PAD_BEGIN) != 0) {
				ufbxi_check(ufbxi_push_size_zero(&uc->tmp_stack, arr_elem_size, 4));
				num_values += 4;
			}
		}
	}

	// Some fields in ASCII may have leading commas eg. `Content: , "base64-string"`
	if (ua->token.type == ',') {
		// HACK: If we are parsing an "array" that should be ignored, ie. `Content` when
		// `opts.ignore_embedded == true` try to skip the next token string if possible.
		if (arr_type == '-') {
			if (!ufbxi_ascii_try_ignore_string(uc, &ua->token)) {
				ufbxi_check(ufbxi_ascii_next_token(uc, &ua->token));
			}
		} else {
			ufbxi_check(ufbxi_ascii_next_token(uc, &ua->token));
		}
	}

	ufbxi_parse_state parse_state = ufbxi_update_parse_state(parent_state, node->name);
	ufbxi_value vals[UFBXI_MAX_NON_ARRAY_VALUES];

	uint32_t deferred_size = 0;

	// NOTE: Infinite loop to allow skipping the comma parsing via `continue`.
	for (;;) {
		ufbxi_ascii_token *tok = &ua->prev_token;

		if (arr_type) {
			size_t num_read = 0;
			if (arr_type == 'f' || arr_type == 'd') {
				ufbxi_check(ufbxi_ascii_read_float_array(uc, (char)arr_type, &num_read));
			} else if (arr_type == 'i' || arr_type == 'l') {
				ufbxi_check(ufbxi_ascii_read_int_array(uc, (char)arr_type, &num_read));
			}
			ufbxi_check(UINT32_MAX - num_values > num_read);
			num_values += (uint32_t)num_read;
		}

		if (ufbxi_ascii_accept(uc, UFBXI_ASCII_STRING)) {

			if (arr_type) {

				if (arr_type == 's' || arr_type == 'S' || arr_type == 'C') {
					bool raw = arr_type == 's';
					ufbx_string *v = ufbxi_push(&uc->tmp_stack, ufbx_string, 1);
					ufbxi_check(v);
					v->data = tok->str_data;
					v->length = tok->str_len;
					if (arr_type == 'C') {
						ufbxi_buf *buf = uc->opts.retain_dom ? &uc->result : tmp_buf;
						v->data = ufbxi_push_copy(buf, char, v->length, v->data);
						ufbxi_check(v->data);
					} else {
						ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, v, raw));
					}
				} else {
					// Ignore strings in non-string arrays, decrement `num_values` as it will be
					// incremented after the loop iteration is done to ignore it.
					num_values--;
				}

			} else if (num_values < UFBXI_MAX_NON_ARRAY_VALUES) {
				type_mask |= (uint32_t)UFBXI_VALUE_STRING << (num_values*2);
				ufbxi_value *v = &vals[num_values];

				const char *str = tok->str_data;
				size_t length = tok->str_len;
				ufbxi_check(str);

				if (length == 0) {
					v->s.raw_data = ufbxi_empty_char;
					v->s.raw_length = 0;
					v->s.utf8_length = 0;
				} else {
					bool non_ascii = false;
					uint32_t hash = ufbxi_hash_string_check_ascii(str, length, &non_ascii);
					bool raw = !non_ascii || ufbxi_is_raw_string(uc, parent_state, name, num_values);
					ufbxi_check(ufbxi_push_sanitized_string(&uc->string_pool, &v->s, str, length, hash, raw));
					if (non_ascii && raw) v->s.utf8_length = UINT32_MAX;
				}
			}

		} else if (ufbxi_ascii_accept(uc, UFBXI_ASCII_INT)) {
			int64_t val = tok->value.i64;
			ufbx_real fsign = !val && tok->negative ? (ufbx_real)-1.0f : (ufbx_real)1.0f;

			switch (arr_type) {

			case 0:
				// Parse version from comment if there was no magic comment
				if (!ua->found_version && parse_state == UFBXI_PARSE_FBX_VERSION && num_values == 0) {
					if (val >= 6000 && val <= 10000) {
						ua->found_version = true;
						uc->version = (uint32_t)val;
					}
				}

				if (num_values < UFBXI_MAX_NON_ARRAY_VALUES) {
					type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (num_values*2);
					ufbxi_value *v = &vals[num_values];
					// False positive: `v->f` and `v->i` do not overlap in the union.
					// cppcheck-suppress overlappingWriteUnion
					v->f = (double)(v->i = val) * (double)fsign;
				}
				break;

			case 'b': { bool *v = ufbxi_push(&uc->tmp_stack, bool, 1); ufbxi_check(v); *v = val != 0; } break;
			case 'c': { uint8_t *v = ufbxi_push(&uc->tmp_stack, uint8_t, 1); ufbxi_check(v); *v = (uint8_t)val; } break;
			case 'i': { int32_t *v = ufbxi_push(&uc->tmp_stack, int32_t, 1); ufbxi_check(v); *v = (int32_t)val; } break;
			case 'l': { int64_t *v = ufbxi_push(&uc->tmp_stack, int64_t, 1); ufbxi_check(v); *v = (int64_t)val; } break;
			case 'f': { float *v = ufbxi_push(&uc->tmp_stack, float, 1); ufbxi_check(v); *v = (float)val * (float)fsign; } break;
			case 'd': { double *v = ufbxi_push(&uc->tmp_stack, double, 1); ufbxi_check(v); *v = (double)val * (double)fsign; } break;
			case '-': num_values--; break;

			default:
				ufbxi_fail("Bad array dst type");

			}

		} else if (ufbxi_ascii_accept(uc, UFBXI_ASCII_FLOAT)) {
			double val = tok->value.f64;

			switch (arr_type) {

			case 0:
				if (num_values < UFBXI_MAX_NON_ARRAY_VALUES) {
					type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (num_values*2);
					ufbxi_value *v = &vals[num_values];
					// False positive: `v->f` and `v->i` do not overlap in the union.
					// cppcheck-suppress overlappingWriteUnion
					v->i = ufbxi_f64_to_i64(v->f = val);
				}
				break;

			case 'b': { bool *v = ufbxi_push(&uc->tmp_stack, bool, 1); ufbxi_check(v); *v = val != 0; } break;
			case 'c': { uint8_t *v = ufbxi_push(&uc->tmp_stack, uint8_t, 1); ufbxi_check(v); *v = (uint8_t)val; } break;
			case 'i': { int32_t *v = ufbxi_push(&uc->tmp_stack, int32_t, 1); ufbxi_check(v); *v = ufbxi_f64_to_i32(val); } break;
			case 'l': { int64_t *v = ufbxi_push(&uc->tmp_stack, int64_t, 1); ufbxi_check(v); *v = ufbxi_f64_to_i64(val); } break;
			case 'f': { float *v = ufbxi_push(&uc->tmp_stack, float, 1); ufbxi_check(v); *v = (float)val; } break;
			case 'd': { double *v = ufbxi_push(&uc->tmp_stack, double, 1); ufbxi_check(v); *v = (double)val; } break;
			case '-': num_values--; break;

			default:
				ufbxi_fail("Bad array dst type");

			}

		} else if (ufbxi_ascii_accept(uc, UFBXI_ASCII_BARE_WORD)) {

			int64_t val = 0;
			if (tok->str_len >= 1) {
				val = (int64_t)tok->str_data[0];
			}

			switch (arr_type) {

			case 0:
				if (num_values < UFBXI_MAX_NON_ARRAY_VALUES) {
					type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (num_values*2);
					ufbxi_value *v = &vals[num_values];
					// False positive: `v->f` and `v->i` do not overlap in the union.
					// cppcheck-suppress overlappingWriteUnion
					v->f = (double)(v->i = val);
				}
				break;

			case 'b': { bool *v = ufbxi_push(&uc->tmp_stack, bool, 1); ufbxi_check(v); *v = val != 0; } break;
			case 'c': { uint8_t *v = ufbxi_push(&uc->tmp_stack, uint8_t, 1); ufbxi_check(v); *v = (uint8_t)val; } break;
			case 'i': { int32_t *v = ufbxi_push(&uc->tmp_stack, int32_t, 1); ufbxi_check(v); *v = (int32_t)val; } break;
			case 'l': { int64_t *v = ufbxi_push(&uc->tmp_stack, int64_t, 1); ufbxi_check(v); *v = (int64_t)val; } break;
			case 'f': { float *v = ufbxi_push(&uc->tmp_stack, float, 1); ufbxi_check(v); *v = (float)val; } break;
			case 'd': { double *v = ufbxi_push(&uc->tmp_stack, double, 1); ufbxi_check(v); *v = (double)val; } break;
			case '-': num_values--; break;

			default:
				ufbxi_fail("Bad array dst type");
			}

		} else if (ufbxi_ascii_accept(uc, '*')) {
			// Parse a post-7000 ASCII array eg. "*3 { 1,2,3 }"
			ufbxi_check(!in_ascii_array);
			ufbxi_check(ufbxi_ascii_accept(uc, UFBXI_ASCII_INT));
			int64_t count = ua->prev_token.value.i64;

			if (ufbxi_ascii_accept(uc, '{')) {
				ufbxi_check(ufbxi_ascii_accept(uc, UFBXI_ASCII_NAME));
				in_ascii_array = true;

				// Optimized array skipping and threaded parsing
				if (arr_type == '-') {
					ufbxi_check(ufbxi_ascii_skip_until(uc, '}'));
				} else if (uc->parse_threaded && !uc->opts.force_single_thread_ascii_parsing
						&& !ua->parse_as_f32
						&& (arr_type == 'i' || arr_type == 'l' || arr_type == 'f' || arr_type == 'd')) {
					// Don't bother with small arrays due to fixed overhead
					if (count >= UFBXI_MIN_THREADED_ASCII_VALUES && count <= UINT32_MAX) {
						deferred_size = (uint32_t)count - 1;
						ufbxi_check(ufbxi_ascii_store_array(uc, tmp_buf));
					}
				}
			}

			// NOTE: This `continue` skips incrementing `num_values` and parsing
			// a comma, continuing to parse the values in the array.
			continue;
		} else {
			break;
		}

		// Add value and keep parsing if there's a comma. This part may be
		// skipped if we enter an array block.
		num_values++;
		ufbxi_check(num_values < UINT32_MAX);
		if (!ufbxi_ascii_accept(uc, ',')) break;
	}

	// Close the ASCII array if we are in one
	if (in_ascii_array) {
		ufbxi_check(ufbxi_ascii_accept(uc, '}'));
	}

	ua->parse_as_f32 = false;

	if (arr_type) {
		if (arr_type == '-') {
			node->array->data = NULL;
			node->array->size = 0;
		} else {
			void *arr_data = NULL;

			if (deferred_size > 0) {
				arr_data = ufbxi_push_size(arr_buf, arr_elem_size, num_values + deferred_size);
				// Pop any previously pushed values
				if (num_values > 0) {
					ufbxi_pop_size(&uc->tmp_stack, arr_elem_size, num_values, arr_data, false);
				}
			} else {
				arr_data = ufbxi_push_pop_size(arr_buf, &uc->tmp_stack, arr_elem_size, num_values);
			}
			ufbxi_check(arr_data);
			if (arr_info.flags & UFBXI_ARRAY_FLAG_PAD_BEGIN) {
				node->array->data = (char*)arr_data + 4*arr_elem_size;
				node->array->size = num_values + deferred_size - 4;
			} else {
				node->array->data = arr_data;
				node->array->size = num_values + deferred_size;
			}

			// Pop alignment helper
			ufbxi_pop_size(&uc->tmp_stack, 8, 1, NULL, false);

			// Deferred parsing
			if (deferred_size > 0) {
				size_t num_spans = uc->tmp_ascii_spans.num_items;
				ufbxi_ascii_span *spans = ufbxi_push_pop(tmp_buf, &uc->tmp_ascii_spans, ufbxi_ascii_span, num_spans);
				ufbxi_check(spans);

				ufbxi_ascii_array_task t; // ufbxi_uninit
				t.arr_data = (char*)arr_data + num_values * arr_elem_size;
				t.arr_type = (char)arr_type;
				t.arr_size = deferred_size;
				t.num_spans = num_spans;
				t.spans = spans;
				t.offset = 0;

				// TODO: Split these further
				ufbxi_task *task = ufbxi_thread_pool_create_task(&uc->thread_pool, &ufbxi_ascii_array_task_fn);
				if (task) {
					task->data = ufbxi_push_copy(tmp_buf, ufbxi_ascii_array_task, 1, &t);
					ufbxi_check(task->data);
					ufbxi_thread_pool_run_task(&uc->thread_pool, task);
				} else {
					ufbxi_check_msg(ufbxi_ascii_array_task_imp(&t), "Threaded ASCII parse error");
				}
			}
		}
	} else {
		num_values = ufbxi_min32(num_values, UFBXI_MAX_NON_ARRAY_VALUES);
		node->value_type_mask = (uint16_t)type_mask;
		node->vals = ufbxi_push_copy(tmp_buf, ufbxi_value, num_values, vals);
		ufbxi_check(node->vals);
	}

	// Recursively parse the children of this node. Update the parse state
	// to provide context for child node parsing.
	if (ufbxi_ascii_accept(uc, '{')) {
		if (recursive) {
			size_t num_children = 0;
			for (;;) {
				bool end = false;
				ufbxi_check(ufbxi_ascii_parse_node(uc, depth + 1, parse_state, &end, tmp_buf, recursive));
				if (end) break;
				num_children++;
			}

			// Pop children from `tmp_stack` to a contiguous array
			node->children = ufbxi_push_pop(tmp_buf, &uc->tmp_stack, ufbxi_node, num_children);
			ufbxi_check(node->children);
			node->num_children = (uint32_t)num_children;
		}

		uc->has_next_child = true;
	} else {
		uc->has_next_child = false;
	}

	return 1;
}

// -- DOM retention

typedef struct {
	uintptr_t node_ptr;
	ufbx_dom_node *dom_node;
} ufbxi_dom_mapping;

ufbxi_nodiscard static ufbxi_noinline ufbx_dom_node *ufbxi_get_dom_node_imp(ufbxi_context *uc, ufbxi_node *node)
{
	if (!node) return NULL;
	ufbxi_dom_mapping mapping = { (uintptr_t)node, NULL };
	uint32_t hash = ufbxi_hash_uptr(mapping.node_ptr);
	ufbxi_dom_mapping *result = ufbxi_map_find(&uc->dom_node_map, ufbxi_dom_mapping, hash, &mapping);
	return result ? result->dom_node : NULL;
}

ufbxi_nodiscard static ufbxi_forceinline ufbx_dom_node *ufbxi_get_dom_node(ufbxi_context *uc, ufbxi_node *node)
{
	if (!uc->opts.retain_dom) return NULL;
	return ufbxi_get_dom_node_imp(uc, node);
}

// Recursion limited by check in ufbxi_[binary/ascii]_parse_node()
ufbxi_nodiscard static ufbxi_noinline int ufbxi_retain_dom_node(ufbxi_context *uc, ufbxi_node *node, ufbx_dom_node **p_dom_node)
	ufbxi_recursive_function(int, ufbxi_retain_dom_node, (uc, node, p_dom_node), UFBXI_MAX_NODE_DEPTH + 1,
		(ufbxi_context *uc, ufbxi_node *node, ufbx_dom_node **p_dom_node))
{
	ufbx_dom_node *dst = ufbxi_push_zero(&uc->result, ufbx_dom_node, 1);
	ufbxi_check(dst);
	ufbxi_check(ufbxi_push_copy(&uc->tmp_dom_nodes, ufbx_dom_node*, 1, &dst));

	if (p_dom_node) {
		*p_dom_node = dst;
	}

	dst->name.data = node->name;
	dst->name.length = node->name_len;

	{
		ufbxi_dom_mapping mapping = { (uintptr_t)node, NULL };
		uint32_t hash = ufbxi_hash_uptr(mapping.node_ptr);
		ufbxi_dom_mapping *result = ufbxi_map_find(&uc->dom_node_map, ufbxi_dom_mapping, hash, &mapping);
		if (!result) {
			result = ufbxi_map_insert(&uc->dom_node_map, ufbxi_dom_mapping, hash, &mapping);
			ufbxi_check(result);
		}
		result->node_ptr = (uintptr_t)node;
		result->dom_node = dst;
	}

	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &dst->name, false));

	if (node->value_type_mask == UFBXI_VALUE_ARRAY) {
		ufbxi_value_array *arr = node->array;
		ufbx_dom_value *val = ufbxi_push_zero(&uc->result, ufbx_dom_value, 1);
		ufbxi_check(val);

		dst->values.data = val;
		dst->values.count = 1;

		size_t elem_size = ufbxi_array_type_size(arr->type);
		val->value_str.data = ufbxi_empty_char;
		val->value_blob.data = arr->data;
		val->value_blob.size = arr->size * elem_size;
		val->value_float = (double)(val->value_int = (int64_t)arr->size);

		switch (arr->type) {
		case 'c': val->type = UFBX_DOM_VALUE_ARRAY_I8; break;
		case 'b': val->type = UFBX_DOM_VALUE_ARRAY_I8; break;
		case 'i': val->type = UFBX_DOM_VALUE_ARRAY_I32; break;
		case 'l': val->type = UFBX_DOM_VALUE_ARRAY_I64; break;
		case 'f': val->type = UFBX_DOM_VALUE_ARRAY_F32; break;
		case 'd': val->type = UFBX_DOM_VALUE_ARRAY_F64; break;
		case 's': val->type = UFBX_DOM_VALUE_ARRAY_RAW_STRING; break;
		case 'C': val->type = UFBX_DOM_VALUE_ARRAY_RAW_STRING; break;
		case '-': val->type = UFBX_DOM_VALUE_ARRAY_IGNORED; break;
		default: ufbxi_fail("Bad array type");
		}
	} else {
		size_t ix;
		for (ix = 0; ix < UFBXI_MAX_NON_ARRAY_VALUES; ix++) {
			uint32_t mask = (node->value_type_mask >> (2*ix)) & 0x3;
			if (!mask) break;
			ufbx_dom_value *val = ufbxi_push_zero(&uc->tmp_stack, ufbx_dom_value, 1);
			ufbxi_check(val);
			val->value_str.data = ufbxi_empty_char;

			if (mask == UFBXI_VALUE_STRING) {
				val->type = UFBX_DOM_VALUE_STRING;
				ufbxi_ignore(ufbxi_get_val_at(node, ix, 'S', &val->value_str));
				ufbxi_ignore(ufbxi_get_val_at(node, ix, 'b', &val->value_blob));
			} else {
				ufbx_assert(mask == UFBXI_VALUE_NUMBER);
				val->type = UFBX_DOM_VALUE_NUMBER;
				val->value_int = node->vals[ix].i;
				val->value_float = node->vals[ix].f;
			}
		}

		dst->values.count = ix;
		dst->values.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_dom_value, ix);
		ufbxi_check(dst->values.data);
	}

	if (node->num_children > 0) {
		ufbxi_for(ufbxi_node, child, node->children, node->num_children) {
			ufbxi_check(ufbxi_retain_dom_node(uc, child, NULL));
		}

		dst->children.count = node->num_children;
		dst->children.data = ufbxi_push_pop(&uc->result, &uc->tmp_dom_nodes, ufbx_dom_node*, node->num_children);
		ufbxi_check(dst->children.data);
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_retain_toplevel(ufbxi_context *uc, ufbxi_node *node)
{
	if (uc->dom_parse_num_children > 0) {
		ufbx_dom_node **children = ufbxi_push_pop(&uc->result, &uc->tmp_dom_nodes, ufbx_dom_node*, uc->dom_parse_num_children);
		ufbxi_check(children);
		uc->dom_parse_toplevel->children.data = children;
		uc->dom_parse_toplevel->children.count = uc->dom_parse_num_children;
		uc->dom_parse_num_children = 0;
	}

	if (node) {
		ufbxi_check(ufbxi_retain_dom_node(uc, node, &uc->dom_parse_toplevel));
	} else {
		uc->dom_parse_toplevel = NULL;

		// Called with NULL argument to finish retaining DOM, collect the final nodes to `ufbx_scene`.
		size_t num_top_nodes = uc->tmp_dom_nodes.num_items;
		ufbx_dom_node **nodes = ufbxi_push_pop(&uc->result, &uc->tmp_dom_nodes, ufbx_dom_node*, num_top_nodes);
		ufbxi_check(nodes);

		ufbx_dom_node *dom_root = ufbxi_push_zero(&uc->result, ufbx_dom_node, 1);
		ufbxi_check(dom_root);

		dom_root->name.data = ufbxi_empty_char;
		dom_root->children.data = nodes;
		dom_root->children.count = num_top_nodes;

		uc->scene.dom_root = dom_root;
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_retain_toplevel_child(ufbxi_context *uc, ufbxi_node *child)
{
	ufbx_assert(uc->dom_parse_toplevel);
	ufbxi_check(ufbxi_retain_dom_node(uc, child, NULL));
	uc->dom_parse_num_children++;

	return 1;
}

// -- General parsing

static ufbxi_noinline bool ufbxi_next_line(ufbx_string *line, ufbx_string *buf, bool skip_space)
{
	if (buf->length == 0) return false;
	const char *newline = (const char*)memchr(buf->data, '\n', buf->length);
	size_t length = newline ? ufbxi_to_size(newline - buf->data) + 1 : buf->length;

	line->data = buf->data;
	line->length = length;
	buf->data += length;
	buf->length -= length;

	if (skip_space) {
		while (line->length > 0 && ufbxi_is_space(line->data[0])) {
			line->data++;
			line->length--;
		}
		while (line->length > 0 && ufbxi_is_space(line->data[line->length - 1])) {
			line->length--;
		}
	}

	return true;
}

// Recursion limited by compile time patterns
static ufbxi_noinline const char *ufbxi_match_skip(const char *fmt, bool alternation)
	ufbxi_recursive_function(const char *, ufbxi_match_skip, (fmt, alternation), 4,
		(const char *fmt, bool alternation))
{
	for (;;) {
		char c = *fmt++;
		switch (c) {
		case '(':
			fmt = ufbxi_match_skip(fmt, false) + 1;
			break;
		case '\\':
			fmt++;
			break;
		case '[':
			c = *fmt;
			while (c != ']') {
				c = *fmt++;
				if (c == '\\') {
					c = *fmt++;
				}
			}
			fmt++;
			break;
		case '|':
			if (alternation) return fmt - 1;
			break;
		case ')':
		case '\0':
			return fmt - 1;
		default: break;
		}
	}
}

// Recursion limited by compile time patterns
static ufbxi_noinline bool ufbxi_match_imp(const char **p_str, const char *end, const char **p_fmt)
	ufbxi_recursive_function(bool, ufbxi_match_imp, (p_str, end, p_fmt), 4,
		(const char **p_str, const char *end, const char **p_fmt))
{
	const char *str_original_begin = *p_str;
	const char *str = str_original_begin;
	const char *fmt_begin = *p_fmt;
	const char *fmt = fmt_begin;
	bool case_insensitive = false;

	size_t count = 0;
	for (;;) {
		char c = *fmt++;
		if (!c) {
			*p_str = str;
			*p_fmt = fmt - 1;
			return true;
		}

		const char *str_begin = str;
		char ref = str != end ? *str : '\0';

		if (case_insensitive) {
			if (ref >= 'A' && ref <= 'Z') {
				ref = (char)((int)(ref - 'A') + 'a');
			}
		}

		bool ok = false;
		switch (c) {

		case '\\': {
			const char *macro = NULL;
			c = *fmt++;
			switch (c) {
			case 'd':
				macro = "[0-9]";
				break;
			case 'F':
				macro = "[\\-+]?[0-9]+(\\.[0-9]+)?([eE][\\-+]?[0-9]+)?";
				break;
			case 's':
				if (ufbxi_is_space(ref)) {
					ok = true;
					str++;
				}
				break;
			case 'S':
				if (!ufbxi_is_space(ref)) {
					ok = true;
					str++;
				}
				break;
			case 'c':
			case 'C':
				case_insensitive = c == 'c';
				ok = true;
				break;
			default:
				if (ref == c) {
					ok = true;
					str++;
				}
				break;
			}
			if (macro) {
				ok = ufbxi_match_imp(&str, end, &macro);
			}
		} break;

		case '[': {
			while (fmt[0] != ']') {
				if (fmt[0] == '\\') {
					if (ref == fmt[1]) ok = true;
					fmt += 2;
				} else if (fmt[1] == '-') {
					if (ref >= fmt[0] && ref <= fmt[2]) {
						ok = true;
					}
					fmt += 3;
				} else {
					if (ref == fmt[0]) ok = true;
					fmt += 1;
				}
			}
			fmt++;
			if (ok) str++;
		} break;

		case '(':
			if (ufbxi_match_imp(&str, end, &fmt)) {
				ok = true;
			}
			break;

		case '|':
			fmt = ufbxi_match_skip(fmt, false);
			ok = true;
			break;

		case ')':
			*p_str = str;
			*p_fmt = fmt;
			return true;

		case '.':
			if (ref != '\0') {
				ok = true;
				str++;
			}
			break;

		default:
			if (c == ref) {
				str++;
				ok = true;
			}
			break;
		}

		bool did_fail = false;
		c = *fmt;
		switch (c) {
		case '*':
			fmt++;
			if (ok) {
				fmt = fmt_begin;
				count++;
				continue;
			}
			break;
		case '+':
			fmt++;
			if (ok) {
				fmt = fmt_begin;
				count++;
				continue;
			} else if (count == 0) {
				did_fail = true;
			}
			break;
		case '?':
			fmt++;
			break;
		default:
			did_fail = !ok;
			break;
		}

		if (did_fail) {
			fmt = ufbxi_match_skip(fmt, true);
			if (*fmt == '|') {
				fmt++;
				str = str_original_begin;
			} else {
				*p_fmt = ufbxi_match_skip(fmt, false) + 1;
				return false;
			}
		} else {
			if (!ok) {
				str = str_begin;
			}
		}

		fmt_begin = fmt;
		count = 0;
	}
}

static ufbxi_noinline bool ufbxi_match(const ufbx_string *str, const char *fmt)
{
	const char *ptr = str->data, *end = str->data + str->length;
	if (ufbxi_match_imp(&ptr, end, &fmt)) {
		return ptr == end;
	} else {
		return false;
	}
}

static ufbxi_noinline bool ufbxi_is_format(const char *data, size_t size, ufbx_file_format format)
{
	ufbx_string line, buf = { data, size };

	if (format == UFBX_FILE_FORMAT_FBX) {
		if (size >= UFBXI_BINARY_MAGIC_SIZE && !memcmp(data, ufbxi_binary_magic, UFBXI_BINARY_MAGIC_SIZE)) {
			return true;
		}

		while (ufbxi_next_line(&line, &buf, true)) {
			if (ufbxi_match(&line, ";\\s*FBX\\s*\\d+\\.\\d+\\.\\d+\\s*project\\s+file")) return true;
			if (ufbxi_match(&line, "FBXHeaderExtension:.*")) return true;
		}
	} else if (format == UFBX_FILE_FORMAT_OBJ) {
		while (ufbxi_next_line(&line, &buf, true)) {
			const char *pattern =
				"(vn?\\s+\\F|vt)\\s+\\F\\s+\\F.*" "|"
				"f\\s+[\\-/0-9]+\\s+[\\-/0-9]+\\s*[\\-/0-9]+.*" "|"
				"(usemtl|mtllib)\\s+\\S.*";
			if (ufbxi_match(&line, pattern)) return true;
		}
	} else if (format == UFBX_FILE_FORMAT_MTL) {
		while (ufbxi_next_line(&line, &buf, true)) {
			const char *pattern =
				"newmtl\\s+\\S.*";
			if (ufbxi_match(&line, pattern)) return true;
		}
	} else {
		ufbxi_unreachable("Unhandled format");
	}

	return false;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_determine_format(ufbxi_context *uc)
{
	ufbx_file_format format = uc->opts.file_format;

	if (format == UFBX_FILE_FORMAT_UNKNOWN && !uc->opts.no_format_from_content) {
		ufbxi_pause_progress(uc);

		size_t lookahead = UFBXI_MIN_FILE_FORMAT_LOOKAHEAD;
		while (format == UFBX_FILE_FORMAT_UNKNOWN && lookahead <= uc->opts.file_format_lookahead) {
			if (lookahead > uc->data_size) {
				if (uc->eof) break;
				ufbxi_check(ufbxi_refill(uc, lookahead, false));
			}

			size_t data_size = ufbxi_min_sz(lookahead, uc->data_size);
			ufbxi_check_msg(data_size > 0, "Empty file");

			for (uint32_t fmt = UFBX_FILE_FORMAT_FBX; fmt < UFBX_FILE_FORMAT_COUNT; fmt++) {
				if (ufbxi_is_format(uc->data, data_size, (ufbx_file_format)fmt)) {
					format = (ufbx_file_format)fmt;
					break;
				}
			}

			if (lookahead >= uc->opts.file_format_lookahead) {
				break;
			} else if (lookahead < SIZE_MAX / 2) {
				lookahead = ufbxi_min_sz(lookahead * 2, uc->opts.file_format_lookahead);
			} else {
				lookahead = SIZE_MAX;
			}
		}

		ufbxi_check(ufbxi_resume_progress(uc));
	}

	if (format == UFBX_FILE_FORMAT_UNKNOWN && !uc->opts.no_format_from_extension) {
		if (uc->opts.filename.length > 0) {
			ufbx_string extension = uc->opts.filename;
			for (size_t i = extension.length; i > 0; i--) {
				if (extension.data[i - 1] == '.') {
					extension.data += i - 1;
					extension.length -= i - 1;
					break;
				}
			}

			if (ufbxi_match(&extension, "\\c\\.fbx")) {
				format = UFBX_FILE_FORMAT_FBX;
			} else if (ufbxi_match(&extension, "\\c\\.obj")) {
				format = UFBX_FILE_FORMAT_OBJ;
			} else if (ufbxi_match(&extension, "\\c\\.mtl")) {
				format = UFBX_FILE_FORMAT_MTL;
			}
		}
	}

	ufbxi_check_msg(format != UFBX_FILE_FORMAT_UNKNOWN, "Unrecognized file format");
	uc->scene.metadata.file_format = format;

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_begin_parse(ufbxi_context *uc)
{
	const char *header = ufbxi_peek_bytes(uc, UFBXI_BINARY_HEADER_SIZE);
	ufbxi_check(header);

	// If the file starts with the binary magic parse it as binary, otherwise
	// treat it as an ASCII file.
	if (!memcmp(header, ufbxi_binary_magic, UFBXI_BINARY_MAGIC_SIZE)) {

		// The byte after the magic indicates endianness
		char endian = header[UFBXI_BINARY_MAGIC_SIZE + 0];
		uc->file_big_endian = endian != 0;

		// Read the version directly from the header
		const char *version_word = header + UFBXI_BINARY_MAGIC_SIZE + 1;
		if (uc->file_big_endian) {
			version_word = ufbxi_swap_endian(uc, version_word, 1, 4);
			ufbxi_check(version_word);
		}
		uc->version = ufbxi_read_u32(version_word);

		// This is quite probably an FBX file..
		uc->sure_fbx = true;
		ufbxi_consume_bytes(uc, UFBXI_BINARY_HEADER_SIZE);

	} else {
		uc->from_ascii = true;

		// Use the current read buffer as the initial parse buffer
		memset(&uc->ascii, 0, sizeof(uc->ascii));
		uc->ascii.src = uc->data;
		uc->ascii.src_yield = uc->data + uc->yield_size;
		uc->ascii.src_end = uc->data + uc->data_size + uc->yield_size;

		// Initialize the first token
		ufbxi_check(ufbxi_ascii_next_token(uc, &uc->ascii.token));

		// Default to version 7400 if not found in header
		if (uc->version > 0) {
			uc->sure_fbx = true;
		} else {
			if (!uc->opts.strict) uc->version = 7400;
			ufbxi_check_msg(uc->version > 0, "Not an FBX file");
		}
	}

	return 1;
}

ufbxi_nodiscard static int ufbxi_parse_toplevel_child_imp(ufbxi_context *uc, ufbxi_parse_state state, ufbxi_buf *buf, bool *p_end)
{
	if (uc->from_ascii) {
		ufbxi_check(ufbxi_ascii_parse_node(uc, 0, state, p_end, buf, true));
	} else {
		ufbxi_check(ufbxi_binary_parse_node(uc, 0, state, p_end, buf, true));
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_parse_toplevel(ufbxi_context *uc, const char *name)
{
	ufbxi_for(ufbxi_node, node, uc->top_nodes, uc->top_nodes_len) {
		if (node->name == name) {
			uc->top_node = node;
			uc->top_child_index = 0;
			return 1;
		}
	}

	// Reached end and not found in cache
	if (uc->parsed_to_end) {
		uc->top_node = NULL;
		uc->top_child_index = 0;
		return 1;
	}

	for (;;) {
		// Parse the next top-level node
		bool end = false;
		if (uc->from_ascii) {
			ufbxi_check(ufbxi_ascii_parse_node(uc, 0, UFBXI_PARSE_ROOT, &end, &uc->tmp, false));
		} else {
			ufbxi_check(ufbxi_binary_parse_node(uc, 0, UFBXI_PARSE_ROOT, &end, &uc->tmp, false));
		}

		// Top-level node not found
		if (end) {
			uc->top_node = NULL;
			uc->top_child_index = 0;
			uc->parsed_to_end = true;
			if (uc->opts.retain_dom) {
				ufbxi_check(ufbxi_retain_toplevel(uc, NULL));
			}

			// Not needed anymore
			ufbxi_buf_free(&uc->tmp_parse);

			return 1;
		}

		uc->top_nodes_len++;
		ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->top_nodes, &uc->top_nodes_cap, uc->top_nodes_len));
		ufbxi_node *node = &uc->top_nodes[uc->top_nodes_len - 1];
		ufbxi_pop(&uc->tmp_stack, ufbxi_node, 1, node);
		if (uc->opts.retain_dom) {
			ufbxi_check(ufbxi_retain_toplevel(uc, node));
		}

		// Return if we parsed the right one
		if (node->name == name) {
			uc->top_node = node;
			uc->top_child_index = SIZE_MAX;
			return 1;
		}

		// If not we need to parse all the children of the node for later
		uint32_t num_children = 0;
		ufbxi_parse_state state = ufbxi_update_parse_state(UFBXI_PARSE_ROOT, node->name);
		if (uc->has_next_child) {
			for (;;) {
				ufbxi_check(ufbxi_parse_toplevel_child_imp(uc, state, &uc->tmp, &end));
				if (end) break;
				num_children++;
			}
		}

		node->num_children = num_children;
		node->children = ufbxi_push_pop(&uc->tmp, &uc->tmp_stack, ufbxi_node, num_children);
		ufbxi_check(node->children);

		if (uc->opts.retain_dom) {
			for (size_t i = 0; i < num_children; i++) {
				ufbxi_check(ufbxi_retain_toplevel_child(uc, &node->children[i]));
			}
		}
	}
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_parse_toplevel_child(ufbxi_context *uc, ufbxi_node **p_node, ufbxi_buf *tmp_buf)
{
	// Top-level node not found
	if (!uc->top_node) {
		*p_node = NULL;
		return 1;
	}

	if (uc->top_child_index == SIZE_MAX) {
		// Parse children on demand
		if (!tmp_buf) {
			ufbxi_buf_clear(&uc->tmp_parse);
		}
		bool end = false;
		ufbxi_parse_state state = ufbxi_update_parse_state(UFBXI_PARSE_ROOT, uc->top_node->name);
		ufbxi_check(ufbxi_parse_toplevel_child_imp(uc, state, tmp_buf ? tmp_buf : &uc->tmp_parse, &end));
		if (end) {
			*p_node = NULL;
		} else {
			// Parse to either reused `uc->top_child` or push if retaining to `tmp_buf`.
			ufbxi_node *dst = &uc->top_child;
			if (tmp_buf) {
				dst = ufbxi_push_zero(tmp_buf, ufbxi_node, 1);
				ufbxi_check(dst);
			}

			ufbxi_pop(&uc->tmp_stack, ufbxi_node, 1, dst);
			*p_node = dst;

			if (uc->opts.retain_dom) {
				ufbxi_check(ufbxi_retain_toplevel_child(uc, dst));
			}
		}
	} else {
		// Iterate already parsed nodes
		size_t child_index = uc->top_child_index;
		if (child_index == uc->top_node->num_children) {
			*p_node = NULL;
		} else {
			uc->top_child_index++;
			*p_node = &uc->top_node->children[child_index];
		}
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_parse_legacy_toplevel(ufbxi_context *uc)
{
	ufbx_assert(uc->top_nodes_len == 0);

	bool end = false;
	if (uc->from_ascii) {
		ufbxi_check(ufbxi_ascii_parse_node(uc, 0, UFBXI_PARSE_ROOT, &end, &uc->tmp, true));
	} else {
		ufbxi_check(ufbxi_binary_parse_node(uc, 0, UFBXI_PARSE_ROOT, &end, &uc->tmp, true));
	}

	// Top-level node not found
	if (end) {
		uc->top_node = NULL;
		uc->top_child_index = 0;
		uc->parsed_to_end = true;
		return 1;
	}

	ufbxi_pop(&uc->tmp_stack, ufbxi_node, 1, &uc->legacy_node);
	uc->top_child_index = 0;
	uc->top_node = &uc->legacy_node;

	if (uc->opts.retain_dom) {
		ufbxi_check(ufbxi_retain_toplevel(uc, &uc->legacy_node));
	}

	return 1;
}

// -- Setup

ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_strings(ufbxi_context *uc)
{
#if defined(UFBX_REGRESSION)
	ufbx_string reg_prev = ufbx_empty_string;
#endif

	// Push all the global 'ufbxi_*' strings into the pool without copying them
	// This allows us to compare name pointers to the global values
	ufbxi_for(const ufbx_string, str, ufbxi_strings, ufbxi_arraycount(ufbxi_strings)) {
#if defined(UFBX_REGRESSION)
		ufbx_assert(strlen(str->data) == str->length);
		ufbx_assert(ufbxi_str_less(reg_prev, *str));
		reg_prev = *str;
#endif
		ufbxi_check(ufbxi_push_string_imp(&uc->string_pool, str->data, str->length, NULL, false, true));
	}

	return 1;
}

typedef struct {
	const char *name;
	ufbx_prop_type type;
} ufbxi_prop_type_name;

static const ufbxi_prop_type_name ufbxi_prop_type_names[] = {
	{ "Boolean", UFBX_PROP_BOOLEAN },
	{ "bool", UFBX_PROP_BOOLEAN },
	{ "Bool", UFBX_PROP_BOOLEAN },
	{ "Integer", UFBX_PROP_INTEGER },
	{ "int", UFBX_PROP_INTEGER },
	{ "enum", UFBX_PROP_INTEGER },
	{ "Visibility", UFBX_PROP_INTEGER },
	{ "Visibility Inheritance", UFBX_PROP_INTEGER },
	{ "KTime", UFBX_PROP_INTEGER },
	{ "Number", UFBX_PROP_NUMBER },
	{ "double", UFBX_PROP_NUMBER },
	{ "Real", UFBX_PROP_NUMBER },
	{ "Float", UFBX_PROP_NUMBER },
	{ "Intensity", UFBX_PROP_NUMBER },
	{ "Vector", UFBX_PROP_VECTOR },
	{ "Vector3D", UFBX_PROP_VECTOR },
	{ "Color", UFBX_PROP_COLOR },
	{ "ColorAndAlpha", UFBX_PROP_COLOR_WITH_ALPHA },
	{ "ColorRGB", UFBX_PROP_COLOR },
	{ "String", UFBX_PROP_STRING },
	{ "KString", UFBX_PROP_STRING },
	{ "object", UFBX_PROP_STRING },
	{ "DateTime", UFBX_PROP_DATE_TIME },
	{ "Lcl Translation", UFBX_PROP_TRANSLATION },
	{ "Lcl Rotation", UFBX_PROP_ROTATION },
	{ "Lcl Scaling", UFBX_PROP_SCALING },
	{ "Distance", UFBX_PROP_DISTANCE },
	{ "Compound", UFBX_PROP_COMPOUND },
	{ "Blob", UFBX_PROP_BLOB },
	{ "Reference", UFBX_PROP_REFERENCE },
};

static ufbx_prop_type ufbxi_get_prop_type(ufbxi_context *uc, const char *name)
{
	uint32_t hash = ufbxi_hash_ptr(name);
	ufbxi_prop_type_name *entry = ufbxi_map_find(&uc->prop_type_map, ufbxi_prop_type_name, hash, &name);
	if (entry) {
		return entry->type;
	}
	return UFBX_PROP_UNKNOWN;
}

static ufbxi_noinline ufbx_prop *ufbxi_find_prop_with_key(const ufbx_props *props, const char *name, uint32_t key)
{
	do {
		ufbx_prop *prop_data = props->props.data;
		size_t begin = 0;
		size_t end = props->props.count;
		while (end - begin >= 16) {
			size_t mid = (begin + end) >> 1;
			const ufbx_prop *p = &prop_data[mid];
			if (p->_internal_key < key) {
				begin = mid + 1;
			} else {
				end = mid;
			}
		}

		end = props->props.count;
		for (; begin < end; begin++) {
			const ufbx_prop *p = &prop_data[begin];
			if (p->_internal_key > key) break;
			if (p->name.data == name && (p->flags & UFBX_PROP_FLAG_NO_VALUE) == 0) {
				return (ufbx_prop*)p;
			}
		}

		props = props->defaults;
	} while (props);

	return NULL;
}

typedef struct {
	const char *key;
	ufbx_texture_file *file;
} ufbxi_texture_file_entry;

#define ufbxi_find_prop(props, name) ufbxi_find_prop_with_key((props), (name), \
	((uint32_t)(uint8_t)name[0] << 24u) | ((uint32_t)(uint8_t)name[1] << 16u) | \
	((uint32_t)(uint8_t)name[2] << 8u) | (uint32_t)(uint8_t)name[3])

static ufbxi_forceinline ufbx_real ufbxi_find_real(const ufbx_props *props, const char *name, ufbx_real def)
{
	ufbx_prop *prop = ufbxi_find_prop(props, name);
	if (prop) {
		return prop->value_real;
	} else {
		return def;
	}
}

static ufbxi_forceinline ufbx_vec3 ufbxi_find_vec3(const ufbx_props *props, const char *name, ufbx_real def_x, ufbx_real def_y, ufbx_real def_z)
{
	ufbx_prop *prop = ufbxi_find_prop(props, name);
	if (prop) {
		return prop->value_vec3;
	} else {
		ufbx_vec3 def = { def_x, def_y, def_z };
		return def;
	}
}

static ufbxi_forceinline int64_t ufbxi_find_int(const ufbx_props *props, const char *name, int64_t def)
{
	ufbx_prop *prop = ufbxi_find_prop(props, name);
	if (prop) {
		return prop->value_int;
	} else {
		return def;
	}
}

static ufbxi_forceinline int64_t ufbxi_find_enum(const ufbx_props *props, const char *name, int64_t def, int64_t max_value)
{
	ufbx_prop *prop = ufbxi_find_prop(props, name);
	if (prop) {
		int64_t value = prop->value_int;
		if (value >= 0 && value <= max_value) {
			return value;
		} else {
			return def;
		}
	} else {
		return def;
	}
}

ufbxi_noinline static bool ufbxi_matrix_all_zero(const ufbx_matrix *matrix)
{
	for (size_t i = 0; i < 12; i++) {
		if (matrix->v[i] != 0.0f) return false;
	}
	return true;
}

static ufbxi_forceinline bool ufbxi_is_vec3_zero(ufbx_vec3 v)
{
	return (v.x == 0.0) & (v.y == 0.0) & (v.z == 0.0);
}

static ufbxi_forceinline bool ufbxi_is_vec4_zero(ufbx_vec4 v)
{
	return (v.x == 0.0) & (v.y == 0.0) & (v.z == 0.0);
}

static ufbxi_forceinline bool ufbxi_is_vec3_one(ufbx_vec3 v)
{
	return (v.x == 1.0) & (v.y == 1.0) & (v.z == 1.0);
}

static ufbxi_forceinline bool ufbxi_is_quat_identity(ufbx_quat v)
{
	return (v.x == 0.0) & (v.y == 0.0) & (v.z == 0.0) & (v.w == 1.0);
}

static ufbxi_noinline bool ufbxi_is_transform_identity(const ufbx_transform *t)
{
	return (bool)((int)ufbxi_is_vec3_zero(t->translation) & (int)ufbxi_is_quat_identity(t->rotation) & (int)ufbxi_is_vec3_one(t->scale));
}

static ufbxi_forceinline uint32_t ufbxi_get_name_key(const char *name, size_t len)
{
	uint32_t key = 0;
	if (len >= 4) {
		key = (uint32_t)(uint8_t)name[0]<<24 | (uint32_t)(uint8_t)name[1]<<16
			| (uint32_t)(uint8_t)name[2]<<8 | (uint32_t)(uint8_t)name[3];
	} else {
		for (size_t i = 0; i < 4; i++) {
			key <<= 8;
			if (i < len) key |= (uint8_t)name[i];
		}
	}
	return key;
}

static ufbxi_forceinline uint32_t ufbxi_get_name_key_c(const char *name)
{
	if (name[0] == '\0') return 0;
	if (name[1] == '\0') return (uint32_t)(uint8_t)name[0]<<24;
	if (name[2] == '\0') return (uint32_t)(uint8_t)name[0]<<24 | (uint32_t)(uint8_t)name[1]<<16;
	return (uint32_t)(uint8_t)name[0]<<24 | (uint32_t)(uint8_t)name[1]<<16
		| (uint32_t)(uint8_t)name[2]<<8 | (uint32_t)(uint8_t)name[3];
}

static ufbxi_forceinline bool ufbxi_name_key_less(ufbx_prop *prop, const char *data, size_t name_len, uint32_t key)
{
	if (prop->_internal_key < key) return true;
	if (prop->_internal_key > key) return false;

	size_t prop_len = prop->name.length;
	size_t len = ufbxi_min_sz(prop_len, name_len);
	int cmp = memcmp(prop->name.data, data, len);
	if (cmp != 0) return cmp < 0;
	return prop_len < name_len;
}

static const char *const ufbxi_node_prop_names[] = {
	"AxisLen",
	"DefaultAttributeIndex",
	"Freeze",
	"GeometricRotation",
	"GeometricScaling",
	"GeometricTranslation",
	"InheritType",
	"LODBox",
	"Lcl Rotation",
	"Lcl Scaling",
	"Lcl Translation",
	"LookAtProperty",
	"MaxDampRangeX",
	"MaxDampRangeY",
	"MaxDampRangeZ",
	"MaxDampStrengthX",
	"MaxDampStrengthY",
	"MaxDampStrengthZ",
	"MinDampRangeX",
	"MinDampRangeY",
	"MinDampRangeZ",
	"MinDampStrengthX",
	"MinDampStrengthY",
	"MinDampStrengthZ",
	"NegativePercentShapeSupport",
	"PostRotation",
	"PreRotation",
	"PreferedAngleX",
	"PreferedAngleY",
	"PreferedAngleZ",
	"QuaternionInterpolate",
	"RotationActive",
	"RotationMax",
	"RotationMaxX",
	"RotationMaxY",
	"RotationMaxZ",
	"RotationMin",
	"RotationMinX",
	"RotationMinY",
	"RotationMinZ",
	"RotationOffset",
	"RotationOrder",
	"RotationPivot",
	"RotationSpaceForLimitOnly",
	"RotationStiffnessX",
	"RotationStiffnessY",
	"RotationStiffnessZ",
	"ScalingActive",
	"ScalingMax",
	"ScalingMaxX",
	"ScalingMaxY",
	"ScalingMaxZ",
	"ScalingMin",
	"ScalingMinX",
	"ScalingMinY",
	"ScalingMinZ",
	"ScalingOffset",
	"ScalingPivot",
	"Show",
	"TranslationActive",
	"TranslationMax",
	"TranslationMaxX",
	"TranslationMaxY",
	"TranslationMaxZ",
	"TranslationMin",
	"TranslationMinX",
	"TranslationMinY",
	"TranslationMinZ",
	"UpVectorProperty",
	"Visibility Inheritance",
	"Visibility",
	"notes",
};

ufbxi_nodiscard static ufbxi_noinline int ufbxi_init_node_prop_names(ufbxi_context *uc)
{
	ufbxi_check(ufbxi_map_grow(&uc->node_prop_set, const char*, ufbxi_arraycount(ufbxi_node_prop_names)));
	for (size_t i = 0; i < ufbxi_arraycount(ufbxi_node_prop_names); i++) {
		const char *name = ufbxi_node_prop_names[i];
		const char *pooled = ufbxi_push_string_imp(&uc->string_pool, name, strlen(name), NULL, false, true);
		ufbxi_check(pooled);
		uint32_t hash = ufbxi_hash_ptr(pooled);
		const char **entry = ufbxi_map_insert(&uc->node_prop_set, const char*, hash, &pooled);
		ufbxi_check(entry);
		*entry = pooled;
	}

	return 1;
}

static bool ufbxi_is_node_property(ufbxi_context *uc, const char *name)
{
	// You need to call `ufbxi_init_node_prop_names()` before calling this
	ufbx_assert(uc->node_prop_set.size > 0);

	uint32_t hash = ufbxi_hash_ptr(name);
	const char **entry = ufbxi_map_find(&uc->node_prop_set, const char*, hash, &name);
	return entry != NULL;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_maps(ufbxi_context *uc)
{
	ufbxi_check(ufbxi_map_grow(&uc->prop_type_map, ufbxi_prop_type_name, ufbxi_arraycount(ufbxi_prop_type_names)));
	ufbxi_for(const ufbxi_prop_type_name, name, ufbxi_prop_type_names, ufbxi_arraycount(ufbxi_prop_type_names)) {
		const char *pooled = ufbxi_push_string_imp(&uc->string_pool, name->name, strlen(name->name), NULL, false, true);
		ufbxi_check(pooled);
		uint32_t hash = ufbxi_hash_ptr(pooled);
		ufbxi_prop_type_name *entry = ufbxi_map_insert(&uc->prop_type_map, ufbxi_prop_type_name, hash, &pooled);
		ufbxi_check(entry);
		entry->type = name->type;
		entry->name = pooled;
	}

	return 1;
}

// -- Reading the parsed data

ufbxi_noinline static void ufbxi_decode_base64(char *dst, const char *src, size_t src_length)
{
	uint8_t table[256] = { 0 };
	for (char c = 'A'; c <= 'Z'; c++) table[(size_t)c] = (uint8_t)(c - 'A');
	for (char c = 'a'; c <= 'z'; c++) table[(size_t)c] = (uint8_t)(26 + (c - 'a'));
	for (char c = '0'; c <= '9'; c++) table[(size_t)c] = (uint8_t)(52 + (c - '0'));
	table[(size_t)'+'] = 62;
	table[(size_t)'/'] = 63;

	for (size_t i = 0; i + 4 <= src_length; i += 4) {
		uint32_t a = table[(size_t)(uint8_t)src[i + 0]];
		uint32_t b = table[(size_t)(uint8_t)src[i + 1]];
		uint32_t c = table[(size_t)(uint8_t)src[i + 2]];
		uint32_t d = table[(size_t)(uint8_t)src[i + 3]];

		dst[0] = (char)(uint8_t)(a << 2 | b >> 4);
		dst[1] = (char)(uint8_t)(b << 4 | c >> 2);
		dst[2] = (char)(uint8_t)(c << 6 | d);
		dst += 3;
	}
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_embedded_blob(ufbxi_context *uc, ufbx_blob *dst_blob, ufbxi_node *node)
{
	if (!node) return 1;

	ufbxi_value_array *content_arr = ufbxi_get_array(node, 'C');
	if (content_arr && content_arr->size > 0) {
		ufbx_string content;
		size_t num_parts = content_arr->size;
		ufbx_string *parts = (ufbx_string*)content_arr->data;
		if (num_parts == 1) {
			content = parts[0];
		} else {
			size_t total_size = 0;
			ufbxi_for(ufbx_string, part, parts, num_parts) {
				total_size += part->length;
			}
			ufbxi_buf *dst_buf = uc->from_ascii ? &uc->tmp_parse : &uc->result;
			char *dst = ufbxi_push(dst_buf, char, total_size);
			ufbxi_check(dst);
			content.data = dst;
			content.length = total_size;
			ufbxi_for(ufbx_string, part, parts, num_parts) {
				memcpy(dst, part->data, part->length);
				dst += part->length;
			}
		}

		if (uc->from_ascii) {
			if (content.length % 4 == 0) {
				size_t padding = 0;
				while (padding < 2 && padding < content.length && content.data[content.length - 1 - padding] == '=') {
					padding++;
				}

				dst_blob->size = content.length / 4 * 3 - padding;
				dst_blob->data = ufbxi_push(&uc->result, char, dst_blob->size + 3);
				ufbxi_check(dst_blob->data);

				ufbxi_decode_base64((char*)dst_blob->data, content.data, content.length);
			}
		} else {
			dst_blob->data = content.data;
			dst_blob->size = content.length;
		}
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_property(ufbxi_context *uc, ufbxi_node *node, ufbx_prop *prop, int version)
{
	const char *type_str = NULL, *subtype_str = NULL;
	ufbxi_check(ufbxi_get_val2(node, "SC", &prop->name, (char**)&type_str));
	uint32_t val_ix = 2;
	if (version == 70) {
		ufbxi_check(ufbxi_get_val_at(node, val_ix++, 'C', (char**)&subtype_str));
	}

	uint32_t flags = 0;
	prop->_internal_key = ufbxi_get_name_key(prop->name.data, prop->name.length);

	ufbx_string flags_str;
	if (ufbxi_get_val_at(node, val_ix++, 'S', &flags_str)) {
		for (size_t i = 0; i < flags_str.length; i++) {
			char next = i + 1 < flags_str.length ? flags_str.data[i + 1] : '0';
			switch (flags_str.data[i]) {
			case 'A': flags |= UFBX_PROP_FLAG_ANIMATABLE; break;
			case 'U': flags |= UFBX_PROP_FLAG_USER_DEFINED; break;
			case 'H': flags |= UFBX_PROP_FLAG_HIDDEN; break;
			case 'L': flags |= ((uint32_t)(next - '0') & 0xf) << 4; break; // UFBX_PROP_FLAG_LOCK_*
			case 'M': flags |= ((uint32_t)(next - '0') & 0xf) << 8; break; // UFBX_PROP_FLAG_MUTE_*
			default: break; // Ignore unknown flags
			}
		}
	}

	prop->type = ufbxi_get_prop_type(uc, type_str);
	if (prop->type == UFBX_PROP_UNKNOWN && subtype_str) {
		prop->type = ufbxi_get_prop_type(uc, subtype_str);
	}

	if (ufbxi_get_val_at(node, val_ix, 'L', &prop->value_int)) {
		flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_INT;
	}

	size_t real_ix;
	for (real_ix = 0; real_ix < 4; real_ix++) {
		if (!ufbxi_get_val_at(node, val_ix + real_ix, 'R', &prop->value_real_arr[real_ix])) break;
	}
	if (real_ix > 0) {
		flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_REAL << (real_ix - 1);
	}

	// Distance properties have a string unit _after_ the real value, eg. `10, "cm"`
	if (prop->type == UFBX_PROP_DISTANCE) {
		val_ix++;
	}

	if (ufbxi_get_val_at(node, val_ix, 'S', &prop->value_str)) {
		if (prop->value_str.length > 0) {
			ufbxi_ignore(ufbxi_get_val_at(node, val_ix, 'b', &prop->value_blob));
		}
		flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_STR;
	} else {
		prop->value_str = ufbx_empty_string;
	}

	// Very unlikely, seems to only exist in some "non standard" FBX files
	if (node->num_children > 0) {
		ufbxi_node *binary = ufbxi_find_child(node, ufbxi_BinaryData);
		ufbxi_check(ufbxi_read_embedded_blob(uc, &prop->value_blob, binary));
		flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_BLOB;
	}

	prop->flags = (ufbx_prop_flags)flags;

	return 1;
}

static ufbxi_forceinline bool ufbxi_prop_less(ufbx_prop *a, ufbx_prop *b)
{
	if (a->_internal_key < b->_internal_key) return true;
	if (a->_internal_key > b->_internal_key) return false;
	return strcmp(a->name.data, b->name.data) < 0;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_properties(ufbxi_context *uc, ufbx_prop *props, size_t count)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_prop)));
	ufbxi_macro_stable_sort(ufbx_prop, 32, props, uc->tmp_arr, count, ( ufbxi_prop_less(a, b) ));
	return 1;
}

ufbxi_noinline static void ufbxi_deduplicate_properties(ufbx_prop_list *list)
{
	if (list->count >= 2) {
		ufbx_prop *ps = list->data;
		size_t dst = 0, src = 0, end = list->count;
		while (src < end) {
			if (src + 1 < end && ps[src].name.data == ps[src + 1].name.data) {
				src++;
			} else if (dst != src) {
				ps[dst++] = ps[src++];
			} else {
				dst++; src++;
			}
		}
		list->count = dst;
	}
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_properties(ufbxi_context *uc, ufbxi_node *parent, ufbx_props *props)
{
	props->defaults = NULL;

	int version = 70;
	ufbxi_node *node = ufbxi_find_child(parent, ufbxi_Properties70);
	if (!node) {
		node = ufbxi_find_child(parent, ufbxi_Properties60);
		if (!node) {
			// No properties found, not an error
			props->props.data = NULL;
			props->props.count = 0;
			return 1;
		}
		version = 60;
	}

	props->props.data = ufbxi_push_zero(&uc->result, ufbx_prop, node->num_children);
	props->props.count = node->num_children;
	ufbxi_check(props->props.data);

	for (size_t i = 0; i < props->props.count; i++) {
		ufbxi_check(ufbxi_read_property(uc, &node->children[i], &props->props.data[i], version));
	}

	ufbxi_check(ufbxi_sort_properties(uc, props->props.data, props->props.count));
	ufbxi_deduplicate_properties(&props->props);

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_thumbnail(ufbxi_context *uc, ufbxi_node *node, ufbx_thumbnail *thumbnail)
{
	ufbxi_check(ufbxi_read_properties(uc, node, &thumbnail->props));

	int64_t custom_width = ufbx_find_int(&thumbnail->props, "CustomWidth", 0);
	int64_t custom_height = ufbx_find_int(&thumbnail->props, "CustomHeight", 0);

	int32_t format;
	ufbxi_node *format_node = ufbxi_find_child_strcmp(node, "Format");
	if (format_node && ufbxi_get_val1(format_node, "I", &format)) {
		if (format >= 0 && format + 1 < UFBX_THUMBNAIL_FORMAT_COUNT) {
			thumbnail->format = (ufbx_thumbnail_format)(format + 1);
		}
	}

	int32_t size;
	if (ufbxi_find_val1(node, ufbxi_Size, "I", &size)) {
		if (size > 0) {
			thumbnail->width = (uint32_t)size;
			thumbnail->height = (uint32_t)size;
		} else if (size < 0 && custom_width > 0 && custom_height > 0) {
			thumbnail->width = (uint32_t)custom_width;
			thumbnail->height = (uint32_t)custom_height;
		}
	}

	ufbxi_value_array *data_arr = ufbxi_find_array(node, ufbxi_ImageData, 'c');
	if (data_arr) {
		thumbnail->data.data = data_arr->data;
		thumbnail->data.size = data_arr->size;
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_scene_info(ufbxi_context *uc, ufbxi_node *node)
{
	ufbxi_check(ufbxi_read_properties(uc, node, &uc->scene.metadata.scene_props));

	ufbxi_node *thumbnail = ufbxi_find_child(node, ufbxi_Thumbnail);
	if (thumbnail) {
		ufbxi_check(ufbxi_read_thumbnail(uc, thumbnail, &uc->scene.metadata.thumbnail));
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_header_extension(ufbxi_context *uc)
{
	// TODO: Read TCDefinition and adjust timestamps
	uc->ktime_sec = 46186158000;
	uc->ktime_sec_double = (double)uc->ktime_sec;

	for (;;) {
		ufbxi_node *child;
		ufbxi_check(ufbxi_parse_toplevel_child(uc, &child, NULL));
		if (!child) break;

		if (child->name == ufbxi_Creator) {
			ufbxi_ignore(ufbxi_get_val1(child, "S", &uc->scene.metadata.creator));
		}

		if (uc->version < 6000 && child->name == ufbxi_FBXVersion) {
			int32_t version;
			if (ufbxi_get_val1(child, "I", &version)) {
				if (version > 0 && version < 6000 && (uint32_t)version > uc->version) {
					uc->version = (uint32_t)version;
				}
			}
		}

		if (child->name == ufbxi_SceneInfo) {
			ufbxi_check(ufbxi_read_scene_info(uc, child));
		}

	}

	return 1;
}

static bool ufbxi_match_version_string(const char *fmt, ufbx_string str, uint32_t *p_version)
{
	size_t num_ix = 0;
	size_t pos = 0;
	while (*fmt) {
		char c = *fmt++;
		if (c >= 'a' && c <= 'z') {
			if (pos >= str.length) return false;
			char s = str.data[pos];
			if (s != c && (int)s + (int)('a' - 'A') != (int)c) return false;
			pos++;
		} else if (c == ' ') {
			while (pos < str.length) {
				char s = str.data[pos];
				if (s != ' ' && s != '\t') break;
				pos++;
			}
		} else if (c == '-') {
			while (pos < str.length) {
				char s = str.data[pos];
				if (s == '-') break;
				pos++;
			}
			if (pos >= str.length) return false;
			pos++;
		} else if (c == '/' || c == '.' || c == '(' || c == ')') {
			if (pos >= str.length) return false;
			if (str.data[pos] != c) return false;
			pos++;
		} else if (c == '?') {
			uint32_t num = 0;
			size_t len = 0;
			while (pos < str.length) {
				char s = str.data[pos];
				if (!(s >= '0' && s <= '9')) break;
				num = num*10 + (uint32_t)(s - '0');
				pos++;
				len++;
			}
			if (len == 0) return false;
			p_version[num_ix++] = num;
		} else {
			ufbxi_unreachable("Unhandled match character");
		}
	}

	return true;
}

ufbxi_nodiscard static int ufbxi_match_exporter(ufbxi_context *uc)
{
	ufbx_string creator = uc->scene.metadata.creator;
	uint32_t version[3] = { 0 };
	if (ufbxi_match_version_string("blender-- ?.?.?", creator, version)) {
		uc->exporter = UFBX_EXPORTER_BLENDER_BINARY;
		uc->exporter_version = ufbx_pack_version(version[0], version[1], version[2]);
	} else if (ufbxi_match_version_string("blender- ?.?", creator, version)) {
		uc->exporter = UFBX_EXPORTER_BLENDER_BINARY;
		uc->exporter_version = ufbx_pack_version(version[0], version[1], 0);
	} else if (ufbxi_match_version_string("blender version ?.?", creator, version)) {
		uc->exporter = UFBX_EXPORTER_BLENDER_ASCII;
		uc->exporter_version = ufbx_pack_version(version[0], version[1], 0);
	} else if (ufbxi_match_version_string("fbx sdk/fbx plugins version ?.?", creator, version)) {
		uc->exporter = UFBX_EXPORTER_FBX_SDK;
		uc->exporter_version = ufbx_pack_version(version[0], version[1], 0);
	} else if (ufbxi_match_version_string("fbx sdk/fbx plugins build ?", creator, version)) {
		uc->exporter = UFBX_EXPORTER_FBX_SDK;
		uc->exporter_version = ufbx_pack_version(version[0]/10000u, version[0]/100u%100u, version[0]%100u);
	} else if (ufbxi_match_version_string("motionbuilder version ?.?", creator, version)) {
		uc->exporter = UFBX_EXPORTER_MOTION_BUILDER;
		uc->exporter_version = ufbx_pack_version(version[0], version[1], 0);
	} else if (ufbxi_match_version_string("motionbuilder/mocap/online version ?.?", creator, version)) {
		uc->exporter = UFBX_EXPORTER_MOTION_BUILDER;
		uc->exporter_version = ufbx_pack_version(version[0], version[1], 0);
	}

	uc->scene.metadata.exporter = uc->exporter;
	uc->scene.metadata.exporter_version = uc->exporter_version;

	// Un-detect the exporter in `ufbxi_context` to disable special cases
	if (uc->opts.disable_quirks) {
		uc->exporter = UFBX_EXPORTER_UNKNOWN;
		uc->exporter_version = 0;
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_document(ufbxi_context *uc)
{
	bool found_root_id = 0;

	for (;;) {
		ufbxi_node *child;
		ufbxi_check(ufbxi_parse_toplevel_child(uc, &child, NULL));
		if (!child) break;

		if (child->name == ufbxi_Document && !found_root_id) {
			// Post-7000: Try to find the first document node and root ID.
			// TODO: Multiple documents / roots?
			if (ufbxi_find_val1(child, ufbxi_RootNode, "L", &uc->root_id)) {
				found_root_id = true;
			}
		}
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_definitions(ufbxi_context *uc)
{
	for (;;) {
		ufbxi_node *object;
		ufbxi_check(ufbxi_parse_toplevel_child(uc, &object, NULL));
		if (!object) break;

		if (object->name != ufbxi_ObjectType) continue;

		ufbxi_template *tmpl = ufbxi_push_zero(&uc->tmp_stack, ufbxi_template, 1);
		uc->num_templates++;
		ufbxi_check(tmpl);
		ufbxi_check(ufbxi_get_val1(object, "C", (char**)&tmpl->type));

		// Pre-7000 FBX versions don't have property templates, they just have
		// the object counts by themselves.
		ufbxi_node *props = ufbxi_find_child(object, ufbxi_PropertyTemplate);
		if (props) {
			ufbxi_check(ufbxi_get_val1(props, "S", &tmpl->sub_type));

			// Remove the "Fbx" prefix from sub-types, remember to re-intern!
			if (tmpl->sub_type.length > 3 && !strncmp(tmpl->sub_type.data, "Fbx", 3)) {
				tmpl->sub_type.data += 3;
				tmpl->sub_type.length -= 3;

				// HACK: LOD groups use LODGroup for Template, LodGroup for Object?
				if (tmpl->sub_type.length == 8 && !memcmp(tmpl->sub_type.data, "LODGroup", 8)) {
					tmpl->sub_type.data = "LodGroup";
				}

				ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &tmpl->sub_type, false));
			}

			ufbxi_check(ufbxi_read_properties(uc, props, &tmpl->props));
		}
	}

	// TODO: Preserve only the `props` part of the templates
	uc->templates = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbxi_template, uc->num_templates);
	ufbxi_check(uc->templates);

	return 1;
}

ufbxi_nodiscard static ufbx_props *ufbxi_find_template(ufbxi_context *uc, const char *name, const char *sub_type)
{
	// TODO: Binary search
	ufbxi_for(ufbxi_template, tmpl, uc->templates, uc->num_templates) {
		if (tmpl->type == name) {

			// Check that sub_type matches unless the type is Material, Model, AnimationStack, AnimationLayer.
			// Those match to all sub-types.
			if (tmpl->type != ufbxi_Material && tmpl->type != ufbxi_Model
				&& tmpl->type != ufbxi_AnimationStack && tmpl->type != ufbxi_AnimationLayer) {
				if (tmpl->sub_type.data != sub_type) {
					return NULL;
				}
			}

			if (tmpl->props.props.count > 0) {
				return &tmpl->props;
			} else {
				return NULL;
			}
		}
	}
	return NULL;
}

// Name ID categories
#define UFBXI_SYNTHETIC_ID_BIT UINT64_C(0x8000000000000000)

ufbx_static_assert(uptr_size, sizeof(uintptr_t) <= sizeof(uint64_t));

static ufbxi_forceinline uint64_t ufbxi_synthetic_id_from_pointer(const void *ptr)
{
	uintptr_t uptr = (uintptr_t)ptr;
	ufbx_assert((uptr & 0x1) == 0);
	return (uptr >> 1u) | UFBXI_SYNTHETIC_ID_BIT;
}

static ufbxi_forceinline uint64_t ufbxi_synthetic_id_from_string(const char *str)
{
	uintptr_t uptr = (uintptr_t)str;
	uptr &= ~(uintptr_t)1;
	return (uptr >> 1u) | UFBXI_SYNTHETIC_ID_BIT;
}

static ufbxi_noinline int ufbxi_push_synthetic_id(ufbxi_context *uc, uint64_t *p_dst)
{
	void *ptr = ufbxi_push_size(&uc->tmp, 8, 1);
	ufbxi_check(ptr);
	*p_dst = ufbxi_synthetic_id_from_pointer(ptr);
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_split_type_and_name(ufbxi_context *uc, ufbx_string type_and_name, ufbx_string *type, ufbx_string *name)
{
	// Name and type are packed in a single property as Type::Name (in ASCII)
	// or Name\x00\x01Type (in binary)
	const char *sep = uc->from_ascii ? "::" : "\x00\x01";
	size_t type_end = 2;
	for (; type_end <= type_and_name.length; type_end++) {
		const char *ch = type_and_name.data + type_end - 2;
		if (ch[0] == sep[0] && ch[1] == sep[1]) break;
	}

	// ???: ASCII and binary store type and name in different order
	if (type_end <= type_and_name.length) {
		if (uc->from_ascii) {
			name->data = type_and_name.data + type_end;
			name->length = type_and_name.length - type_end;
			type->data = type_and_name.data;
			type->length = type_end - 2;
		} else {
			name->data = type_and_name.data;
			name->length = type_end - 2;
			type->data = type_and_name.data + type_end;
			type->length = type_and_name.length - type_end;
		}
	} else {
		*name = type_and_name;
		type->data = ufbxi_empty_char;
		type->length = 0;
	}

	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, type, false));
	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, name, false));

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_insert_fbx_id(ufbxi_context *uc, uint64_t fbx_id, uint32_t element_id)
{
	uint32_t hash = ufbxi_hash64(fbx_id);
	ufbxi_fbx_id_entry *entry = ufbxi_map_find(&uc->fbx_id_map, ufbxi_fbx_id_entry, hash, &fbx_id);

	if (!entry) {
		entry = ufbxi_map_insert(&uc->fbx_id_map, ufbxi_fbx_id_entry, hash, &fbx_id);
		ufbxi_check(entry);
		entry->fbx_id = fbx_id;
		entry->element_id = element_id;
		entry->user_id = 0;
	} else {
		ufbxi_check(ufbxi_warnf(UFBX_WARNING_DUPLICATE_OBJECT_ID, "Duplicate object ID"));
	}

	return 1;
}

static ufbxi_noinline ufbxi_fbx_id_entry *ufbxi_find_fbx_id(ufbxi_context *uc, uint64_t fbx_id)
{
	uint32_t hash = ufbxi_hash64(fbx_id);
	return ufbxi_map_find(&uc->fbx_id_map, ufbxi_fbx_id_entry, hash, &fbx_id);
}

static ufbxi_forceinline bool ufbxi_fbx_id_exists(ufbxi_context *uc, uint64_t fbx_id)
{
	return ufbxi_find_fbx_id(uc, fbx_id) != NULL;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_insert_fbx_attr(ufbxi_context *uc, uint64_t fbx_id, uint64_t attrib_fbx_id)
{
	uint32_t hash = ufbxi_hash64(fbx_id);
	ufbxi_fbx_attr_entry *entry = ufbxi_map_find(&uc->fbx_attr_map, ufbxi_fbx_attr_entry, hash, &fbx_id);
	// TODO: Strict / warn about duplicate objects

	if (!entry) {
		entry = ufbxi_map_insert(&uc->fbx_attr_map, ufbxi_fbx_attr_entry, hash, &fbx_id);
		ufbxi_check(entry);
		entry->node_fbx_id = fbx_id;
		entry->attr_fbx_id = attrib_fbx_id;
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static ufbx_element *ufbxi_push_element_size(ufbxi_context *uc, ufbxi_element_info *info, size_t size, ufbx_element_type type)
{
	size_t aligned_size = (size + 7u) & ~0x7u;

	uint32_t typed_id = (uint32_t)uc->tmp_typed_element_offsets[type].num_items;
	uint32_t element_id = uc->num_elements++;

	ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_typed_element_offsets[type], size_t, 1, &uc->tmp_element_byte_offset), NULL);
	ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_offsets, size_t, 1, &uc->tmp_element_byte_offset), NULL);
	ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_fbx_ids, uint64_t, 1, &info->fbx_id), NULL);
	uc->tmp_element_byte_offset += aligned_size;

	ufbx_element *elem = (ufbx_element*)ufbxi_push_zero(&uc->tmp_elements, uint64_t, aligned_size/8);
	ufbxi_check_return(elem, NULL);
	elem->type = type;
	elem->element_id = element_id;
	elem->typed_id = typed_id;
	elem->name = info->name;
	elem->props = info->props;
	elem->dom_node = info->dom_node;

	if (uc->p_element_id) {
		*uc->p_element_id = element_id;
	}

	ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_ptrs, ufbx_element*, 1, &elem), NULL);

	ufbxi_check_return(ufbxi_insert_fbx_id(uc, info->fbx_id, element_id), NULL);

	return elem;
}

ufbxi_nodiscard ufbxi_noinline static ufbx_element *ufbxi_push_synthetic_element_size(ufbxi_context *uc, uint64_t *p_fbx_id, ufbxi_node *node, const char *name, size_t size, ufbx_element_type type)
{
	size_t aligned_size = (size + 7u) & ~0x7u;

	uint32_t typed_id = (uint32_t)uc->tmp_typed_element_offsets[type].num_items;
	uint32_t element_id = uc->num_elements++;

	ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_typed_element_offsets[type], size_t, 1, &uc->tmp_element_byte_offset), NULL);
	ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_offsets, size_t, 1, &uc->tmp_element_byte_offset), NULL);
	uc->tmp_element_byte_offset += aligned_size;

	ufbx_element *elem = (ufbx_element*)ufbxi_push_zero(&uc->tmp_elements, uint64_t, aligned_size/8);
	ufbxi_check_return(elem, NULL);
	elem->type = type;
	elem->element_id = element_id;
	elem->typed_id = typed_id;
	elem->dom_node = ufbxi_get_dom_node(uc, node);
	if (name) {
		elem->name.data = name;
		elem->name.length = strlen(name);
	}

	ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_ptrs, ufbx_element*, 1, &elem), NULL);

	uint64_t fbx_id = ufbxi_synthetic_id_from_pointer(elem);
	*p_fbx_id = fbx_id;

	ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_fbx_ids, uint64_t, 1, &fbx_id), NULL);
	ufbxi_check_return(ufbxi_insert_fbx_id(uc, fbx_id, element_id), NULL);

	return elem;
}

#define ufbxi_push_element(uc, info, type_name, type_enum) ufbxi_maybe_null((type_name*)ufbxi_push_element_size((uc), (info), sizeof(type_name), (type_enum)))
#define ufbxi_push_synthetic_element(uc, p_fbx_id, node, name, type_name, type_enum) ufbxi_maybe_null((type_name*)ufbxi_push_synthetic_element_size((uc), (p_fbx_id), (node), (name), sizeof(type_name), (type_enum)))

ufbxi_nodiscard ufbxi_noinline static int ufbxi_connect_oo(ufbxi_context *uc, uint64_t src, uint64_t dst)
{
	ufbxi_tmp_connection *conn = ufbxi_push(&uc->tmp_connections, ufbxi_tmp_connection, 1);
	ufbxi_check(conn);
	conn->src = src;
	conn->dst = dst;
	conn->src_prop = conn->dst_prop = ufbx_empty_string;
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_connect_op(ufbxi_context *uc, uint64_t src, uint64_t dst, ufbx_string prop)
{
	ufbxi_tmp_connection *conn = ufbxi_push(&uc->tmp_connections, ufbxi_tmp_connection, 1);
	ufbxi_check(conn);
	conn->src = src;
	conn->dst = dst;
	conn->src_prop = ufbx_empty_string;
	conn->dst_prop = prop;
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_connect_pp(ufbxi_context *uc, uint64_t src, uint64_t dst, ufbx_string src_prop, ufbx_string dst_prop)
{
	ufbxi_tmp_connection *conn = ufbxi_push(&uc->tmp_connections, ufbxi_tmp_connection, 1);
	ufbxi_check(conn);
	conn->src = src;
	conn->dst = dst;
	conn->src_prop = src_prop;
	conn->dst_prop = dst_prop;
	return 1;
}

ufbxi_noinline static void ufbxi_init_synthetic_int_prop(ufbx_prop *dst, const char *name, int64_t value, ufbx_prop_type type)
{
	dst->type = type;
	dst->name.data = name;
	dst->name.length = strlen(name);
	dst->value_real = (ufbx_real)value;
	dst->flags = (ufbx_prop_flags)(UFBX_PROP_FLAG_SYNTHETIC|UFBX_PROP_FLAG_VALUE_REAL|UFBX_PROP_FLAG_VALUE_INT);
	dst->value_int = value;
	dst->value_str.data = ufbxi_empty_char;

	ufbxi_dev_assert(dst->name.length >= 4);
	dst->_internal_key = ufbxi_get_name_key(name, 4);
}

ufbxi_noinline static void ufbxi_init_synthetic_real_prop(ufbx_prop *dst, const char *name, ufbx_real value, ufbx_prop_type type)
{
	dst->type = type;
	dst->name.data = name;
	dst->name.length = strlen(name);
	dst->value_real = value;
	dst->flags = (ufbx_prop_flags)(UFBX_PROP_FLAG_SYNTHETIC|UFBX_PROP_FLAG_VALUE_REAL);
	dst->value_int = (int64_t)value;
	dst->value_str.data = ufbxi_empty_char;

	ufbxi_dev_assert(dst->name.length >= 4);
	dst->_internal_key = ufbxi_get_name_key(name, 4);
}

ufbxi_noinline static void ufbxi_init_synthetic_vec3_prop(ufbx_prop *dst, const char *name, const ufbx_vec3 *value, ufbx_prop_type type)
{
	dst->type = type;
	dst->name.data = name;
	dst->name.length = strlen(name);
	dst->value_vec3 = *value;
	dst->flags = (ufbx_prop_flags)(UFBX_PROP_FLAG_SYNTHETIC|UFBX_PROP_FLAG_VALUE_VEC3);
	dst->value_int = ufbxi_f64_to_i64(dst->value_real);
	dst->value_str.data = ufbxi_empty_char;

	ufbxi_dev_assert(dst->name.length >= 4);
	dst->_internal_key = ufbxi_get_name_key(name, 4);
}

ufbxi_noinline static void ufbxi_set_own_prop_vec3_uniform(ufbx_props *props, const char *name, ufbx_real value)
{
	ufbx_props local_props = *props;
	local_props.defaults = NULL;
	ufbx_prop *prop = ufbx_find_prop(&local_props, name);
	if (prop) {
		prop->value_vec4.x = value;
		prop->value_vec4.y = value;
		prop->value_vec4.z = value;
		prop->value_vec4.w = 0.0f;
		prop->value_int = (int64_t)value;
	}
}

typedef struct {
	uint32_t geometry_helper_id;
	uint32_t scale_helper_id;
} ufbxi_node_extra;

ufbxi_nodiscard ufbxi_noinline static int ufbxi_setup_geometry_transform_helper(ufbxi_context *uc, ufbx_node *node, uint64_t node_fbx_id)
{
	ufbx_vec3 geo_translation = ufbxi_find_vec3(&node->props, ufbxi_GeometricTranslation, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 geo_rotation = ufbxi_find_vec3(&node->props, ufbxi_GeometricRotation, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 geo_scaling = ufbxi_find_vec3(&node->props, ufbxi_GeometricScaling, 1.0f, 1.0f, 1.0f);
	if (!ufbxi_is_vec3_zero(geo_translation) || !ufbxi_is_vec3_zero(geo_rotation) || !ufbxi_is_vec3_one(geo_scaling)) {

		uint64_t geo_fbx_id;
		ufbx_node *geo_node = ufbxi_push_synthetic_element(uc, &geo_fbx_id, NULL, uc->opts.geometry_transform_helper_name.data, ufbx_node, UFBX_ELEMENT_NODE);
		ufbxi_check(geo_node);
		ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &geo_node->element.element_id));
		geo_node->element.dom_node = node->element.dom_node;

		ufbx_prop *props = ufbxi_push_zero(&uc->result, ufbx_prop, 3);
		ufbxi_check(props);
		ufbxi_init_synthetic_vec3_prop(&props[0], ufbxi_Lcl_Rotation, &geo_rotation, UFBX_PROP_ROTATION);
		ufbxi_init_synthetic_vec3_prop(&props[1], ufbxi_Lcl_Scaling, &geo_scaling, UFBX_PROP_SCALING);
		ufbxi_init_synthetic_vec3_prop(&props[2], ufbxi_Lcl_Translation, &geo_translation, UFBX_PROP_TRANSLATION);

		geo_node->props.props.data = props;
		geo_node->props.props.count = 3;

		node->has_geometry_transform = true;
		geo_node->is_geometry_transform_helper = true;

		ufbxi_check(ufbxi_connect_oo(uc, geo_fbx_id, node_fbx_id));
		uc->has_geometry_transform_nodes = true;

		ufbxi_node_extra *extra = ufbxi_push_element_extra(uc, node->element_id, ufbxi_node_extra);
		ufbxi_check(extra);
		extra->geometry_helper_id = geo_node->element_id;
	}

	return 1;
}

typedef struct {
	const char *name;
	ufbx_vec3 default_value;
} ufbxi_scale_helper_prop;

static const ufbxi_scale_helper_prop ufbxi_scale_helper_props[] = {
	{ ufbxi_GeometricRotation, { 0.0f, 0.0f, 0.0f } },
	{ ufbxi_GeometricScaling, { 1.0f, 1.0f, 1.0f } },
	{ ufbxi_GeometricTranslation, { 0.0f, 0.0f, 0.0f } },
	{ ufbxi_Lcl_Scaling, { 1.0f, 1.0f, 1.0f } },
};

ufbxi_nodiscard ufbxi_noinline static int ufbxi_setup_scale_helper(ufbxi_context *uc, ufbx_node *node, uint64_t node_fbx_id)
{
	uint64_t scale_fbx_id;
	ufbx_node *scale_node = ufbxi_push_synthetic_element(uc, &scale_fbx_id, NULL, uc->opts.scale_helper_name.data, ufbx_node, UFBX_ELEMENT_NODE);
	ufbxi_check(scale_node);
	ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &scale_node->element.element_id));
	scale_node->element.dom_node = node->element.dom_node;

	node->scale_helper = scale_node;
	scale_node->is_scale_helper = true;

	ufbxi_check(ufbxi_connect_oo(uc, scale_fbx_id, node_fbx_id));
	uc->has_scale_helper_nodes = true;

	ufbxi_node_extra *extra = ufbxi_push_element_extra(uc, node->element.element_id, ufbxi_node_extra);
	ufbxi_check(extra);
	extra->scale_helper_id = scale_node->element_id;

	size_t max_props = ufbxi_arraycount(ufbxi_scale_helper_props);
	ufbx_prop *helper_props = ufbxi_push(&uc->result, ufbx_prop, max_props);
	ufbxi_check(helper_props);

	size_t num_props = 0;
	ufbx_props props_copy = node->props;
	props_copy.defaults = NULL;
	for (size_t i = 0; i < max_props; i++) {
		const ufbxi_scale_helper_prop *hp = &ufbxi_scale_helper_props[i];
		ufbx_prop *src_prop = ufbxi_find_prop(&props_copy, hp->name);
		if (!src_prop) continue;

		helper_props[num_props++] = *src_prop;
		src_prop->value_vec3 = hp->default_value;
		src_prop->value_int = (int64_t)src_prop->value_vec3.x;
	}

	scale_node->props.props.data = helper_props;
	scale_node->props.props.count = num_props;

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_model(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	(void)node;
	ufbx_node *elem_node = ufbxi_push_element(uc, info, ufbx_node, UFBX_ELEMENT_NODE);
	ufbxi_check(elem_node);
	ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &elem_node->element.element_id));

	int64_t inherit_type = ufbxi_find_int(&elem_node->props, ufbxi_InheritType, -1);
	switch (inherit_type) {
	case 0: // RrSs
		elem_node->original_inherit_mode = UFBX_INHERIT_MODE_COMPONENTWISE_SCALE;
		break;
	case 2: // Rrs
		elem_node->original_inherit_mode = UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE;
		break;
	default: break;
	}

	if (uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_PRESERVE) {
		elem_node->inherit_mode = elem_node->original_inherit_mode;
	} else if (uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_IGNORE) {
		elem_node->original_inherit_mode = UFBX_INHERIT_MODE_NORMAL;
		elem_node->inherit_mode = UFBX_INHERIT_MODE_NORMAL;
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_element(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info, size_t size, ufbx_element_type type)
{
	(void)node;
	ufbx_element *elem = ufbxi_push_element_size(uc, info, size, type);
	ufbxi_check(elem);
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_unknown(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *element, ufbx_string type, ufbx_string sub_type, const char *node_name)
{
	(void)node;
	ufbx_unknown *unknown = ufbxi_push_element(uc, element, ufbx_unknown, UFBX_ELEMENT_UNKNOWN);
	ufbxi_check(unknown);
	unknown->type = type;
	unknown->sub_type = sub_type;
	unknown->super_type.data = node_name;
	unknown->super_type.length = strlen(node_name);

	// `type`, `sub_type` and `node_name` are raw strings so they may need to be sanitized.
	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &unknown->type, false));
	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &unknown->sub_type, false));
	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &unknown->super_type, false));

	return 1;
}

typedef struct {
	ufbx_vertex_vec3 elem;
	uint32_t index;
} ufbxi_tangent_layer;

static ufbx_real ufbxi_zero_element[8] = { 0 };

// Sentinel pointers used for zero/sequential index buffers
static const uint32_t ufbxi_sentinel_index_zero[1] = { 100000000 };
static const uint32_t ufbxi_sentinel_index_consecutive[1] = { 123456789 };

ufbxi_noinline static int ufbxi_fix_index(ufbxi_context *uc, uint32_t *p_dst, uint32_t index, size_t one_past_max_val)
{
	switch (uc->opts.index_error_handling) {
	case UFBX_INDEX_ERROR_HANDLING_CLAMP:
		ufbxi_check(one_past_max_val > 0);
		ufbxi_check(one_past_max_val <= UINT32_MAX);
		*p_dst = (uint32_t)one_past_max_val - 1;
		ufbxi_check(ufbxi_warnf(UFBX_WARNING_INDEX_CLAMPED, "Clamped index"));
		break;
	case UFBX_INDEX_ERROR_HANDLING_NO_INDEX:
		*p_dst = UFBX_NO_INDEX;
		break;
	case UFBX_INDEX_ERROR_HANDLING_ABORT_LOADING:
		ufbxi_fmt_err_info(&uc->error, "%u (max %u)", index, one_past_max_val ? (one_past_max_val - 1) : 0);
		ufbxi_fail_msg("UFBX_INDEX_ERROR_HANDLING_ABORT_LOADING", "Bad index");
	case UFBX_INDEX_ERROR_HANDLING_UNSAFE_IGNORE:
		*p_dst = index;
		break;
	default:
		ufbxi_unreachable("Unhandled index_error_handling");
		return 0;
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_check_indices(ufbxi_context *uc, uint32_t **p_dst, uint32_t *indices, bool owns_indices, size_t num_indices, size_t num_indexers, size_t num_elems)
{
	// If the indices are truncated extend them with `UFBX_NO_INDEX`, the following normalization pass
	// will handle them the same way as other out-of-bounds indices.
	if (num_indices < num_indexers) {
		uint32_t *new_indices = ufbxi_push(&uc->result, uint32_t, num_indexers);
		ufbxi_check(new_indices);

		memcpy(new_indices, indices, sizeof(uint32_t) * num_indices);
		for (size_t i = num_indices; i < num_indexers; i++) {
			new_indices[i] = UFBX_NO_INDEX;
		}

		indices = new_indices;
		num_indices = num_indexers;
		owns_indices = true;
	}

	// Normalize out-of-bounds indices to `invalid_index`
	for (size_t i = 0; i < num_indices; i++) {
		uint32_t ix = indices[i];
		if (ix >= num_elems) {
			// If the indices refer to an external buffer we need to
			// allocate a separate buffer for them
			if (!owns_indices) {
				indices = ufbxi_push_copy(&uc->result, uint32_t, num_indices, indices);
				ufbxi_check(indices);
				owns_indices = true;
			}
			ufbxi_check(ufbxi_fix_index(uc, &indices[i], ix, num_elems));
		}
	}

	*p_dst = indices;

	return 1;
}

ufbx_static_assert(vertex_real_size, sizeof(ufbx_vertex_real) == sizeof(ufbx_vertex_attrib));
ufbx_static_assert(vertex_vec2_size, sizeof(ufbx_vertex_vec2) == sizeof(ufbx_vertex_attrib));
ufbx_static_assert(vertex_vec3_size, sizeof(ufbx_vertex_vec3) == sizeof(ufbx_vertex_attrib));
ufbx_static_assert(vertex_vec4_size, sizeof(ufbx_vertex_vec4) == sizeof(ufbx_vertex_attrib));

ufbxi_nodiscard ufbxi_noinline static int ufbxi_warn_polygon_mapping(ufbxi_context *uc, const char *data_name, const char *mapping)
{
	ufbxi_check(ufbxi_warnf(UFBX_WARNING_MISSING_POLYGON_MAPPING, "Ignoring geometry '%s' with bad mapping mode '%s'", data_name, mapping));
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_vertex_element(ufbxi_context *uc, ufbx_mesh *mesh, ufbxi_node *node,
	ufbx_vertex_attrib *attrib, const char *data_name, const char *index_name, const char *w_name, char data_type, size_t num_components)
{
	ufbx_real **p_dst_data = (ufbx_real**)&attrib->values.data;

	ufbxi_value_array *data = ufbxi_find_array(node, data_name, data_type);
	ufbxi_value_array *indices = ufbxi_find_array(node, index_name, 'i');

	if (!uc->opts.strict) {
		if (!data) return 1;
	}

	ufbxi_check(data);
	ufbxi_check(data->size % num_components == 0);

	size_t num_elems = data->size / num_components;

	// HACK: If there's no elements at all keep the attribute as NULL
	// TODO: Strict mode for this?
	if (num_elems == 0) {
		return 1;
	}

	ufbxi_check(num_elems > 0 && num_elems < INT32_MAX);

	attrib->exists = true;
	attrib->indices.count = mesh->num_indices;

	const char *mapping = "";
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_MappingInformationType, "C", (char**)&mapping));

	attrib->values.count = num_elems ? num_elems : 1;

	// Data array is always used as-is, if empty set the data to a global
	// zero buffer so invalid zero index can point to some valid data.
	// The zero data is offset by 4 elements to accommodate for invalid index (-1)
	if (num_elems > 0) {
		*p_dst_data = (ufbx_real*)data->data;
	} else {
		*p_dst_data = ufbxi_zero_element + 4;
	}

	// HACK: Some old exporters seem to use ByPolygon to mean ByPolygonVertex,
	// it should be quite safe to remap this
	if (mapping == ufbxi_ByPolygon) {
		size_t num_indices = indices ? indices->size : num_elems;
		if (num_indices == mesh->num_indices) {
			mapping = ufbxi_ByPolygonVertex;
		}
	}

	if (indices) {
		size_t num_indices = indices->size;
		uint32_t *index_data = (uint32_t*)indices->data;

		if (mapping == ufbxi_ByPolygonVertex) {

			// Indexed by polygon vertex: We can use the provided indices directly.
			ufbxi_check(ufbxi_check_indices(uc, &attrib->indices.data, index_data, true, num_indices, mesh->num_indices, num_elems));

		} else if (mapping == ufbxi_ByVertex || mapping == ufbxi_ByVertice) {

			// Indexed by vertex: Follow through the position index mapping to get the final indices.
			uint32_t *new_index_data = ufbxi_push(&uc->result, uint32_t, mesh->num_indices);
			ufbxi_check(new_index_data);

			uint32_t *vert_ix = mesh->vertex_indices.data;
			for (size_t i = 0; i < mesh->num_indices; i++) {
				uint32_t ix = vert_ix[i];
				if (ix < num_indices) {
					new_index_data[i] = index_data[ix];
				} else {
					ufbxi_check(ufbxi_fix_index(uc, &new_index_data[i], ix, num_elems));
				}
			}

			ufbxi_check(ufbxi_check_indices(uc, &attrib->indices.data, new_index_data, true, mesh->num_indices, mesh->num_indices, num_elems));
			attrib->unique_per_vertex = true;

		} else if (mapping == ufbxi_ByPolygon) {

			// Indexed by polygon: Generate new indices based on polygons
			uint32_t *new_index_data = ufbxi_push(&uc->result, uint32_t, mesh->num_indices);
			ufbxi_check(new_index_data);

			size_t num_faces = mesh->num_faces;
			for (size_t face_ix = 0; face_ix < num_faces; face_ix++) {
				ufbx_face face = mesh->faces.data[face_ix];
				uint32_t index = UFBX_NO_INDEX;
				if (face_ix < num_indices) {
					index = index_data[face_ix];
				}
				if (index >= num_elems) {
					ufbxi_check(ufbxi_fix_index(uc, &index, index, num_elems));
				}
				for (size_t i = 0; i < face.num_indices; i++) {
					new_index_data[face.index_begin + i] = index;
				}
			}

			attrib->indices.data = new_index_data;

		} else if (mapping == ufbxi_AllSame) {

			// Indexed by all same: ??? This could be possibly used for making
			// holes with invalid indices, but that seems really fringe.
			// Just use the shared zero index buffer for this.
			uc->max_zero_indices = ufbxi_max_sz(uc->max_zero_indices, mesh->num_indices);
			attrib->indices.data = (uint32_t*)ufbxi_sentinel_index_zero;
			attrib->unique_per_vertex = true;

		} else {
			memset(attrib, 0, sizeof(ufbx_vertex_attrib));
			ufbxi_check(ufbxi_warn_polygon_mapping(uc, data_name, mapping));
			return 1;
		}

	} else {

		if (mapping == ufbxi_ByPolygonVertex) {

			// Direct by polygon index: Use shared consecutive array if there's enough
			// elements, otherwise use a unique truncated consecutive index array.
			if (num_elems >= mesh->num_indices) {
				uc->max_consecutive_indices = ufbxi_max_sz(uc->max_consecutive_indices, mesh->num_indices);
				attrib->indices.data = (uint32_t*)ufbxi_sentinel_index_consecutive;
			} else {
				uint32_t *index_data = ufbxi_push(&uc->result, uint32_t, mesh->num_indices);
				ufbxi_check(index_data);
				for (size_t i = 0; i < mesh->num_indices; i++) {
					index_data[i] = (uint32_t)i;
				}
				ufbxi_check(ufbxi_check_indices(uc, &attrib->indices.data, index_data, true, mesh->num_indices, mesh->num_indices, num_elems));
			}

		} else if (mapping == ufbxi_ByVertex || mapping == ufbxi_ByVertice) {

			// Direct by vertex: We can re-use the position indices..
			ufbxi_check(ufbxi_check_indices(uc, &attrib->indices.data, mesh->vertex_position.indices.data, false, mesh->num_indices, mesh->num_indices, num_elems));
			attrib->unique_per_vertex = true;

		} else if (mapping == ufbxi_ByPolygon) {

			// Direct by polygon: Generate new indices based on polygons
			uint32_t *new_index_data = ufbxi_push(&uc->result, uint32_t, mesh->num_indices);
			ufbxi_check(new_index_data);

			uint32_t num_faces = (uint32_t)mesh->num_faces;
			for (uint32_t face_ix = 0; face_ix < num_faces; face_ix++) {
				ufbx_face face = mesh->faces.data[face_ix];
				for (size_t i = 0; i < face.num_indices; i++) {
					new_index_data[face.index_begin + i] = face_ix;
				}
			}

			ufbxi_check(ufbxi_check_indices(uc, &attrib->indices.data, new_index_data, true, mesh->num_indices, mesh->num_indices, num_elems));

		} else if (mapping == ufbxi_AllSame) {

			// Direct by all same: This cannot fail as the index list is just zero.
			uc->max_zero_indices = ufbxi_max_sz(uc->max_zero_indices, mesh->num_indices);
			attrib->indices.data = (uint32_t*)ufbxi_sentinel_index_zero;
			attrib->unique_per_vertex = true;

		} else {
			memset(attrib, 0, sizeof(ufbx_vertex_attrib));
			ufbxi_check(ufbxi_warn_polygon_mapping(uc, data_name, mapping));
			return 1;
		}
	}

	if (uc->opts.retain_vertex_attrib_w && w_name) {
		ufbxi_value_array *w_data = ufbxi_find_array(node, w_name, 'r');
		if (w_data) {
			if (w_data->size == num_elems) {
				attrib->values_w.count = w_data->size;
				attrib->values_w.data = (ufbx_real*)w_data->data;
			} else {
				ufbxi_check(ufbxi_warnf(UFBX_WARNING_BAD_VERTEX_W_ATTRIBUTE, "Bad W array size %s=%zu, %s=%zu",
					w_name, w_data->size, data_name, num_elems));
			}
		}
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_truncated_array(ufbxi_context *uc, void *p_data, size_t *p_count, ufbxi_node *node, const char *name, char fmt, size_t size)
{
	ufbxi_value_array *arr = ufbxi_find_array(node, name, fmt);
	if (!arr) {
		ufbxi_check(ufbxi_warnf(UFBX_WARNING_MISSING_GEOMETRY_DATA, "Missing geometry data: %s", name));
		return 1;
	}

	*p_count = size;

	void *data = arr->data;
	if (arr->size < size) {
		ufbxi_check(ufbxi_warnf(UFBX_WARNING_TRUNCATED_ARRAY, "Truncated array: %s", name));

		size_t elem_size = ufbxi_array_type_size(fmt);
		void *new_data = ufbxi_push_size(&uc->result, elem_size, size);
		ufbxi_check(new_data);
		memcpy(new_data, data, arr->size * elem_size);
		// Extend the array with the last element if possible
		if (arr->size > 0) {
			char *first_elem = (char*)data + (arr->size - 1) * elem_size;
			for (size_t i = arr->size; i < size; i++) {
				memcpy((char*)new_data + i * elem_size, first_elem, elem_size);
			}
		} else {
			memset(new_data, 0, size * elem_size);
		}
		data = new_data;
	}

	*(void**)p_data = data;
	return 1;
}

ufbxi_noinline static bool ufbxi_uv_set_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_uv_set *a = (const ufbx_uv_set *)va, *b = (const ufbx_uv_set *)vb;
	return a->index < b->index;
}

ufbxi_noinline static bool ufbxi_color_set_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_color_set *a = (const ufbx_color_set *)va, *b = (const ufbx_color_set *)vb;
	return a->index < b->index;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_uv_sets(ufbxi_context *uc, ufbx_uv_set *sets, size_t count)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_uv_set)));
	ufbxi_stable_sort(sizeof(ufbx_uv_set), 32, sets, uc->tmp_arr, count, &ufbxi_uv_set_less, NULL);
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_color_sets(ufbxi_context *uc, ufbx_color_set *sets, size_t count)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_color_set)));
	ufbxi_stable_sort(sizeof(ufbx_color_set), 32, sets, uc->tmp_arr, count, &ufbxi_color_set_less, NULL);
	return 1;
}

typedef struct ufbxi_blend_offset {
	uint32_t vertex;
	ufbx_vec3 position_offset;
	ufbx_vec3 normal_offset;
} ufbxi_blend_offset;

static ufbxi_noinline bool ufbxi_blend_offset_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbxi_blend_offset *a = (const ufbxi_blend_offset*)va, *b = (const ufbxi_blend_offset*)vb;
	return a->vertex < b->vertex;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_blend_offsets(ufbxi_context *uc, ufbxi_blend_offset *offsets, size_t count)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbxi_blend_offset)));
	ufbxi_stable_sort(sizeof(ufbxi_blend_offset), 16, offsets, uc->tmp_arr, count, &ufbxi_blend_offset_less, NULL);
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_shape(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbxi_node *node_vertices = ufbxi_find_child(node, ufbxi_Vertices);
	ufbxi_node *node_indices = ufbxi_find_child(node, ufbxi_Indexes);
	ufbxi_node *node_normals = ufbxi_find_child(node, ufbxi_Normals);
	if (!node_vertices || !node_indices) return 1;

	ufbx_blend_shape *shape = ufbxi_push_element(uc, info, ufbx_blend_shape, UFBX_ELEMENT_BLEND_SHAPE);
	ufbxi_check(shape);

	if (uc->opts.ignore_geometry) return 1;

	ufbxi_value_array *vertices = ufbxi_get_array(node_vertices, 'r');
	ufbxi_value_array *indices = ufbxi_get_array(node_indices, 'i');

	ufbxi_check(vertices && indices);
	ufbxi_check(vertices->size % 3 == 0);
	ufbxi_check(indices->size == vertices->size / 3);

	size_t num_offsets = indices->size;
	uint32_t *vertex_indices = (uint32_t*)indices->data;

	shape->num_offsets = num_offsets;
	shape->position_offsets.data = (ufbx_vec3*)vertices->data;
	shape->offset_vertices.data = vertex_indices;
	shape->position_offsets.count = num_offsets;
	shape->offset_vertices.count = num_offsets;

	if (node_normals) {
		ufbxi_value_array *normals = ufbxi_get_array(node_normals, 'r');
		ufbxi_check(normals && normals->size == vertices->size);
		shape->normal_offsets.data = (ufbx_vec3*)normals->data;
		shape->normal_offsets.count = num_offsets;
	}

	// Sort the blend shape vertices only if absolutely necessary
	bool sorted = true;
	for (size_t i = 1; i < num_offsets; i++) {
		if (vertex_indices[i - 1] > vertex_indices[i]) {
			sorted = false;
			break;
		}
	}

	if (!sorted) {
		ufbxi_blend_offset *offsets = ufbxi_push(&uc->tmp_stack, ufbxi_blend_offset, num_offsets);
		ufbxi_check(offsets);

		for (size_t i = 0; i < num_offsets; i++) {
			offsets[i].vertex = shape->offset_vertices.data[i];
			offsets[i].position_offset = shape->position_offsets.data[i];
			if (node_normals) offsets[i].normal_offset = shape->normal_offsets.data[i];
		}

		ufbxi_check(ufbxi_sort_blend_offsets(uc, offsets, num_offsets));

		for (size_t i = 0; i < num_offsets; i++) {
			shape->offset_vertices.data[i] = offsets[i].vertex;
			shape->position_offsets.data[i] = offsets[i].position_offset;
			if (node_normals) shape->normal_offsets.data[i] = offsets[i].normal_offset;
		}
		ufbxi_pop(&uc->tmp_stack, ufbxi_blend_offset, num_offsets, NULL);
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_synthetic_blend_shapes(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_blend_deformer *deformer = NULL;
	uint64_t deformer_fbx_id = 0;

	ufbxi_for (ufbxi_node, n, node->children, node->num_children) {
		if (n->name != ufbxi_Shape) continue;

		ufbx_string name;
		ufbxi_check(ufbxi_get_val1(n, "S", &name));

		if (deformer == NULL) {
			deformer = ufbxi_push_synthetic_element(uc, &deformer_fbx_id, n, name.data, ufbx_blend_deformer, UFBX_ELEMENT_BLEND_DEFORMER);
			ufbxi_check(deformer);
			ufbxi_check(ufbxi_connect_oo(uc, deformer_fbx_id, info->fbx_id));
		}

		uint64_t channel_fbx_id = 0;
		ufbx_blend_channel *channel = ufbxi_push_synthetic_element(uc, &channel_fbx_id, n, name.data, ufbx_blend_channel, UFBX_ELEMENT_BLEND_CHANNEL);
		ufbxi_check(channel);

		ufbx_real_list weight_list = { NULL, 0 };
		ufbxi_check(ufbxi_push_copy(&uc->tmp_full_weights, ufbx_real_list, 1, &weight_list));

		size_t num_shape_props = 1;
		ufbx_prop *shape_props = ufbxi_push_zero(&uc->result, ufbx_prop, num_shape_props);
		ufbxi_check(shape_props);
		shape_props[0].name.data = ufbxi_DeformPercent;
		shape_props[0].name.length = sizeof(ufbxi_DeformPercent) - 1;
		shape_props[0]._internal_key = ufbxi_get_name_key_c(ufbxi_DeformPercent);
		shape_props[0].type = UFBX_PROP_NUMBER;
		shape_props[0].value_real = (ufbx_real)0.0;
		shape_props[0].value_str = ufbx_empty_string;
		shape_props[0].value_blob = ufbx_empty_blob;

		ufbx_prop *self_prop = ufbx_find_prop_len(&info->props, name.data, name.length);
		if (self_prop && (self_prop->type == UFBX_PROP_NUMBER || self_prop->type == UFBX_PROP_INTEGER)) {
			shape_props[0].value_real = self_prop->value_real;
			ufbxi_check(ufbxi_connect_pp(uc, info->fbx_id, channel_fbx_id, name, shape_props[0].name));
		} else if (uc->version < 6000) {
			ufbxi_check(ufbxi_connect_pp(uc, info->fbx_id, channel_fbx_id, name, shape_props[0].name));
		}

		channel->name = name;
		channel->props.props.data = shape_props;
		channel->props.props.count = num_shape_props;

		ufbxi_element_info shape_info = { 0 };

		ufbxi_check(ufbxi_push_synthetic_id(uc, &shape_info.fbx_id));
		shape_info.name = name;
		shape_info.dom_node = ufbxi_get_dom_node(uc, n);

		ufbxi_check(ufbxi_read_shape(uc, n, &shape_info));

		ufbxi_check(ufbxi_connect_oo(uc, channel_fbx_id, deformer_fbx_id));
		ufbxi_check(ufbxi_connect_oo(uc, shape_info.fbx_id, channel_fbx_id));
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_process_indices(ufbxi_context *uc, ufbx_mesh *mesh, uint32_t *index_data)
{
	// Count the number of faces and allocate the index list
	// Indices less than zero (~actual_index) ends a polygon
	size_t num_total_faces = 0;
	ufbxi_for (uint32_t, p_ix, index_data, mesh->num_indices) {
		num_total_faces += ((int32_t)*p_ix < 0) ? 1u : 0u;
	}
	mesh->faces.data = ufbxi_push(&uc->result, ufbx_face, num_total_faces);
	ufbxi_check(mesh->faces.data);

	size_t num_triangles = 0;
	size_t max_face_triangles = 0;
	size_t num_bad_faces[3] = { 0 };

	ufbx_face *dst_face = mesh->faces.data;
	uint32_t *p_face_begin = index_data;
	ufbxi_for (uint32_t, p_ix, index_data, mesh->num_indices) {
		uint32_t ix = *p_ix;
		// Un-negate final indices of polygons
		if ((int32_t)ix < 0) {
			ix = ~ix;
			*p_ix =  ix;
			uint32_t num_indices = (uint32_t)((p_ix - p_face_begin) + 1);
			dst_face->index_begin = (uint32_t)(p_face_begin - index_data);
			dst_face->num_indices = num_indices;
			if (num_indices >= 3) {
				num_triangles += num_indices - 2;
				max_face_triangles = ufbxi_max_sz(max_face_triangles, num_indices - 2);
			} else {
				num_bad_faces[num_indices]++;
			}
			dst_face++;
			p_face_begin = p_ix + 1;
		}
		ufbxi_check((size_t)ix < mesh->num_vertices);
	}

	mesh->vertex_position.indices.data = index_data;
	mesh->num_faces = ufbxi_to_size(dst_face - mesh->faces.data);
	mesh->faces.count = mesh->num_faces;
	mesh->num_triangles = num_triangles;
	mesh->max_face_triangles = max_face_triangles;
	mesh->num_empty_faces = num_bad_faces[0];
	mesh->num_point_faces = num_bad_faces[1];
	mesh->num_line_faces = num_bad_faces[2];

	mesh->vertex_first_index.count = mesh->num_vertices;
	mesh->vertex_first_index.data = ufbxi_push(&uc->result, uint32_t, mesh->num_vertices);
	ufbxi_check(mesh->vertex_first_index.data);

	ufbxi_for_list(uint32_t, p_vx_ix, mesh->vertex_first_index) {
		*p_vx_ix = UFBX_NO_INDEX;
	}

	{
		size_t num_indices = mesh->num_indices;
		size_t num_vertices = mesh->num_vertices;
		uint32_t *vertex_indices = mesh->vertex_indices.data;
		uint32_t *vertex_first_index = mesh->vertex_first_index.data;
		for (size_t ix = 0; ix < num_indices; ix++) {
			uint32_t vx = vertex_indices[ix];
			if (vx < num_vertices) {
				if (vertex_first_index[vx] == UFBX_NO_INDEX) {
					vertex_first_index[vx] = (uint32_t)ix;
				}
			} else {
				ufbxi_check(ufbxi_fix_index(uc, &vertex_indices[ix], vx, mesh->num_vertices));
			}
		}
	}

	// HACK(consecutive-faces): Prepare for finalize to re-use a consecutive/zero
	// index buffer for face materials..
	uc->max_zero_indices = ufbxi_max_sz(uc->max_zero_indices, mesh->num_faces);
	uc->max_consecutive_indices = ufbxi_max_sz(uc->max_consecutive_indices, mesh->num_faces);

	return 1;
}

ufbxi_noinline static void ufbxi_patch_mesh_reals(ufbx_mesh *mesh)
{
	mesh->vertex_position.value_reals = 3;
	mesh->vertex_normal.value_reals = 3;
	mesh->vertex_uv.value_reals = 2;
	mesh->vertex_tangent.value_reals = 3;
	mesh->vertex_bitangent.value_reals = 3;
	mesh->vertex_color.value_reals = 4;
	mesh->vertex_crease.value_reals = 1;
	mesh->skinned_position.value_reals = 3;
	mesh->skinned_normal.value_reals = 3;

	ufbxi_nounroll ufbxi_for_list(ufbx_uv_set, set, mesh->uv_sets) {
		set->vertex_uv.value_reals = 2;
		set->vertex_tangent.value_reals = 3;
		set->vertex_bitangent.value_reals = 3;
	}

	ufbxi_nounroll ufbxi_for_list(ufbx_color_set, set, mesh->color_sets) {
		set->vertex_color.value_reals = 4;
	}
}

typedef struct {
	uint32_t id, index;
} ufbxi_id_group;

static bool ufbxi_less_int32(void *user, const void *va, const void *vb)
{
	(void)user;
	const int32_t a = *(const int32_t*)va, b = *(const int32_t*)vb;
	return a < b;
}

ufbx_static_assert(mesh_mat_point_faces, offsetof(ufbx_mesh_part, num_point_faces) - offsetof(ufbx_mesh_part, num_empty_faces) == 1 * sizeof(size_t));
ufbx_static_assert(mesh_mat_line_faces, offsetof(ufbx_mesh_part, num_line_faces) - offsetof(ufbx_mesh_part, num_empty_faces) == 2 * sizeof(size_t));
static ufbxi_forceinline void ufbxi_mesh_part_add_face(ufbx_mesh_part *part, uint32_t num_indices)
{
	part->num_faces++;
	if (num_indices >= 3) {
		part->num_triangles += num_indices - 2;
	} else {
		// `num_empty/point/line_faces` are consecutive, see static asserts above.
		// cppcheck-suppress objectIndex
		(&part->num_empty_faces)[num_indices]++;
	}
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_assign_face_groups(ufbxi_buf *buf, ufbx_error *error, ufbx_mesh *mesh, size_t *p_consecutive_indices, bool retain_parts)
{
	size_t num_faces = mesh->num_faces;
	ufbxi_check_err(error, num_faces > 0);
	ufbxi_check_err(error, num_faces < UINT32_MAX);
	ufbxi_check_err(error, mesh->face_group.count == num_faces);

	uint32_t *ids = ufbxi_push(buf, uint32_t, num_faces);
	ufbxi_check_err(error, ids);

	uint32_t num_ids = 0;

	ufbxi_id_group seen_ids[1 << UFBXI_FACE_GROUP_HASH_BITS];
	memset(seen_ids, 0, sizeof(seen_ids));

	uint32_t seed = 2654435769u;
	uint32_t rehash_threshold = 256;

	// Loosely deduplicate group IDs
	ufbxi_for_list(uint32_t, p_id, mesh->face_group) {
		uint32_t id = *p_id;
		uint32_t id_hash = (id * seed) >> (32u - UFBXI_FACE_GROUP_HASH_BITS);
		if (seen_ids[id_hash].id != id || seen_ids[id_hash].index == 0) {
			seen_ids[id_hash].id = id;
			if (++seen_ids[id_hash].index > rehash_threshold) {
				seed *= seed;
				rehash_threshold *= 2;
			}
			ids[num_ids++] = id;
		}
	}

	// Sort and deduplicate remaining IDs
	ufbxi_unstable_sort(ids, num_ids, sizeof(uint32_t), &ufbxi_less_int32, NULL);

	size_t num_groups = 0;
	for (size_t i = 0; i < num_ids; ) {
		uint32_t id = ids[i];
		ids[num_groups++] = id;
		do { i++; } while (i < num_ids && ids[i] == id);
	}

	// Allocate group info structs
	ufbx_face_group *groups = ufbxi_push_zero(buf, ufbx_face_group, num_groups);
	ufbxi_check_err(error, groups);
	for (size_t i = 0; i < num_groups; i++) {
		groups[i].id = (int32_t)ids[i];
		groups[i].name.data = ufbxi_empty_char;
	}

	mesh->face_groups.data = groups;
	mesh->face_groups.count = num_groups;

	ufbx_mesh_part *parts = NULL;
	if (retain_parts) {
		parts = ufbxi_push_zero(buf, ufbx_mesh_part, num_groups);
		ufbxi_check_err(error, parts);
		mesh->face_group_parts.data = parts;
		mesh->face_group_parts.count = num_groups;
	}

	// Optimization: Use `consecutive_indices` for a single group
	if (p_consecutive_indices && num_groups == 1) {
		memset(mesh->face_group.data, 0, sizeof(uint32_t) * num_faces);

		if (parts) {
			parts[0].face_indices.data = (uint32_t*)ufbxi_sentinel_index_consecutive;
			parts[0].face_indices.count = num_faces;
			parts[0].num_empty_faces = mesh->num_empty_faces;
			parts[0].num_point_faces = mesh->num_point_faces;
			parts[0].num_line_faces = mesh->num_line_faces;
			parts[0].num_faces = num_faces;
			parts[0].num_triangles = mesh->num_triangles;
		}

		*p_consecutive_indices = ufbxi_max_sz(*p_consecutive_indices, num_faces);
		return 1;
	}

	memset(seen_ids, 0, sizeof(seen_ids));

	// Count faces and triangles per group and reassign IDs
	const ufbx_face *p_face = mesh->faces.data;
	ufbxi_for_list(uint32_t, p_id, mesh->face_group) {
		uint32_t id = *p_id;
		uint32_t id_hash = (id * seed) >> (32u - UFBXI_FACE_GROUP_HASH_BITS);

		uint32_t num_indices = p_face->num_indices;

		size_t index;
		if (seen_ids[id_hash].id == id && seen_ids[id_hash].index > 0) {
			index = seen_ids[id_hash].index - 1;
			*p_id = (uint32_t)index;
		} else {
			int32_t signed_id = (int32_t)id;
			index = SIZE_MAX;
			ufbxi_macro_lower_bound_eq(ufbx_face_group, 8, &index, groups, 0, num_groups, ( a->id < signed_id ), ( a->id == signed_id ));
			ufbx_assert(index < num_groups);
			seen_ids[id_hash].id = id;
			seen_ids[id_hash].index = (uint32_t)index + 1;
		}

		if (parts) {
			ufbxi_mesh_part_add_face(&parts[index], num_indices);
		}

		*p_id = (uint32_t)index;
		p_face++;
	}

	if (!parts) return 1;

	// Subdivide `ids` for per-group `face_indices`
	uint32_t *face_indices = ids;
	uint32_t part_index = 0;
	ufbxi_for(ufbx_mesh_part, part, parts, num_groups) {
		part->index = part_index++;
		part->face_indices.data = face_indices;
		face_indices += part->num_faces;
	}
	ufbx_assert(face_indices == ids + num_faces);

	// Collect per-group faces
	uint32_t face_index = 0;
	ufbxi_for_list(uint32_t, p_id, mesh->face_group) {
		ufbx_mesh_part *part = &parts[*p_id];
		part->face_indices.data[part->face_indices.count++] = face_index++;
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_update_face_groups(ufbxi_buf *buf, ufbx_error *error, ufbx_mesh *mesh, bool need_copy)
{
	size_t num_faces = mesh->faces.count;
	size_t num_groups = mesh->face_group_parts.count;
	if (num_groups == 0) return 1;

	if (need_copy) {
		mesh->face_group_parts.data = ufbxi_push_zero(buf, ufbx_mesh_part, num_groups);
		ufbxi_check_err(error, mesh->face_group_parts.data);
	}

	uint32_t *face_indices = ufbxi_push(buf, uint32_t, num_faces);
	ufbxi_check_err(error, face_indices);

	ufbxi_nounroll for (size_t i = 0; i < num_faces; i++) {
		ufbx_mesh_part *part = &mesh->face_group_parts.data[mesh->face_group.data[i]];
		ufbxi_mesh_part_add_face(part, mesh->faces.data[i].num_indices);
	}

	uint32_t part_index = 0;
	ufbxi_for_list(ufbx_mesh_part, part, mesh->face_group_parts) {
		part->index = part_index++;
		part->face_indices.data = face_indices;
		part->face_indices.count = 0;
		face_indices += part->num_faces;
	}

	ufbxi_nounroll for (uint32_t i = 0; i < num_faces; i++) {
		ufbx_mesh_part *part = &mesh->face_group_parts.data[mesh->face_group.data[i]];
		part->face_indices.data[part->face_indices.count++] = i;
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_mesh(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_mesh *ufbxi_restrict mesh = ufbxi_push_element(uc, info, ufbx_mesh, UFBX_ELEMENT_MESH);
	ufbxi_check(mesh);

	// In up to version 7100 FBX files blend shapes are contained within the same geometry node
	if (uc->version <= 7100) {
		ufbxi_check(ufbxi_read_synthetic_blend_shapes(uc, node, info));
	}

	ufbxi_patch_mesh_reals(mesh);

	// Sometimes there are empty meshes in FBX files?
	// TODO: Should these be included in output? option? strict mode?
	ufbxi_node *node_vertices = ufbxi_find_child(node, ufbxi_Vertices);
	ufbxi_node *node_indices = ufbxi_find_child(node, ufbxi_PolygonVertexIndex);
	if (!node_vertices) return 1;

	if (uc->opts.ignore_geometry) return 1;

	ufbxi_value_array *vertices = ufbxi_get_array(node_vertices, 'r');
	ufbxi_value_array *indices = node_indices ? ufbxi_get_array(node_indices, 'i') : NULL;
	ufbxi_value_array *edge_indices = ufbxi_find_array(node, ufbxi_Edges, 'i');
	ufbxi_check(vertices);
	ufbxi_check(!node_indices || indices); // If node_indices exists, it must be an array
	ufbxi_check(vertices->size % 3 == 0);

	mesh->num_vertices = vertices->size / 3;
	mesh->num_indices = indices ? indices->size : 0;

	uint32_t *index_data = indices ? (uint32_t*)indices->data : NULL;

	// Duplicate `index_data` for modification if we retain DOM
	if (uc->opts.retain_dom) {
		index_data = ufbxi_push_copy(&uc->result, uint32_t, mesh->num_indices, index_data);
		ufbxi_check(index_data);
	}

	mesh->vertices.data = (ufbx_vec3*)vertices->data;
	mesh->vertices.count = mesh->num_vertices;
	mesh->vertex_indices.data = index_data;
	mesh->vertex_indices.count = mesh->num_indices;

	mesh->vertex_position.exists = true;
	mesh->vertex_position.values.data = (ufbx_vec3*)vertices->data;
	mesh->vertex_position.values.count = mesh->num_vertices;
	mesh->vertex_position.indices.data = index_data;
	mesh->vertex_position.indices.count = mesh->num_indices;
	mesh->vertex_position.unique_per_vertex = true;

	// Check/make sure that the last index is negated (last of polygon)
	if (mesh->num_indices > 0) {
		if ((int32_t)index_data[mesh->num_indices - 1] >= 0) {
			if (uc->opts.strict) ufbxi_fail("Non-negated last index");
			index_data[mesh->num_indices - 1] = ~index_data[mesh->num_indices - 1];
		}
	}

	// Read edges before un-negating the indices
	if (edge_indices) {
		size_t num_edges = edge_indices->size;
		ufbx_edge *edges = ufbxi_push(&uc->result, ufbx_edge, num_edges);
		ufbxi_check(edges);

		size_t dst_ix = 0;

		// Edges are represented using a single index into PolygonVertexIndex.
		// The edge is between two consecutive vertices in the polygon.
		uint32_t *edge_data = (uint32_t*)edge_indices->data;
		for (size_t i = 0; i < num_edges; i++) {
			uint32_t index_ix = edge_data[i];
			if (index_ix >= mesh->num_indices) {
				if (uc->opts.strict) ufbxi_fail("Edge index out of bounds");
				continue;
			}
			edges[dst_ix].a = index_ix;
			if ((int32_t)index_data[index_ix] < 0) {
				// Previous index is the last one of this polygon, rewind to first index.
				while (index_ix > 0 && (int32_t)index_data[index_ix - 1] >= 0) {
					index_ix--;
				}
			} else {
				// Connect to the next index in the same polygon
				index_ix++;
			}
			ufbxi_check(index_ix < mesh->num_indices);
			edges[dst_ix].b = index_ix;
			dst_ix++;
		}

		mesh->edges.data = edges;
		mesh->edges.count = dst_ix;
		mesh->num_edges = mesh->edges.count;
	}

	ufbxi_check(ufbxi_process_indices(uc, mesh, index_data));

	// Count the number of UV/color sets
	size_t num_uv = 0, num_color = 0, num_bitangents = 0, num_tangents = 0;
	ufbxi_for (ufbxi_node, n, node->children, node->num_children) {
		if (n->name == ufbxi_LayerElementUV) num_uv++;
		if (n->name == ufbxi_LayerElementColor) num_color++;
		if (n->name == ufbxi_LayerElementBinormal) num_bitangents++;
		if (n->name == ufbxi_LayerElementTangent) num_tangents++;
	}

	size_t num_textures = 0;

	ufbxi_tangent_layer *bitangents = ufbxi_push_zero(&uc->tmp_stack, ufbxi_tangent_layer, num_bitangents);
	ufbxi_tangent_layer *tangents = ufbxi_push_zero(&uc->tmp_stack, ufbxi_tangent_layer, num_tangents);
	ufbxi_check(bitangents);
	ufbxi_check(tangents);

	mesh->uv_sets.data = ufbxi_push_zero(&uc->result, ufbx_uv_set, num_uv);
	mesh->color_sets.data = ufbxi_push_zero(&uc->result, ufbx_color_set, num_color);
	ufbxi_check(mesh->uv_sets.data);
	ufbxi_check(mesh->color_sets.data);

	size_t num_bitangents_read = 0, num_tangents_read = 0;
	ufbxi_for (ufbxi_node, n, node->children, node->num_children) {
		if (n->name[0] != 'L') continue; // All names start with 'LayerElement*'

		if (n->name == ufbxi_LayerElementNormal) {
			if (mesh->vertex_normal.exists) continue;
			ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&mesh->vertex_normal,
				ufbxi_Normals, ufbxi_NormalsIndex, ufbxi_NormalsW, 'r', 3));
		} else if (n->name == ufbxi_LayerElementBinormal) {
			ufbxi_tangent_layer *layer = &bitangents[num_bitangents_read++];

			ufbxi_ignore(ufbxi_get_val1(n, "I", &layer->index));
			ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&layer->elem,
				ufbxi_Binormals, ufbxi_BinormalsIndex, ufbxi_BinormalsW, 'r', 3));
			if (!layer->elem.exists) num_bitangents_read--;

		} else if (n->name == ufbxi_LayerElementTangent) {
			ufbxi_tangent_layer *layer = &tangents[num_tangents_read++];

			ufbxi_ignore(ufbxi_get_val1(n, "I", &layer->index));
			ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&layer->elem,
				ufbxi_Tangents, ufbxi_TangentsIndex, ufbxi_TangentsW, 'r', 3));
			if (!layer->elem.exists) num_tangents_read--;

		} else if (n->name == ufbxi_LayerElementUV) {
			ufbx_uv_set *set = &mesh->uv_sets.data[mesh->uv_sets.count++];

			ufbxi_ignore(ufbxi_get_val1(n, "I", &set->index));
			if (!ufbxi_find_val1(n, ufbxi_Name, "S", &set->name)) {
				set->name = ufbx_empty_string;
			}

			ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&set->vertex_uv,
				ufbxi_UV, ufbxi_UVIndex, NULL, 'r', 2));
			if (!set->vertex_uv.exists) mesh->uv_sets.count--;

		} else if (n->name == ufbxi_LayerElementColor) {
			ufbx_color_set *set = &mesh->color_sets.data[mesh->color_sets.count++];

			ufbxi_ignore(ufbxi_get_val1(n, "I", &set->index));
			if (!ufbxi_find_val1(n, ufbxi_Name, "S", &set->name)) {
				set->name = ufbx_empty_string;
			}

			ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&set->vertex_color,
				ufbxi_Colors, ufbxi_ColorIndex, NULL, 'r', 4));
			if (!set->vertex_color.exists) mesh->color_sets.count--;

		} else if (n->name == ufbxi_LayerElementVertexCrease) {
			ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&mesh->vertex_crease,
				ufbxi_VertexCrease, ufbxi_VertexCreaseIndex, NULL, 'r', 1));
		} else if (n->name == ufbxi_LayerElementEdgeCrease) {
			const char *mapping = "";
			ufbxi_ignore(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping));
			if (mapping == ufbxi_ByEdge) {
				if (mesh->edge_crease.count) continue;
				ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->edge_crease.data, &mesh->edge_crease.count, n, ufbxi_EdgeCrease, 'r', mesh->num_edges));
			} else {
				ufbxi_check(ufbxi_warn_polygon_mapping(uc, ufbxi_EdgeCrease, mapping));
			}
		} else if (n->name == ufbxi_LayerElementSmoothing) {
			const char *mapping = "";
			ufbxi_ignore(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping));
			if (mapping == ufbxi_ByEdge) {
				if (mesh->edge_smoothing.count) continue;
				ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->edge_smoothing.data, &mesh->edge_smoothing.count, n, ufbxi_Smoothing, 'b', mesh->num_edges));
			} else if (mapping == ufbxi_ByPolygon) {
				if (mesh->face_smoothing.count) continue;
				ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->face_smoothing.data, &mesh->face_smoothing.count, n, ufbxi_Smoothing, 'b', mesh->num_faces));
			} else {
				ufbxi_check(ufbxi_warn_polygon_mapping(uc, ufbxi_Smoothing, mapping));
			}
		} else if (n->name == ufbxi_LayerElementVisibility) {
			const char *mapping = "";
			ufbxi_ignore(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping));
			if (mapping == ufbxi_ByEdge) {
				if (mesh->edge_visibility.count) continue;
				ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->edge_visibility.data, &mesh->edge_visibility.count, n, ufbxi_Visibility, 'b', mesh->num_edges));
			} else {
				ufbxi_check(ufbxi_warn_polygon_mapping(uc, ufbxi_Visibility, mapping));
			}
		} else if (n->name == ufbxi_LayerElementMaterial) {
			if (mesh->face_material.count) continue;
			const char *mapping = "";
			ufbxi_ignore(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping));
			if (mapping == ufbxi_ByPolygon) {
				ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->face_material.data, &mesh->face_material.count, n, ufbxi_Materials, 'i', mesh->num_faces));
			} else if (mapping == ufbxi_AllSame) {
				ufbxi_value_array *arr = ufbxi_find_array(n, ufbxi_Materials, 'i');
				ufbxi_check(arr && arr->size >= 1);
				uint32_t material = *(uint32_t*)arr->data;
				mesh->face_material.count = mesh->num_faces;
				if (material == 0) {
					mesh->face_material.data = (uint32_t*)ufbxi_sentinel_index_zero;
				} else {
					mesh->face_material.data = ufbxi_push(&uc->result, uint32_t, mesh->num_faces);
					ufbxi_check(mesh->face_material.data);
					ufbxi_for_list(uint32_t, p_mat, mesh->face_material) {
						*p_mat = material;
					}
				}
			} else {
				ufbxi_check(ufbxi_warn_polygon_mapping(uc, ufbxi_Materials, mapping));
			}
		} else if (n->name == ufbxi_LayerElementPolygonGroup) {
			if (mesh->face_group.count) continue;
			const char *mapping = NULL;
			ufbxi_check(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping));
			if (mapping == ufbxi_ByPolygon) {
				ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->face_group.data, &mesh->face_group.count, n, ufbxi_PolygonGroup, 'i', mesh->num_faces));
			}
		} else if (n->name == ufbxi_LayerElementHole) {
			if (mesh->face_group.count) continue;
			const char *mapping = NULL;
			ufbxi_check(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping));
			if (mapping == ufbxi_ByPolygon) {
				ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->face_hole.data, &mesh->face_hole.count, n, ufbxi_Hole, 'b', mesh->num_faces));
			}
		} else if (!strncmp(n->name, "LayerElement", 12)) {

			// Make sure the name has no internal zero bytes
			ufbxi_check(!memchr(n->name, '\0', n->name_len));

			// What?! 6x00 stores textures in mesh geometry, eg. "LayerElementTexture",
			// "LayerElementDiffuseFactorTextures", "LayerElementEmissive_Textures"...
			ufbx_string prop_name = ufbx_empty_string;
			if (n->name_len > 20 && !strcmp(n->name + n->name_len - 8, "Textures")) {
				prop_name.data = n->name + 12;
				prop_name.length = (size_t)n->name_len - 20;
				if (prop_name.data[prop_name.length - 1] == '_') {
					prop_name.length -= 1;
				}
			} else if (!strcmp(n->name, "LayerElementTexture")) {
				prop_name.data = "Diffuse";
				prop_name.length = 7;
			}

			if (prop_name.length > 0) {
				ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &prop_name, false));
				const char *mapping = NULL;
				if (ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)) {
					ufbxi_value_array *arr = ufbxi_find_array(n, ufbxi_TextureId, 'i');

					ufbxi_tmp_mesh_texture *tex = ufbxi_push_zero(&uc->tmp_mesh_textures, ufbxi_tmp_mesh_texture, 1);
					ufbxi_check(tex);
					if (arr) {
						tex->face_texture = (uint32_t*)arr->data;
						tex->num_faces = arr->size;
					}
					tex->prop_name = prop_name;
					tex->all_same = (mapping == ufbxi_AllSame);
					num_textures++;
				}
			}
		}
	}

	// Always use a default zero material, this will be removed if no materials are found
	if (!mesh->face_material.count) {
		uc->max_zero_indices = ufbxi_max_sz(uc->max_zero_indices, mesh->num_faces);
		mesh->face_material.data = (uint32_t*)ufbxi_sentinel_index_zero;
		mesh->face_material.count = mesh->num_faces;
	}

	if (uc->opts.strict) {
		ufbxi_check(mesh->uv_sets.count == num_uv);
		ufbxi_check(mesh->color_sets.count == num_color);
		ufbxi_check(num_bitangents_read == num_bitangents);
		ufbxi_check(num_tangents_read == num_tangents);
	}

	// Connect bitangents/tangents to UV sets
	ufbxi_for (ufbxi_node, n, node->children, node->num_children) {
		if (n->name != ufbxi_Layer) continue;
		ufbx_uv_set *uv_set = NULL;
		ufbxi_tangent_layer *bitangent_layer = NULL;
		ufbxi_tangent_layer *tangent_layer = NULL;

		ufbxi_for (ufbxi_node, c, n->children, n->num_children) {
			uint32_t index;
			const char *type;
			if (c->name != ufbxi_LayerElement) continue;
			if (!ufbxi_find_val1(c, ufbxi_TypedIndex, "I", &index)) continue;
			if (!ufbxi_find_val1(c, ufbxi_Type, "C", (char**)&type)) continue;

			if (type == ufbxi_LayerElementUV) {
				ufbxi_for(ufbx_uv_set, set, mesh->uv_sets.data, mesh->uv_sets.count) {
					if (set->index == index) {
						uv_set = set;
						break;
					}
				}
			} else if (type == ufbxi_LayerElementBinormal) {
				ufbxi_for(ufbxi_tangent_layer, layer, bitangents, num_bitangents_read) {
					if (layer->index == index) {
						bitangent_layer = layer;
						break;
					}
				}
			} else if (type == ufbxi_LayerElementTangent) {
				ufbxi_for(ufbxi_tangent_layer, layer, tangents, num_tangents_read) {
					if (layer->index == index) {
						tangent_layer = layer;
						break;
					}
				}
			}
		}

		if (uv_set) {
			if (bitangent_layer) {
				uv_set->vertex_bitangent = bitangent_layer->elem;
			}
			if (tangent_layer) {
				uv_set->vertex_tangent = tangent_layer->elem;
			}
		}
	}

	mesh->skinned_is_local = true;
	mesh->skinned_position = mesh->vertex_position;
	mesh->skinned_normal = mesh->vertex_normal;

	ufbxi_patch_mesh_reals(mesh);

	if (mesh->face_group.count > 0 && mesh->face_groups.count == 0) {
		ufbxi_check(ufbxi_assign_face_groups(&uc->result, &uc->error, mesh, &uc->max_consecutive_indices, uc->retain_mesh_parts));
	}

	// Sort UV and color sets by set index
	ufbxi_check(ufbxi_sort_uv_sets(uc, mesh->uv_sets.data, mesh->uv_sets.count));
	ufbxi_check(ufbxi_sort_color_sets(uc, mesh->color_sets.data, mesh->color_sets.count));

	if (num_textures > 0) {
		ufbxi_mesh_extra *extra = ufbxi_push_element_extra(uc, mesh->element.element_id, ufbxi_mesh_extra);
		ufbxi_check(extra);
		extra->texture_count = num_textures;
		extra->texture_arr = ufbxi_push_pop(&uc->tmp, &uc->tmp_mesh_textures, ufbxi_tmp_mesh_texture, num_textures);
		ufbxi_check(extra->texture_arr);
	}

	// Subdivision

	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_PreviewDivisionLevels, "I", &mesh->subdivision_preview_levels));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RenderDivisionLevels, "I", &mesh->subdivision_render_levels));

	int32_t smoothness, boundary;
	if (ufbxi_find_val1(node, ufbxi_Smoothness, "I", &smoothness)) {
		if (smoothness >= 0 && smoothness <= UFBX_SUBDIVISION_DISPLAY_SMOOTH) {
			mesh->subdivision_display_mode = (ufbx_subdivision_display_mode)smoothness;
		}
	}
	if (ufbxi_find_val1(node, ufbxi_BoundaryRule, "I", &boundary)) {
		if (boundary >= 0 && boundary <= UFBX_SUBDIVISION_BOUNDARY_SHARP_CORNERS - 1) {
			mesh->subdivision_boundary = (ufbx_subdivision_boundary)(boundary + 1);
		}
	}

	return 1;
}

ufbxi_noinline static ufbx_nurbs_topology ufbxi_read_nurbs_topology(const char *form)
{
	if (!strcmp(form, "Open")) {
		return UFBX_NURBS_TOPOLOGY_OPEN;
	} else if (!strcmp(form, "Closed")) {
		return UFBX_NURBS_TOPOLOGY_CLOSED;
	} else if (!strcmp(form, "Periodic")) {
		return UFBX_NURBS_TOPOLOGY_PERIODIC;
	}
	return UFBX_NURBS_TOPOLOGY_OPEN;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_nurbs_curve(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_nurbs_curve *nurbs = ufbxi_push_element(uc, info, ufbx_nurbs_curve, UFBX_ELEMENT_NURBS_CURVE);
	ufbxi_check(nurbs);

	int32_t dimension = 3;

	const char *form = NULL;
	ufbxi_check(ufbxi_find_val1(node, ufbxi_Order, "I", &nurbs->basis.order));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Dimension, "I", &dimension));
	ufbxi_check(ufbxi_find_val1(node, ufbxi_Form, "C", (char**)&form));
	nurbs->basis.topology = ufbxi_read_nurbs_topology(form);
	nurbs->basis.is_2d = dimension == 2;

	if (!uc->opts.ignore_geometry) {
		ufbxi_value_array *points = ufbxi_find_array(node, ufbxi_Points, 'r');
		ufbxi_value_array *knot = ufbxi_find_array(node, ufbxi_KnotVector, 'r');
		ufbxi_check(points);
		ufbxi_check(knot);
		ufbxi_check(points->size % 4 == 0);

		nurbs->control_points.count = points->size / 4;
		nurbs->control_points.data = (ufbx_vec4*)points->data;
		nurbs->basis.knot_vector.data = (ufbx_real*)knot->data;
		nurbs->basis.knot_vector.count = knot->size;
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_nurbs_surface(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_nurbs_surface *nurbs = ufbxi_push_element(uc, info, ufbx_nurbs_surface, UFBX_ELEMENT_NURBS_SURFACE);
	ufbxi_check(nurbs);

	const char *form_u = NULL, *form_v = NULL;
	size_t dimension_u = 0, dimension_v = 0;
	int32_t step_u = 0, step_v = 0;
	ufbxi_check(ufbxi_find_val2(node, ufbxi_NurbsSurfaceOrder, "II", &nurbs->basis_u.order, &nurbs->basis_v.order));
	ufbxi_check(ufbxi_find_val2(node, ufbxi_Dimensions, "ZZ", &dimension_u, &dimension_v));
	ufbxi_check(ufbxi_find_val2(node, ufbxi_Step, "II", &step_u, &step_v));
	ufbxi_check(ufbxi_find_val2(node, ufbxi_Form, "CC", (char**)&form_u, (char**)&form_v));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_FlipNormals, "B", &nurbs->flip_normals));
	nurbs->basis_u.topology = ufbxi_read_nurbs_topology(form_u);
	nurbs->basis_v.topology = ufbxi_read_nurbs_topology(form_v);
	nurbs->num_control_points_u = dimension_u;
	nurbs->num_control_points_v = dimension_v;
	nurbs->span_subdivision_u = step_u > 0 ? (uint32_t)step_u : 4u;
	nurbs->span_subdivision_v = step_v > 0 ? (uint32_t)step_v : 4u;

	if (!uc->opts.ignore_geometry) {
		ufbxi_value_array *points = ufbxi_find_array(node, ufbxi_Points, 'r');
		ufbxi_value_array *knot_u = ufbxi_find_array(node, ufbxi_KnotVectorU, 'r');
		ufbxi_value_array *knot_v = ufbxi_find_array(node, ufbxi_KnotVectorV, 'r');
		ufbxi_check(points);
		ufbxi_check(knot_u);
		ufbxi_check(knot_v);
		ufbxi_check(points->size % 4 == 0);
		ufbxi_check(points->size / 4 == (size_t)dimension_u * (size_t)dimension_v);

		nurbs->control_points.count = points->size / 4;
		nurbs->control_points.data = (ufbx_vec4*)points->data;
		nurbs->basis_u.knot_vector.data = (ufbx_real*)knot_u->data;
		nurbs->basis_u.knot_vector.count = knot_u->size;
		nurbs->basis_v.knot_vector.data = (ufbx_real*)knot_v->data;
		nurbs->basis_v.knot_vector.count = knot_v->size;
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_line(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_line_curve *line = ufbxi_push_element(uc, info, ufbx_line_curve, UFBX_ELEMENT_LINE_CURVE);
	ufbxi_check(line);

	if (!uc->opts.ignore_geometry) {
		ufbxi_value_array *points = ufbxi_find_array(node, ufbxi_Points, 'r');
		ufbxi_value_array *points_index = ufbxi_find_array(node, ufbxi_PointsIndex, 'i');
		ufbxi_check(points);
		ufbxi_check(points_index);
		ufbxi_check(points->size % 3 == 0);

		if (points->size > 0) {
			line->control_points.count = points->size / 3;
			line->control_points.data = (ufbx_vec3*)points->data;
			line->point_indices.count = points_index->size;
			line->point_indices.data = (uint32_t*)points_index->data;

			ufbxi_check(line->control_points.count < INT32_MAX);

			// Count end points
			size_t num_segments = 1;
			if (line->point_indices.count > 0) {
				for (size_t i = 0; i < line->point_indices.count - 1; i++) {
					uint32_t ix = line->point_indices.data[i];
					num_segments += (int32_t)ix < 0 ? 1u : 0u;
				}
			}

			size_t prev_end = 0;
			line->segments.data = ufbxi_push(&uc->result, ufbx_line_segment, num_segments);
			ufbxi_check(line->segments.data);
			for (size_t i = 0; i < line->point_indices.count; i++) {
				uint32_t ix = line->point_indices.data[i];
				if ((int32_t)ix < 0) {
					ix = ~ix;
					if (i + 1 < line->point_indices.count) {
						ufbx_line_segment *segment = &line->segments.data[line->segments.count++];
						segment->index_begin = (uint32_t)prev_end;
						segment->num_indices = (uint32_t)(i - prev_end);
						prev_end = i;
					}
				}

				if (ix < line->control_points.count) {
					line->point_indices.data[i] = ix;
				} else {
					ufbxi_check(ufbxi_fix_index(uc, &line->point_indices.data[i], ix, line->control_points.count));
				}
			}

			ufbx_line_segment *segment = &line->segments.data[line->segments.count++];
			segment->index_begin = (uint32_t)prev_end;
			segment->num_indices = (uint32_t)ufbxi_to_size(line->point_indices.count - prev_end);
			ufbx_assert(line->segments.count == num_segments);
		}
	}

	return 1;
}

ufbxi_noinline static void ufbxi_read_transform_matrix(ufbx_matrix *m, ufbx_real *data)
{
	m->m00 = data[ 0]; m->m10 = data[ 1]; m->m20 = data[ 2];
	m->m01 = data[ 4]; m->m11 = data[ 5]; m->m21 = data[ 6];
	m->m02 = data[ 8]; m->m12 = data[ 9]; m->m22 = data[10];
	m->m03 = data[12]; m->m13 = data[13]; m->m23 = data[14];
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_bone(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info, const char *sub_type)
{
	(void)node;

	ufbx_bone *bone = ufbxi_push_element(uc, info, ufbx_bone, UFBX_ELEMENT_BONE);
	ufbxi_check(bone);

	if (sub_type == ufbxi_Root) {
		bone->is_root = true;
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_marker(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info, const char *sub_type, ufbx_marker_type type)
{
	(void)node;
	(void)sub_type;

	ufbx_marker *marker = ufbxi_push_element(uc, info, ufbx_marker, UFBX_ELEMENT_MARKER);
	ufbxi_check(marker);

	marker->type = type;

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_skin(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_skin_deformer *skin = ufbxi_push_element(uc, info, ufbx_skin_deformer, UFBX_ELEMENT_SKIN_DEFORMER);
	ufbxi_check(skin);

	const char *skinning_type = NULL;
	if (ufbxi_find_val1(node, ufbxi_SkinningType, "C", (char**)&skinning_type)) {
		if (!strcmp(skinning_type, "Rigid")) {
			skin->skinning_method = UFBX_SKINNING_METHOD_RIGID;
		} else if (!strcmp(skinning_type, "Linear")) {
			skin->skinning_method = UFBX_SKINNING_METHOD_LINEAR;
		} else if (!strcmp(skinning_type, "DualQuaternion")) {
			skin->skinning_method = UFBX_SKINNING_METHOD_DUAL_QUATERNION;
		} else if (!strcmp(skinning_type, "Blend")) {
			skin->skinning_method = UFBX_SKINNING_METHOD_BLENDED_DQ_LINEAR;
		}
	}

	ufbxi_value_array *indices = ufbxi_find_array(node, ufbxi_Indexes, 'i');
	ufbxi_value_array *weights = ufbxi_find_array(node, ufbxi_BlendWeights, 'r');
	if (indices && weights) {
		// TODO strict: ufbxi_check(indices->size == weights->size);
		skin->num_dq_weights = ufbxi_min_sz(indices->size, weights->size);
		skin->dq_vertices.data = (uint32_t*)indices->data;
		skin->dq_weights.data = (ufbx_real*)weights->data;
		skin->dq_vertices.count = skin->num_dq_weights;
		skin->dq_weights.count = skin->num_dq_weights;
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_skin_cluster(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_skin_cluster *cluster = ufbxi_push_element(uc, info, ufbx_skin_cluster, UFBX_ELEMENT_SKIN_CLUSTER);
	ufbxi_check(cluster);

	ufbxi_value_array *indices = ufbxi_find_array(node, ufbxi_Indexes, 'i');
	ufbxi_value_array *weights = ufbxi_find_array(node, ufbxi_Weights, 'r');

	if (indices && weights) {
		ufbxi_check(indices->size == weights->size);
		cluster->num_weights = indices->size;
		cluster->vertices.data = (uint32_t*)indices->data;
		cluster->weights.data = (ufbx_real*)weights->data;
		cluster->vertices.count = cluster->num_weights;
		cluster->weights.count = cluster->num_weights;
	}

	ufbxi_value_array *transform = ufbxi_find_array(node, ufbxi_Transform, 'r');
	ufbxi_value_array *transform_link = ufbxi_find_array(node, ufbxi_TransformLink, 'r');
	if (transform && transform_link) {
		ufbxi_check(transform->size >= 16);
		ufbxi_check(transform_link->size >= 16);

		ufbxi_read_transform_matrix(&cluster->mesh_node_to_bone, (ufbx_real*)transform->data);
		ufbxi_read_transform_matrix(&cluster->bind_to_world, (ufbx_real*)transform_link->data);
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_blend_channel(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_blend_channel *channel = ufbxi_push_element(uc, info, ufbx_blend_channel, UFBX_ELEMENT_BLEND_CHANNEL);
	ufbxi_check(channel);

	ufbx_real_list list = { NULL, 0 };
	ufbxi_value_array *full_weights = ufbxi_find_array(node, ufbxi_FullWeights, 'r');
	if (full_weights) {
		list.data = (ufbx_real*)full_weights->data;
		list.count = full_weights->size;
	}
	ufbxi_check(ufbxi_push_copy(&uc->tmp_full_weights, ufbx_real_list, 1, &list));

	// Blender saves blend shapes with DeformPercent as a field, not a property.
	// However, the animations are mapped to the DeformPercent property.
	ufbxi_node *deform_percent = ufbxi_find_child(node, ufbxi_DeformPercent);
	if (channel->props.props.count == 0 && deform_percent) {
		size_t num_shape_props = 1;
		ufbx_prop *shape_props = ufbxi_push_zero(&uc->result, ufbx_prop, num_shape_props);
		ufbxi_check(shape_props);
		shape_props[0].name.data = ufbxi_DeformPercent;
		shape_props[0].name.length = sizeof(ufbxi_DeformPercent) - 1;
		shape_props[0]._internal_key = ufbxi_get_name_key_c(ufbxi_DeformPercent);
		shape_props[0].type = UFBX_PROP_NUMBER;
		shape_props[0].value_str = ufbx_empty_string;
		shape_props[0].value_real = 100.0f;
		ufbxi_ignore(ufbxi_get_val1(deform_percent, "R", &shape_props[0].value_real));
		channel->props.props.data = shape_props;
		channel->props.props.count = num_shape_props;
	}

	return 1;
}

typedef enum {
	UFBXI_KEY_INTERPOLATION_CONSTANT = 0x2,
	UFBXI_KEY_INTERPOLATION_LINEAR = 0x4,
	UFBXI_KEY_INTERPOLATION_CUBIC = 0x8,
	UFBXI_KEY_TANGENT_AUTO = 0x100,
	UFBXI_KEY_TANGENT_TCB = 0x200,
	UFBXI_KEY_TANGENT_USER = 0x400,
	UFBXI_KEY_TANGENT_BROKEN = 0x800,
	UFBXI_KEY_CONSTANT_NEXT = 0x100,
	UFBXI_KEY_CLAMP = 0x1000,
	UFBXI_KEY_TIME_INDEPENDENT = 0x2000,
	UFBXI_KEY_CLAMP_PROGRESSIVE = 0x4000,
	UFBXI_KEY_WEIGHTED_RIGHT = 0x1000000,
	UFBXI_KEY_WEIGHTED_NEXT_LEFT = 0x2000000,
	UFBXI_KEY_VELOCITY_RIGHT = 0x10000000,
	UFBXI_KEY_VELOCITY_NEXT_LEFT = 0x20000000,
} ufbxi_key_flags;

static ufbxi_noinline float ufbxi_solve_auto_tangent(ufbxi_context *uc, double prev_time, double time, double next_time, ufbx_real prev_value, ufbx_real value, ufbx_real next_value, float weight_left, float weight_right, float auto_bias, uint32_t flags)
{
	// Clamp tangent to zero if near either left or right key
	if (flags & UFBXI_KEY_CLAMP) {
		if (ufbx_fmin(ufbx_fabs(prev_value - value), ufbx_fabs(next_value - value)) <= uc->opts.key_clamp_threshold) {
			return 0.0f;
		}
	}

	// Time-independent: Set the initial slope to be the difference between the two keyframes.
	double slope = (next_value - prev_value) / (next_time - prev_time);

	// Non-time-independent tangents seem to blend between left/right tangent and the total difference.
	if ((flags & UFBXI_KEY_TIME_INDEPENDENT) == 0) {
		double slope_left = (value - prev_value) / (time - prev_time);
		double slope_right = (next_value - value) / (next_time - time);
		double delta = (time - prev_time) / (next_time - prev_time);
		slope = slope * 0.5 + (slope_left * (1.0 - delta) + slope_right * delta) * 0.5;

		double bias_weight = ufbx_fabs(auto_bias) / 100.0;
		if (bias_weight > 0.0001) {
			double bias_target = auto_bias > 0.0 ? slope_right : slope_left;
			double bias_delta = bias_target - slope;
			slope = slope * (1.0 - bias_weight) + bias_target * bias_weight;

			// Auto bias larger than 500 (positive or negative) adds an absolute
			// value to the slope, determined by `((bias-500) / 100)^2 * 40`.
			double abs_bias_weight = bias_weight - 5.0;
			if (abs_bias_weight > 0.0) {
				double bias_sign = ufbx_fabs(bias_delta) > 0.00001 ? bias_delta : auto_bias;
				bias_sign = bias_sign > 0.0 ? 1.0 : -1.0;
				slope += abs_bias_weight * abs_bias_weight * bias_sign * 40.0;
			}
		}
	}

	// Prevent overshooting by clamping the slope in case either
	// tangent goes above/below the endpoints.
	if (flags & UFBXI_KEY_CLAMP_PROGRESSIVE) {
		// Split the slope to sign and a non-negative absolute value
		double slope_sign = slope >= 0.0 ? 1.0 : -1.0;
		double abs_slope = slope_sign * slope;

		// Find limits for the absolute value of the slope
		double range_left = weight_left * (time - prev_time);
		double range_right = weight_right * (next_time - time);
		double max_left = range_left > 0.0 ? slope_sign * (value - prev_value) / range_left : 0.0;
		double max_right = range_right > 0.0 ? slope_sign * (next_value - value) / range_right : 0.0;

		// Clamp negative values and NaNs to zero
		if (!(max_left > 0.0)) max_left = 0.0;
		if (!(max_right > 0.0)) max_right = 0.0;

		// Clamp the absolute slope from both sides
		if (abs_slope > max_left) abs_slope = max_left;
		if (abs_slope > max_right) abs_slope = max_right;

		slope = (slope_sign * abs_slope);
	}

	return (float)slope;
}

static float ufbxi_solve_auto_tangent_left(ufbxi_context *uc, double prev_time, double time, ufbx_real prev_value, ufbx_real value, float weight_left, float auto_bias, uint32_t flags)
{
	(void)weight_left;
	if (flags & UFBXI_KEY_CLAMP_PROGRESSIVE) return 0.0f;
	if (flags & UFBXI_KEY_CLAMP) {
		if (ufbx_fabs(prev_value - value) <= uc->opts.key_clamp_threshold) {
			return 0.0f;
		}
	}

	double slope = (value - prev_value) / (time - prev_time);

	if ((flags & UFBXI_KEY_TIME_INDEPENDENT) == 0) {
		double abs_bias_weight = ufbx_fabs(auto_bias) / 100.0 - 5.0;
		if (abs_bias_weight > 0.0) {
			double bias_sign = auto_bias > 0.0 ? 1.0 : -1.0;
			slope += abs_bias_weight * abs_bias_weight * bias_sign * 40.0;
		}
	}

	return (float)slope;
}

static float ufbxi_solve_auto_tangent_right(ufbxi_context *uc, double time, double next_time, ufbx_real value, ufbx_real next_value, float weight_right, float auto_bias, uint32_t flags)
{
	(void)weight_right;
	if (flags & UFBXI_KEY_CLAMP_PROGRESSIVE) return 0.0f;
	if (flags & UFBXI_KEY_CLAMP) {
		if (ufbx_fabs(next_value - value) <= uc->opts.key_clamp_threshold) {
			return 0.0f;
		}
	}

	double slope = (next_value - value) / (next_time - time);

	if ((flags & UFBXI_KEY_TIME_INDEPENDENT) == 0) {
		double abs_bias_weight = ufbx_fabs(auto_bias) / 100.0 - 5.0;
		if (abs_bias_weight > 0.0) {
			double bias_sign = auto_bias > 0.0 ? 1.0 : -1.0;
			slope += abs_bias_weight * abs_bias_weight * bias_sign * 40.0;
		}
	}

	return (float)slope;
}

static void ufbxi_solve_tcb(float *p_slope_left, float *p_slope_right, double tension, double continuity, double bias, double slope_left, double slope_right, bool edge)
{
	double factor = edge ? 1.0 : 0.5;
	double d00 = factor * (1.0 - tension) * (1.0 + bias) * (1.0 - continuity);
	double d01 = factor * (1.0 - tension) * (1.0 - bias) * (1.0 + continuity);
	double d10 = factor * (1.0 - tension) * (1.0 + bias) * (1.0 + continuity);
	double d11 = factor * (1.0 - tension) * (1.0 - bias) * (1.0 - continuity);

	*p_slope_left = (float)(d00 * slope_left + d01 * slope_right);
	*p_slope_right = (float)(d10 * slope_left + d11 * slope_right);
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_animation_curve(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_anim_curve *curve = ufbxi_push_element(uc, info, ufbx_anim_curve, UFBX_ELEMENT_ANIM_CURVE);
	ufbxi_check(curve);

	if (uc->opts.ignore_animation) return 1;

	ufbxi_value_array *times, *values, *attr_flags, *attrs, *refs;
	ufbxi_check(times = ufbxi_find_array(node, ufbxi_KeyTime, 'l'));
	ufbxi_check(values = ufbxi_find_array(node, ufbxi_KeyValueFloat, 'r'));
	ufbxi_check(attr_flags = ufbxi_find_array(node, ufbxi_KeyAttrFlags, 'i'));
	ufbxi_check(attrs = ufbxi_find_array(node, ufbxi_KeyAttrDataFloat, '?'));
	ufbxi_check(refs = ufbxi_find_array(node, ufbxi_KeyAttrRefCount, 'i'));

	// Time and value arrays that define the keyframes should be parallel
	ufbxi_check(times->size == values->size);

	// Flags and attributes are run-length encoded where KeyAttrRefCount (refs)
	// is an array that describes how many times to repeat a given flag/attribute.
	// Attributes consist of 4 32-bit floating point values per key.
	ufbxi_check(attr_flags->size == refs->size);
	ufbxi_check(attrs->size == refs->size * 4u);

	size_t num_keys = times->size;
	ufbx_keyframe *keys = ufbxi_push(&uc->result, ufbx_keyframe, num_keys);
	ufbxi_check(keys);

	curve->keyframes.data = keys;
	curve->keyframes.count = num_keys;

	int64_t *p_time = (int64_t*)times->data;
	ufbx_real *p_value = (ufbx_real*)values->data;
	int32_t *p_flag = (int32_t*)attr_flags->data;
	float *p_attr = (float*)attrs->data;
	int32_t *p_ref = (int32_t*)refs->data, *p_ref_end = p_ref + refs->size;

	// The previous key defines the weight/slope of the left tangent
	float slope_left = 0.0f;
	float weight_left = 0.333333f;
	// float velocity_left = 0.0f;

	double prev_time = 0.0;
	double next_time = 0.0;

	int32_t refs_left = 0;
	if (num_keys > 0) {
		next_time = (double)p_time[0] / uc->ktime_sec_double;
		if (p_ref < p_ref_end) refs_left = *p_ref;
	}

	for (size_t i = 0; i < num_keys; i++) {
		ufbx_keyframe *key = &keys[i];
		ufbxi_check(refs_left > 0);

		ufbx_real value = *p_value;
		if (i == 0) {
			curve->min_value = value;
			curve->max_value = value;
		} else {
			curve->min_value = ufbxi_min_real(curve->min_value, value);
			curve->max_value = ufbxi_max_real(curve->max_value, value);
		}

		key->time = next_time;
		key->value = value;

		if (i + 1 < num_keys) {
			next_time = (double)p_time[1] / uc->ktime_sec_double;
		}

		uint32_t flags = (uint32_t)*p_flag;

		float slope_right = p_attr[0];
		float weight_right = 0.333333f;
		//float velocity_right = 0.0f;
		float next_slope_left = p_attr[1];
		float next_weight_left = 0.333333f;
		// float next_velocity_left = 0.0f;

		if ((flags & (UFBXI_KEY_WEIGHTED_RIGHT|UFBXI_KEY_WEIGHTED_NEXT_LEFT)) != 0) {
			// At least one of the tangents is weighted. The weights are encoded as
			// two 0.4 _decimal_ fixed point values that are packed into 32 bits and
			// interpreted as a 32-bit float.
			uint32_t packed_weights;
			memcpy(&packed_weights, &p_attr[2], sizeof(uint32_t));

			if (flags & UFBXI_KEY_WEIGHTED_RIGHT) {
				// Right tangent is weighted
				weight_right = (float)(packed_weights & 0xffff) * 0.0001f;
			}

			if (flags & UFBXI_KEY_WEIGHTED_NEXT_LEFT) {
				// Next left tangent is weighted
				next_weight_left = (float)(packed_weights >> 16) * 0.0001f;
			}
		}
#if 0
		if ((flags & (UFBXI_KEY_VELOCITY_RIGHT|UFBXI_KEY_VELOCITY_NEXT_LEFT)) != 0) {
			// Velocities are encoded in the same way as weights, see above.
			uint32_t packed_velocities;
			memcpy(&packed_velocities, &p_attr[3], sizeof(uint32_t));

			if (flags & UFBXI_KEY_VELOCITY_RIGHT) {
				// Right tangent has velocity
				velocity_right = (float)(int16_t)(packed_velocities & 0xffff) * 0.0001f;
			}

			if (flags & UFBXI_KEY_VELOCITY_NEXT_LEFT) {
				// Next left tangent has velocity
				next_velocity_left = (float)(int16_t)(packed_velocities >> 16) * 0.0001f;
			}
		}
#endif

		if (flags & UFBXI_KEY_INTERPOLATION_CONSTANT) {
			// Constant interpolation: Set cubic tangents to flat.

			if (flags & UFBXI_KEY_CONSTANT_NEXT) {
				// Take constant value from next key
				key->interpolation = UFBX_INTERPOLATION_CONSTANT_NEXT;

			} else {
				// Take constant value from the previous key
				key->interpolation = UFBX_INTERPOLATION_CONSTANT_PREV;
			}

			weight_right = next_weight_left = 0.333333f;
			slope_right = next_slope_left = 0.0f;

		} else if (flags & UFBXI_KEY_INTERPOLATION_CUBIC) {
			// Cubic interpolation
			key->interpolation = UFBX_INTERPOLATION_CUBIC;

			if (flags & UFBXI_KEY_TANGENT_TCB) {
				double tcb_slope_left = 0.0;
				double tcb_slope_right = 0.0;
				bool tcb_edge = false;
				if (i > 0 && key->time > prev_time) {
					tcb_slope_left = (key->value - p_value[-1]) / (key->time - prev_time);
				} else {
					tcb_edge = true;
				}
				if (i + 1 < num_keys && next_time > key->time) {
					tcb_slope_right = (p_value[1] - key->value) / (next_time - key->time);
				} else {
					tcb_edge = true;
				}

				ufbxi_solve_tcb(&slope_left, &slope_right, p_attr[0], p_attr[1], p_attr[2], tcb_slope_left, tcb_slope_right, tcb_edge);

				// TODO: How to handle these?
				next_slope_left = 0.0f;
				next_weight_left = 0.333333f;
				// next_velocity_left = 0.0f;
			} else if (flags & UFBXI_KEY_TANGENT_USER) {
				// User tangents

				if (flags & UFBXI_KEY_TANGENT_BROKEN) {
					// Broken tangents: No need to modify slopes
				} else {
					// Unified tangents: Use right slope for both sides
					// TODO: ??? slope_left = slope_right;
				}

			} else {
				// TODO: Auto break (0x800)

				if (i > 0 && i + 1 < num_keys && key->time > prev_time && next_time > key->time) {
					if (ufbx_fabs(slope_left + slope_right) <= 0.0001f) {
						slope_left = slope_right = ufbxi_solve_auto_tangent(uc,
							prev_time, key->time, next_time,
							p_value[-1], key->value, p_value[1],
							weight_left, weight_right, slope_right, flags);
					} else {
						slope_left = ufbxi_solve_auto_tangent(uc,
							prev_time, key->time, next_time,
							p_value[-1], key->value, p_value[1],
							weight_left, weight_right, -slope_left, flags);
						slope_right = ufbxi_solve_auto_tangent(uc,
							prev_time, key->time, next_time,
							p_value[-1], key->value, p_value[1],
							weight_left, weight_right, slope_right, flags);
					}
				} else if (i > 0 && key->time > prev_time) {
					slope_left = slope_right = ufbxi_solve_auto_tangent_left(uc,
						prev_time, key->time,
						p_value[-1], key->value,
						weight_left, -slope_left, flags);
				} else if (i + 1 < num_keys && next_time > key->time) {
					slope_left = slope_right = ufbxi_solve_auto_tangent_right(uc,
						key->time, next_time,
						key->value, p_value[1],
						weight_right, slope_right, flags);
				} else {
					// Only / invalid keyframe: Set both slopes to zero
					slope_left = slope_right = 0.0f;
				}


				// ??? Looks like at least MotionBuilder adjusts weight and auto bias to
				// implement velocity and the velocity information in the file is purely
				// for UI (?) If auto bias is not accounted for the velocity computation
				// below results in the correct tangents, but with auto bias the velocity
				// seems to be accounted for twice resulting in incorrect values...
#if 0
				if (weight_left >= UFBX_EPSILON) {
					slope_left *= (float)(1.0 - ufbx_fmin(velocity_left / weight_left, 1.0));
				}
				if (weight_right >= UFBX_EPSILON) {
					slope_right *= (float)(1.0 - ufbx_fmin(velocity_right / weight_right, 1.0));
				}
#endif
			}

		} else {
			// Linear or unknown interpolation: Set cubic tangents to match
			// the linear interpolation with weights of 1/3.
			key->interpolation = UFBX_INTERPOLATION_LINEAR;

			weight_right = 0.333333f;
			next_weight_left = 0.333333f;

			if (next_time > key->time) {
				double delta_time = next_time - key->time;
				if (delta_time > 0.0) {
					double slope = (p_value[1] - key->value) / delta_time;
					slope_right = next_slope_left = (float)slope;
				} else {
					slope_right = next_slope_left = 0.0f;
				}
			} else {
				slope_right = next_slope_left = 0.0f;
			}
		}

		// Set the tangents based on weights (dx relative to the time difference
		// between the previous/next key) and slope (simply d = slope * dx)
		if (key->time > prev_time) {
			double delta = key->time - prev_time;
			key->left.dx = (float)(weight_left * delta);
			key->left.dy = key->left.dx * slope_left;
		} else {
			key->left.dx = 0.0f;
			key->left.dy = 0.0f;
		}

		if (next_time > key->time) {
			double delta = next_time - key->time;
			key->right.dx = (float)(weight_right * delta);
			key->right.dy = key->right.dx * slope_right;
		} else {
			key->right.dx = 0.0f;
			key->right.dy = 0.0f;
		}

		slope_left = next_slope_left;
		weight_left = next_weight_left;
		// velocity_left = next_velocity_left;
		prev_time = key->time;

		// Decrement attribute refcount and potentially move to the next one.
		if (--refs_left == 0) {
			p_flag++;
			p_attr += 4;
			p_ref++;
			if (p_ref < p_ref_end) refs_left = *p_ref;
		}
		p_time++;
		p_value++;
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_material(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_material *material = ufbxi_push_element(uc, info, ufbx_material, UFBX_ELEMENT_MATERIAL);
	ufbxi_check(material);

	if (!ufbxi_find_val1(node, ufbxi_ShadingModel, "S", &material->shading_model_name)) {
		material->shading_model_name = ufbx_empty_string;
	}

	material->shader_prop_prefix = ufbx_empty_string;

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_texture(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_texture *texture = ufbxi_push_element(uc, info, ufbx_texture, UFBX_ELEMENT_TEXTURE);
	ufbxi_check(texture);

	texture->type = UFBX_TEXTURE_FILE;

	texture->filename = ufbx_empty_string;
	texture->absolute_filename = ufbx_empty_string;
	texture->relative_filename = ufbx_empty_string;

	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_FileName, "S", &texture->absolute_filename));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Filename, "S", &texture->absolute_filename));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFileName, "S", &texture->relative_filename));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFilename, "S", &texture->relative_filename));

	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_FileName, "b", &texture->raw_absolute_filename));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Filename, "b", &texture->raw_absolute_filename));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFileName, "b", &texture->raw_relative_filename));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFilename, "b", &texture->raw_relative_filename));

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_layered_texture(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_texture *texture = ufbxi_push_element(uc, info, ufbx_texture, UFBX_ELEMENT_TEXTURE);
	ufbxi_check(texture);

	texture->type = UFBX_TEXTURE_LAYERED;

	texture->filename = ufbx_empty_string;
	texture->absolute_filename = ufbx_empty_string;
	texture->relative_filename = ufbx_empty_string;

	ufbxi_texture_extra *extra = ufbxi_push_element_extra(uc, texture->element.element_id, ufbxi_texture_extra);
	ufbxi_check(extra);

	ufbxi_value_array *alphas = ufbxi_find_array(node, ufbxi_Alphas, 'r');
	if (alphas) {
		extra->alphas = (ufbx_real*)alphas->data;
		extra->num_alphas = alphas->size;
	}

	ufbxi_value_array *blend_modes = ufbxi_find_array(node, ufbxi_BlendModes, 'i');
	if (blend_modes) {
		extra->blend_modes = (int32_t*)blend_modes->data;
		extra->num_blend_modes = blend_modes->size;
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_video(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_video *video = ufbxi_push_element(uc, info, ufbx_video, UFBX_ELEMENT_VIDEO);
	ufbxi_check(video);

	video->filename = ufbx_empty_string;
	video->absolute_filename = ufbx_empty_string;
	video->relative_filename = ufbx_empty_string;

	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_FileName, "S", &video->absolute_filename));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Filename, "S", &video->absolute_filename));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFileName, "S", &video->relative_filename));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFilename, "S", &video->relative_filename));

	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_FileName, "b", &video->raw_absolute_filename));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Filename, "b", &video->raw_absolute_filename));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFileName, "b", &video->raw_relative_filename));
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFilename, "b", &video->raw_relative_filename));

	ufbxi_node *content_node = ufbxi_find_child(node, ufbxi_Content);
	ufbxi_check(ufbxi_read_embedded_blob(uc, &video->content, content_node));

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_anim_stack(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	(void)node;

	ufbx_anim_stack *stack = ufbxi_push_element(uc, info, ufbx_anim_stack, UFBX_ELEMENT_ANIM_STACK);
	ufbxi_check(stack);

	uint32_t hash = ufbxi_hash_ptr(info->name.data);
	ufbxi_tmp_anim_stack *entry = ufbxi_map_find(&uc->anim_stack_map, ufbxi_tmp_anim_stack, hash, &info->name.data);
	if (!entry) {
		entry = ufbxi_map_insert(&uc->anim_stack_map, ufbxi_tmp_anim_stack, hash, &info->name.data);
		ufbxi_check(entry);
		entry->name = info->name.data;
		entry->stack = stack;
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_pose(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info, const char *sub_type)
{
	ufbx_pose *pose = ufbxi_push_element(uc, info, ufbx_pose, UFBX_ELEMENT_POSE);
	ufbxi_check(pose);

	// TODO: What are the actual other types?
	pose->is_bind_pose = sub_type == ufbxi_BindPose;

	size_t num_bones = 0;
	ufbxi_for(ufbxi_node, n, node->children, node->num_children) {
		if (n->name != ufbxi_PoseNode) continue;

		// Bones are linked with FBX names/IDs bypassing the connection system (!?)
		uint64_t fbx_id = 0;
		if (uc->version < 7000) {
			char *name = NULL;
			if (!ufbxi_find_val1(n, ufbxi_Node, "c", &name)) continue;
			fbx_id = ufbxi_synthetic_id_from_string(name);
		} else {
			if (!ufbxi_find_val1(n, ufbxi_Node, "L", &fbx_id)) continue;
		}

		ufbxi_value_array *matrix = ufbxi_find_array(n, ufbxi_Matrix, 'r');
		if (!matrix) continue;
		ufbxi_check(matrix->size >= 16);

		ufbxi_tmp_bone_pose *tmp_pose = ufbxi_push(&uc->tmp_stack, ufbxi_tmp_bone_pose, 1);
		ufbxi_check(tmp_pose);

		num_bones++;
		tmp_pose->bone_fbx_id = fbx_id;
		ufbxi_read_transform_matrix(&tmp_pose->bone_to_world, (ufbx_real*)matrix->data);
	}

	// HACK: Transport `ufbxi_tmp_bone_pose` array through the `ufbx_bone_pose` pointer
	pose->bone_poses.count = num_bones;
	pose->bone_poses.data = (ufbx_bone_pose*)ufbxi_push_pop(&uc->tmp, &uc->tmp_stack, ufbxi_tmp_bone_pose, num_bones);
	ufbxi_check(pose->bone_poses.data);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_shader_prop_bindings(ufbxi_context *uc, ufbx_shader_prop_binding *bindings, size_t count)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_shader_prop_binding)));
	ufbxi_macro_stable_sort(ufbx_shader_prop_binding, 32, bindings, uc->tmp_arr, count,
		( ufbxi_str_less(a->shader_prop, b->shader_prop) ) );
	return 1;
}


ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_binding_table(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_shader_binding *bindings = ufbxi_push_element(uc, info, ufbx_shader_binding, UFBX_ELEMENT_SHADER_BINDING);
	ufbxi_check(bindings);

	size_t num_entries = 0;
	ufbxi_for (ufbxi_node, n, node->children, node->num_children) {
		if (n->name != ufbxi_Entry) continue;

		ufbx_string src, dst;
		const char *src_type = NULL, *dst_type = NULL;
		if (!ufbxi_get_val4(n, "SCSC", &src, (char**)&src_type, &dst, (char**)&dst_type)) {
			continue;
		}

		if (src_type == ufbxi_FbxPropertyEntry && dst_type == ufbxi_FbxSemanticEntry) {
			ufbx_shader_prop_binding *bind = ufbxi_push(&uc->tmp_stack, ufbx_shader_prop_binding, 1);
			ufbxi_check(bind);
			bind->material_prop = src;
			bind->shader_prop = dst;
			num_entries++;
		} else if (src_type == ufbxi_FbxSemanticEntry && dst_type == ufbxi_FbxPropertyEntry) {
			ufbx_shader_prop_binding *bind = ufbxi_push(&uc->tmp_stack, ufbx_shader_prop_binding, 1);
			ufbxi_check(bind);
			bind->material_prop = dst;
			bind->shader_prop = src;
			num_entries++;
		}
	}

	bindings->prop_bindings.count = num_entries;
	bindings->prop_bindings.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_shader_prop_binding, num_entries);
	ufbxi_check(bindings->prop_bindings.data);

	ufbxi_check(ufbxi_sort_shader_prop_bindings(uc, bindings->prop_bindings.data, bindings->prop_bindings.count));

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_selection_set(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	(void)node;

	ufbx_selection_set *set = ufbxi_push_element(uc, info, ufbx_selection_set, UFBX_ELEMENT_SELECTION_SET);
	ufbxi_check(set);

	return 1;
}

ufbxi_noinline static void ufbxi_find_uint32_list(ufbx_uint32_list *dst, ufbxi_node *node, const char *name)
{
	ufbxi_value_array *arr = ufbxi_find_array(node, name, 'i');
	if (arr) {
		dst->data = (uint32_t*)arr->data;
		dst->count = arr->size;
	}
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_selection_node(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_selection_node *sel = ufbxi_push_element(uc, info, ufbx_selection_node, UFBX_ELEMENT_SELECTION_NODE);
	ufbxi_check(sel);

	int32_t in_set = 0;
	if (ufbxi_find_val1(node, ufbxi_IsTheNodeInSet, "I", &in_set) && in_set) {
		sel->include_node = true;
	}

	ufbxi_find_uint32_list(&sel->vertices, node, ufbxi_VertexIndexArray);
	ufbxi_find_uint32_list(&sel->edges, node, ufbxi_EdgeIndexArray);
	ufbxi_find_uint32_list(&sel->faces, node, ufbxi_PolygonIndexArray);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_character(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	(void)node;

	ufbx_character *character = ufbxi_push_element(uc, info, ufbx_character, UFBX_ELEMENT_CHARACTER);
	ufbxi_check(character);

	// TODO: There's some extremely cursed all-caps data in characters

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_audio_clip(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_audio_clip *audio = ufbxi_push_element(uc, info, ufbx_audio_clip, UFBX_ELEMENT_AUDIO_CLIP);
	ufbxi_check(audio);

	audio->filename = ufbx_empty_string;
	audio->absolute_filename = ufbx_empty_string;
	audio->relative_filename = ufbx_empty_string;

	ufbxi_node *content_node = ufbxi_find_child(node, ufbxi_Content);
	ufbxi_check(ufbxi_read_embedded_blob(uc, &audio->content, content_node));

	return 1;
}

typedef struct {
	ufbx_constraint_type type;
	const char *name;
} ufbxi_constraint_type;

static const ufbxi_constraint_type ufbxi_constraint_types[] = {
	{ UFBX_CONSTRAINT_AIM, "Aim" },
	{ UFBX_CONSTRAINT_PARENT, "Parent-Child" },
	{ UFBX_CONSTRAINT_POSITION, "Position From Positions" },
	{ UFBX_CONSTRAINT_ROTATION, "Rotation From Rotations" },
	{ UFBX_CONSTRAINT_SCALE, "Scale From Scales" },
	{ UFBX_CONSTRAINT_SINGLE_CHAIN_IK, "Single Chain IK" },
};

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_constraint(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	(void)node;

	ufbx_constraint *constraint = ufbxi_push_element(uc, info, ufbx_constraint, UFBX_ELEMENT_CONSTRAINT);
	ufbxi_check(constraint);

	if (!ufbxi_find_val1(node, ufbxi_Type, "S", &constraint->type_name)) {
		constraint->type_name = ufbx_empty_string;
	}

	ufbxi_for(const ufbxi_constraint_type, ctype, ufbxi_constraint_types, ufbxi_arraycount(ufbxi_constraint_types)) {
		if (!strcmp(constraint->type_name.data, ctype->name)) {
			constraint->type = ctype->type;
			break;
		}
	}

	// TODO: There's some extremely cursed all-caps data in characters

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_synthetic_attribute(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info, ufbx_string type_str, const char *sub_type, const char *super_type)
{
	if ((sub_type == ufbxi_empty_char || sub_type == ufbxi_Model) && type_str.data == ufbxi_Model) {
		// Plain model
		return 1;
	}

	ufbxi_element_info attrib_info = *info;

	ufbxi_check(ufbxi_push_synthetic_id(uc, &attrib_info.fbx_id));

	// Use type and name from NodeAttributeName if it exists *uniquely*
	ufbx_string type_and_name;
	if (ufbxi_find_val1(node, ufbxi_NodeAttributeName, "s", &type_and_name)) {
		ufbx_string attrib_type_str, attrib_name_str;
		ufbxi_check(ufbxi_split_type_and_name(uc, type_and_name, &attrib_type_str, &attrib_name_str));
		if (attrib_name_str.length > 0) {
			attrib_info.name = attrib_name_str;
			uint64_t attrib_id = ufbxi_synthetic_id_from_string(type_and_name.data);
			if (info->fbx_id != attrib_id && !ufbxi_fbx_id_exists(uc, attrib_id)) {
				attrib_info.fbx_id = attrib_id;
			}
		}
	}

	// 6x00: Link the node to the node attribute so property connections can be
	// redirected from connections if necessary.
	if (uc->version < 7000) {
		ufbxi_check(ufbxi_insert_fbx_attr(uc, info->fbx_id, attrib_info.fbx_id));

		// Split properties between the node and the attribute
		ufbx_prop *ps = info->props.props.data;
		size_t dst = 0, src = 0, end = info->props.props.count;
		while (src < end) {
			if (!ufbxi_is_node_property(uc, ps[src].name.data)) {
				ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_prop, 1, &ps[src]));
				src++;
			} else if (dst != src) {
				ps[dst++] = ps[src++];
			} else {
				dst++; src++;
			}
		}
		attrib_info.props.props.count = end - dst;
		attrib_info.props.props.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_prop, attrib_info.props.props.count);
		ufbxi_check(attrib_info.props.props.data);
		info->props.props.count = dst;
	}

	if (sub_type == ufbxi_Mesh) {
		ufbxi_check(ufbxi_read_mesh(uc, node, &attrib_info));
	} else if (sub_type == ufbxi_Light) {
		ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_light), UFBX_ELEMENT_LIGHT));
	} else if (sub_type == ufbxi_Camera) {
		ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_camera), UFBX_ELEMENT_CAMERA));
	} else if (sub_type == ufbxi_LimbNode || sub_type == ufbxi_Limb || sub_type == ufbxi_Root) {
		ufbxi_check(ufbxi_read_bone(uc, node, &attrib_info, sub_type));
	} else if (sub_type == ufbxi_Null || sub_type == ufbxi_Marker) {
		ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_empty), UFBX_ELEMENT_EMPTY));
	} else if (sub_type == ufbxi_NurbsCurve) {
		if (!ufbxi_find_child(node, ufbxi_KnotVector)) return 1;
		ufbxi_check(ufbxi_read_nurbs_curve(uc, node, &attrib_info));
	} else if (sub_type == ufbxi_NurbsSurface) {
		if (!ufbxi_find_child(node, ufbxi_KnotVectorU)) return 1;
		if (!ufbxi_find_child(node, ufbxi_KnotVectorV)) return 1;
		ufbxi_check(ufbxi_read_nurbs_surface(uc, node, &attrib_info));
	} else if (sub_type == ufbxi_Line) {
		if (!ufbxi_find_child(node, ufbxi_Points)) return 1;
		if (!ufbxi_find_child(node, ufbxi_PointsIndex)) return 1;
		ufbxi_check(ufbxi_read_line(uc, node, &attrib_info));
	} else if (sub_type == ufbxi_TrimNurbsSurface) {
		if (!ufbxi_find_child(node, ufbxi_Layer)) return 1;
		ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_nurbs_trim_surface), UFBX_ELEMENT_NURBS_TRIM_SURFACE));
	} else if (sub_type == ufbxi_Boundary) {
		ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_nurbs_trim_boundary), UFBX_ELEMENT_NURBS_TRIM_BOUNDARY));
	} else if (sub_type == ufbxi_CameraStereo) {
		ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_stereo_camera), UFBX_ELEMENT_STEREO_CAMERA));
	} else if (sub_type == ufbxi_CameraSwitcher) {
		ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_camera_switcher), UFBX_ELEMENT_CAMERA_SWITCHER));
	} else if (sub_type == ufbxi_FKEffector) {
		ufbxi_check(ufbxi_read_marker(uc, node, &attrib_info, sub_type, UFBX_MARKER_FK_EFFECTOR));
	} else if (sub_type == ufbxi_IKEffector) {
		ufbxi_check(ufbxi_read_marker(uc, node, &attrib_info, sub_type, UFBX_MARKER_IK_EFFECTOR));
	} else if (sub_type == ufbxi_LodGroup) {
		ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_lod_group), UFBX_ELEMENT_LOD_GROUP));
	} else {
		ufbx_string sub_type_str = { sub_type, strlen(sub_type) };
		ufbxi_check(ufbxi_read_unknown(uc, node, &attrib_info, type_str, sub_type_str, super_type));
	}

	ufbxi_check(ufbxi_connect_oo(uc, attrib_info.fbx_id, info->fbx_id));
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_global_settings(ufbxi_context *uc, ufbxi_node *node)
{
	ufbxi_check(ufbxi_read_properties(uc, node, &uc->scene.settings.props));
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_object(ufbxi_context *uc, ufbxi_node *node)
{
	ufbxi_element_info info = { 0 };
	info.dom_node = ufbxi_get_dom_node(uc, node);

	if (node->name == ufbxi_GlobalSettings) {
		ufbxi_check(ufbxi_read_global_settings(uc, node));
		return 1;
	}

	ufbx_string type_and_name, sub_type_str;

	// Failing to parse the object properties is not an error since
	// there's some weird objects mixed in every now and then.
	// FBX version 7000 and up uses 64-bit unique IDs per object,
	// older FBX versions just use name/type pairs, which we can
	// use as IDs since all strings are interned into a string pool.
	if (uc->version >= 7000) {
		if (!ufbxi_get_val3(node, "Lss", &info.fbx_id, &type_and_name, &sub_type_str)) return 1;
		ufbxi_check((info.fbx_id & UFBXI_SYNTHETIC_ID_BIT) == 0);
	} else {
		if (!ufbxi_get_val2(node, "ss", &type_and_name, &sub_type_str)) return 1;
		info.fbx_id = ufbxi_synthetic_id_from_string(type_and_name.data);
	}

	// Remove the "Fbx" prefix from sub-types, remember to re-intern!
	if (sub_type_str.length > 3 && !memcmp(sub_type_str.data, "Fbx", 3)) {
		sub_type_str.data += 3;
		sub_type_str.length -= 3;
		ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &sub_type_str, false));
	}

	ufbx_string type_str;
	ufbxi_check(ufbxi_split_type_and_name(uc, type_and_name, &type_str, &info.name));

	const char *name = node->name, *sub_type = sub_type_str.data;
	ufbxi_check(ufbxi_read_properties(uc, node, &info.props));
	info.props.defaults = ufbxi_find_template(uc, name, sub_type);

	if (name == ufbxi_Model) {
		if (uc->version < 7000) {
			ufbxi_check(ufbxi_read_synthetic_attribute(uc, node, &info, type_str, sub_type, name));
		}
		ufbxi_check(ufbxi_read_model(uc, node, &info));
	} else if (name == ufbxi_NodeAttribute) {
		if (sub_type == ufbxi_Light) {
			ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_light), UFBX_ELEMENT_LIGHT));
		} else if (sub_type == ufbxi_Camera) {
			ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_camera), UFBX_ELEMENT_CAMERA));
		} else if (sub_type == ufbxi_LimbNode || sub_type == ufbxi_Limb || sub_type == ufbxi_Root) {
			ufbxi_check(ufbxi_read_bone(uc, node, &info, sub_type));
		} else if (sub_type == ufbxi_Null || sub_type == ufbxi_Marker) {
			ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_empty), UFBX_ELEMENT_EMPTY));
		} else if (sub_type == ufbxi_CameraStereo) {
			ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_stereo_camera), UFBX_ELEMENT_STEREO_CAMERA));
		} else if (sub_type == ufbxi_CameraSwitcher) {
			ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_camera_switcher), UFBX_ELEMENT_CAMERA_SWITCHER));
		} else if (sub_type == ufbxi_FKEffector) {
			ufbxi_check(ufbxi_read_marker(uc, node, &info, sub_type, UFBX_MARKER_FK_EFFECTOR));
		} else if (sub_type == ufbxi_IKEffector) {
			ufbxi_check(ufbxi_read_marker(uc, node, &info, sub_type, UFBX_MARKER_IK_EFFECTOR));
		} else if (sub_type == ufbxi_LodGroup) {
			ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_lod_group), UFBX_ELEMENT_LOD_GROUP));
		} else {
			ufbxi_check(ufbxi_read_unknown(uc, node, &info, type_str, sub_type_str, name));
		}
	} else if (name == ufbxi_Geometry) {
		if (sub_type == ufbxi_Mesh) {
			ufbxi_check(ufbxi_read_mesh(uc, node, &info));
		} else if (sub_type == ufbxi_Shape) {
			ufbxi_check(ufbxi_read_shape(uc, node, &info));
		} else if (sub_type == ufbxi_NurbsCurve) {
			ufbxi_check(ufbxi_read_nurbs_curve(uc, node, &info));
		} else if (sub_type == ufbxi_NurbsSurface) {
			ufbxi_check(ufbxi_read_nurbs_surface(uc, node, &info));
		} else if (sub_type == ufbxi_Line) {
			ufbxi_check(ufbxi_read_line(uc, node, &info));
		} else if (sub_type == ufbxi_TrimNurbsSurface) {
			ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_nurbs_trim_surface), UFBX_ELEMENT_NURBS_TRIM_SURFACE));
		} else if (sub_type == ufbxi_Boundary) {
			ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_nurbs_trim_boundary), UFBX_ELEMENT_NURBS_TRIM_BOUNDARY));
		} else {
			ufbxi_check(ufbxi_read_unknown(uc, node, &info, type_str, sub_type_str, name));
		}
	} else if (name == ufbxi_Deformer) {
		if (sub_type == ufbxi_Skin) {
			ufbxi_check(ufbxi_read_skin(uc, node, &info));
		} else if (sub_type == ufbxi_Cluster) {
			ufbxi_check(ufbxi_read_skin_cluster(uc, node, &info));
		} else if (sub_type == ufbxi_BlendShape) {
			ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_blend_deformer), UFBX_ELEMENT_BLEND_DEFORMER));
		} else if (sub_type == ufbxi_BlendShapeChannel) {
			ufbxi_check(ufbxi_read_blend_channel(uc, node, &info));
		} else if (sub_type == ufbxi_VertexCacheDeformer) {
			ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_cache_deformer), UFBX_ELEMENT_CACHE_DEFORMER));
		} else {
			ufbxi_check(ufbxi_read_unknown(uc, node, &info, type_str, sub_type_str, name));
		}
	} else if (name == ufbxi_Material) {
		ufbxi_check(ufbxi_read_material(uc, node, &info));
	} else if (name == ufbxi_Texture) {
		ufbxi_check(ufbxi_read_texture(uc, node, &info));
	} else if (name == ufbxi_LayeredTexture) {
		ufbxi_check(ufbxi_read_layered_texture(uc, node, &info));
	} else if (name == ufbxi_Video) {
		ufbxi_check(ufbxi_read_video(uc, node, &info));
	} else if (name == ufbxi_AnimationStack) {
		ufbxi_check(ufbxi_read_anim_stack(uc, node, &info));
	} else if (name == ufbxi_AnimationLayer) {
		ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_anim_layer), UFBX_ELEMENT_ANIM_LAYER));
	} else if (name == ufbxi_AnimationCurveNode) {
		ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_anim_value), UFBX_ELEMENT_ANIM_VALUE));
	} else if (name == ufbxi_AnimationCurve) {
		ufbxi_check(ufbxi_read_animation_curve(uc, node, &info));
	} else if (name == ufbxi_Pose) {
		ufbxi_check(ufbxi_read_pose(uc, node, &info, sub_type));
	} else if (name == ufbxi_Implementation) {
		ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_shader), UFBX_ELEMENT_SHADER));
	} else if (name == ufbxi_BindingTable) {
		ufbxi_check(ufbxi_read_binding_table(uc, node, &info));
	} else if (name == ufbxi_Collection) {
		if (sub_type == ufbxi_SelectionSet) {
			ufbxi_check(ufbxi_read_selection_set(uc, node, &info));
		}
	} else if (name == ufbxi_CollectionExclusive) {
		if (sub_type == ufbxi_DisplayLayer) {
			ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_display_layer), UFBX_ELEMENT_DISPLAY_LAYER));
		}
	} else if (name == ufbxi_SelectionNode) {
		ufbxi_check(ufbxi_read_selection_node(uc, node, &info));
	} else if (name == ufbxi_Constraint) {
		if (sub_type == ufbxi_Character) {
			ufbxi_check(ufbxi_read_character(uc, node, &info));
		} else {
			ufbxi_check(ufbxi_read_constraint(uc, node, &info));
		}
	} else if (name == ufbxi_SceneInfo) {
		ufbxi_check(ufbxi_read_scene_info(uc, node));
	} else if (name == ufbxi_Cache) {
		ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_cache_file), UFBX_ELEMENT_CACHE_FILE));
	} else if (name == ufbxi_ObjectMetaData) {
		ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_metadata_object), UFBX_ELEMENT_METADATA_OBJECT));
	} else if (name == ufbxi_AudioLayer) {
		ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_audio_layer), UFBX_ELEMENT_AUDIO_LAYER));
	} else if (name == ufbxi_Audio) {
		ufbxi_check(ufbxi_read_audio_clip(uc, node, &info));
	} else {
		ufbxi_check(ufbxi_read_unknown(uc, node, &info, type_str, sub_type_str, name));
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_objects(ufbxi_context *uc)
{
	for (;;) {
		// Push a deferred element ID for tagging warnings
		uc->p_element_id = ufbxi_push(&uc->tmp_element_id, uint32_t, 1);
		ufbxi_check(uc->p_element_id);
		*uc->p_element_id = UFBX_NO_INDEX;
		uc->warnings.deferred_element_id_plus_one = (uint32_t)uc->tmp_element_id.num_items;

		ufbxi_node *node;
		ufbxi_check(ufbxi_parse_toplevel_child(uc, &node, NULL));
		if (!node) break;

		ufbxi_check(ufbxi_read_object(uc, node));

		uc->warnings.deferred_element_id_plus_one = 0;
		uc->p_element_id = NULL;
	}

	return 1;
}

typedef struct {
	ufbxi_node **nodes;
	size_t num_nodes;
	uint32_t task_index;
} ufbxi_object_batch;

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_objects_threaded(ufbxi_context *uc)
{
	uc->parse_threaded = true;

	bool parsed_to_end = false;
	ufbxi_object_batch batches[UFBX_THREAD_GROUP_COUNT]; // ufbxi_uninit
	memset(batches, 0, sizeof(batches));

	size_t empty_count = 0;
	size_t batch_index = 0;
	while (empty_count < UFBX_THREAD_GROUP_COUNT) {
		ufbxi_object_batch *batch = &batches[batch_index];

		ufbxi_check(ufbxi_thread_pool_wait_group(&uc->thread_pool));

		if (batch->num_nodes > 0) {
			ufbxi_for_ptr(ufbxi_node, p_node, batch->nodes, batch->num_nodes) {
				ufbxi_buf_clear(&uc->tmp_parse);

				// Push a deferred element ID for tagging warnings
				uc->p_element_id = ufbxi_push(&uc->tmp_element_id, uint32_t, 1);
				ufbxi_check(uc->p_element_id);
				*uc->p_element_id = UFBX_NO_INDEX;
				uc->warnings.deferred_element_id_plus_one = (uint32_t)uc->tmp_element_id.num_items;

				ufbxi_check(ufbxi_read_object(uc, *p_node));

				uc->warnings.deferred_element_id_plus_one = 0;
				uc->p_element_id = NULL;
			}
			batch->num_nodes = 0;
		}

		ufbxi_buf *tmp_buf = &uc->tmp_thread_parse[batch_index];

		// ASCII data may be in `tmp_buf`, so copy it to safety in case
		if (uc->ascii.src_buf == tmp_buf) {
			ufbxi_ascii *ua = &uc->ascii;
			size_t size = ufbxi_to_size(ua->src_end - ua->src);
			if (uc->read_buffer_size < size) {
				ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->read_buffer, &uc->read_buffer_size, size));
			}
			memcpy(uc->read_buffer, ua->src, size);
			uc->data = uc->data_begin = ua->src = uc->read_buffer;
			ua->src_end = uc->read_buffer + size;
			ua->src_is_retained = false;
			ua->src_buf = NULL;
			if (ufbxi_to_size(ua->src_end - ua->src) < uc->progress_interval) {
				ua->src_yield = ua->src_end;
			} else {
				ua->src_yield = ua->src + uc->progress_interval;
			}
			uc->data = ua->src;
		}

		ufbxi_buf_clear(tmp_buf);

		if (!parsed_to_end) {
			size_t num_nodes = 0;
			uint32_t task_start = uc->thread_pool.start_index;
			uint32_t max_tasks = uc->thread_pool.num_tasks / UFBX_THREAD_GROUP_COUNT;
			max_tasks = ufbxi_min32(max_tasks, ufbxi_thread_pool_available_tasks(&uc->thread_pool));
			size_t max_memory = uc->opts.thread_opts.memory_limit / UFBX_THREAD_GROUP_COUNT;

			for (;;) {
				ufbxi_node *node = NULL;
				ufbxi_check(ufbxi_parse_toplevel_child(uc, &node, tmp_buf));
				if (!node) {
					parsed_to_end = true;
					break;
				}
				ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbxi_node*, 1, &node));
				num_nodes++;

				uint32_t num_tasks = uc->thread_pool.start_index - task_start;
				if (num_tasks >= max_tasks) break;

				size_t memory_used = tmp_buf->pushed_size + tmp_buf->pos;
				if (memory_used >= max_memory) break;
			}

			batch->num_nodes = num_nodes;
			batch->nodes = ufbxi_push_pop(tmp_buf, &uc->tmp_stack, ufbxi_node*, num_nodes);
			ufbxi_check(batch->nodes);
			batch->task_index = uc->thread_pool.start_index;

		}

		// Not safe to refer to this buffer anymore
		uc->ascii.src_is_retained = false;

		ufbxi_thread_pool_flush_group(&uc->thread_pool);

		if (batch->num_nodes == 0) {
			empty_count += 1;
		}

		batch_index = (batch_index + 1) % UFBX_THREAD_GROUP_COUNT;
	}

	ufbxi_check(ufbxi_thread_pool_wait_all(&uc->thread_pool));

	uc->parse_threaded = false;

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_connections(ufbxi_context *uc)
{
	// Read the connections to the list first
	for (;;) {
		ufbxi_node *node;
		ufbxi_check(ufbxi_parse_toplevel_child(uc, &node, NULL));
		if (!node) break;

		char *type;

		uint64_t src_id = 0, dst_id = 0;
		ufbx_string src_prop = ufbx_empty_string, dst_prop = ufbx_empty_string;

		if (uc->version < 7000) {
			char *src_name = NULL, *dst_name = NULL;
			// Pre-7000 versions use Type::Name pairs as identifiers

			if (!ufbxi_get_val1(node, "c", &type)) continue;

			if (type == ufbxi_OO) {
				if (!ufbxi_get_val3(node, "_cc", NULL, &src_name, &dst_name)) continue;
			} else if (type == ufbxi_OP) {
				if (!ufbxi_get_val4(node, "_ccs", NULL, &src_name, &dst_name, &dst_prop)) continue;
			} else if (type == ufbxi_PO) {
				if (!ufbxi_get_val4(node, "_csc", NULL, &src_name, &src_prop, &dst_name)) continue;
			} else if (type == ufbxi_PP) {
				if (!ufbxi_get_val5(node, "_cscs", NULL, &src_name, &src_prop, &dst_name, &dst_prop)) continue;
			} else {
				// TODO: Strict mode?
				continue;
			}

			if (src_prop.length > 0) {
				ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &src_prop, false));
			}
			if (dst_prop.length > 0) {
				ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &dst_prop, false));
			}

			src_id = ufbxi_synthetic_id_from_string(src_name);
			dst_id = ufbxi_synthetic_id_from_string(dst_name);

		} else {
			// Post-7000 versions use proper unique 64-bit IDs

			if (!ufbxi_get_val1(node, "C", &type)) continue;

			if (type == ufbxi_OO) {
				if (!ufbxi_get_val3(node, "_LL", NULL, &src_id, &dst_id)) continue;
			} else if (type == ufbxi_OP) {
				if (!ufbxi_get_val4(node, "_LLS", NULL, &src_id, &dst_id, &dst_prop)) continue;
			} else if (type == ufbxi_PO) {
				if (!ufbxi_get_val4(node, "_LSL", NULL, &src_id, &src_prop, &dst_id)) continue;
			} else if (type == ufbxi_PP) {
				if (!ufbxi_get_val5(node, "_LSLS", NULL, &src_id, &src_prop, &dst_id, &dst_prop)) continue;
			} else {
				// TODO: Strict mode?
				continue;
			}
		}

		ufbxi_tmp_connection *conn = ufbxi_push(&uc->tmp_connections, ufbxi_tmp_connection, 1);
		ufbxi_check(conn);
		conn->src = src_id;
		conn->dst = dst_id;
		conn->src_prop = src_prop;
		conn->dst_prop = dst_prop;
	}

	return 1;
}

// -- Pre-7000 "Take" based animation

ufbxi_forceinline static char ufbxi_double_to_char(double value)
{
	if (value >= 0.0 && value <= 127.0) {
		return (char)(int)value;
	} else {
		return 0;
	}
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_take_anim_channel(ufbxi_context *uc, ufbxi_node *node, uint64_t value_fbx_id, const char *name, ufbx_real *p_default)
{
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Default, "R", p_default));

	// Find the key array, early return with success if not found as we may have only a default
	ufbxi_value_array *keys = ufbxi_find_array(node, ufbxi_Key, 'd');
	if (!keys) return 1;

	uint64_t curve_fbx_id = 0;
	ufbx_anim_curve *curve = ufbxi_push_synthetic_element(uc, &curve_fbx_id, node, name, ufbx_anim_curve, UFBX_ELEMENT_ANIM_CURVE);
	ufbxi_check(curve);

	ufbxi_check(ufbxi_connect_op(uc, curve_fbx_id, value_fbx_id, curve->name));

	if (uc->opts.ignore_animation) return 1;

	size_t num_keys = 0;
	ufbxi_check(ufbxi_find_val1(node, ufbxi_KeyCount, "Z", &num_keys));
	curve->keyframes.data = ufbxi_push(&uc->result, ufbx_keyframe, num_keys);
	curve->keyframes.count = num_keys;
	ufbxi_check(curve->keyframes.data);

	float slope_left = 0.0f;
	float weight_left = 0.333333f;

	double next_time = 0.0;
	double next_value = 0.0;
	double prev_time = 0.0;

	// The pre-7000 keyframe data is stored as a _heterogenous_ array containing 64-bit integers,
	// floating point values, and _bare characters_. We cast all values to double and interpret them.
	double *data = (double*)keys->data, *data_end = data + keys->size;

	if (num_keys > 0) {
		ufbxi_check(data_end - data >= 2);
		next_time = data[0] / uc->ktime_sec_double;
		next_value = data[1];
	}

	for (size_t i = 0; i < num_keys; i++) {
		ufbx_keyframe *key = &curve->keyframes.data[i];

		if (i == 0) {
			curve->min_value = (ufbx_real)next_value;
			curve->max_value = (ufbx_real)next_value;
		} else {
			curve->min_value = ufbxi_min_real(curve->min_value, (ufbx_real)next_value);
			curve->max_value = ufbxi_max_real(curve->max_value, (ufbx_real)next_value);
		}

		// First three values: Time, Value, InterpolationMode
		ufbxi_check(data_end - data >= 3);
		key->time = next_time;
		key->value = (ufbx_real)next_value;
		char mode = ufbxi_double_to_char(data[2]);
		data += 3;

		float slope_right = 0.0f;
		float weight_right = 0.333333f;
		float next_slope_left = 0.0f;
		float next_weight_left = 0.333333f;
		bool auto_slope = false;

		if (mode == 'U') {
			// Cubic interpolation
			key->interpolation = UFBX_INTERPOLATION_CUBIC;

			ufbxi_check(data_end - data >= 1);
			char slope_mode = ufbxi_double_to_char(data[0]);
			data += 1;

			size_t num_weights = 1;
			if (slope_mode == 's' || slope_mode == 'b') {
				// Slope mode 's'/'b' (standard? broken?) always have two explicit slopes
				// TODO: `b` might actually be some kind of TCB curve
				ufbxi_check(data_end - data >= 2);
				slope_right = (float)data[0];
				next_slope_left = (float)data[1];
				data += 2;
			} else if (slope_mode == 'a') {
				// Parameterless slope mode 'a' seems to appear in baked animations. Let's just assume
				// automatic tangents for now as they're the least likely to break with
				// objectionable artifacts. We need to defer the automatic tangent resolve
				// until we have read the next time/value.
				// TODO: Solve what this is more throroughly
				auto_slope = true;
				if (uc->version == 5000) {
					num_weights = 0;
				}
			} else if (slope_mode == 'p') {
				// TODO: What is this mode? It seems to have negative values sometimes?
				// Also it seems to have _two_ trailing weights values, currently observed:
				// `n,n` and `a,X,Y,n`...
				// Ignore unknown values for now
				ufbxi_check(data_end - data >= 2);
				data += 2;
				num_weights = 2;
			} else if (slope_mode == 't') {
				// TODO: What is this mode? It seems that it does not have any weights and the
				// third value seems _tiny_ (around 1e-30?)
				ufbxi_check(data_end - data >= 3);
				data += 3;
				num_weights = 0;
			} else {
				ufbxi_fail("Unknown slope mode");
			}

			for (; num_weights > 0; num_weights--) {
				ufbxi_check(data_end - data >= 1);
				char weight_mode = ufbxi_double_to_char(data[0]);
				data += 1;

				if (weight_mode == 'n') {
					// Automatic weights (0.3333...)
				} else if (weight_mode == 'a') {
					// Manual weights: RightWeight, NextLeftWeight
					ufbxi_check(data_end - data >= 2);
					weight_right = (float)data[0];
					next_weight_left = (float)data[1];
					data += 2;
				} else if (weight_mode == 'l') {
					// Next left tangent is weighted
					ufbxi_check(data_end - data >= 1);
					next_weight_left = (float)data[0];
					data += 1;
				} else if (weight_mode == 'r') {
					// Right tangent is weighted
					ufbxi_check(data_end - data >= 1);
					weight_right = (float)data[0];
					data += 1;
				} else if (weight_mode == 'c') {
					// TODO: What is this mode? At least it has no parameters so let's
					// just assume automatic weights for the time being (0.3333...)
				} else {
					ufbxi_fail("Unknown weight mode");
				}
			}

		} else if (mode == 'L') {
			// Linear interpolation: No parameters
			key->interpolation = UFBX_INTERPOLATION_LINEAR;
		} else if (mode == 'C') {
			// Constant interpolation: Single parameter (use prev/next)
			ufbxi_check(data_end - data >= 1);
			key->interpolation = ufbxi_double_to_char(data[0]) == 'n' ? UFBX_INTERPOLATION_CONSTANT_NEXT : UFBX_INTERPOLATION_CONSTANT_PREV;
			data += 1;
		} else {
			ufbxi_fail("Unknown key mode");
		}

		// Retrieve next key and value
		if (i + 1 < num_keys) {
			ufbxi_check(data_end - data >= 2);
			next_time = data[0] / uc->ktime_sec_double;
			next_value = data[1];
		}

		if (auto_slope) {
			if (i > 0) {
				slope_left = slope_right = ufbxi_solve_auto_tangent(uc,
					prev_time, key->time, next_time,
					key[-1].value, key->value, (ufbx_real)next_value,
					weight_left, weight_right, 0.0f, UFBXI_KEY_CLAMP_PROGRESSIVE|UFBXI_KEY_TIME_INDEPENDENT);
			} else {
				slope_left = slope_right = 0.0f;
			}
		}

		// Set up linear cubic tangents if necessary
		if (key->interpolation == UFBX_INTERPOLATION_LINEAR) {
			if (next_time > key->time) {
				double slope = (next_value - key->value) / (next_time - key->time);
				slope_right = next_slope_left = (float)slope;
			} else {
				slope_right = next_slope_left = 0.0f;
			}
		}

		if (key->time > prev_time) {
			double delta = key->time - prev_time;
			key->left.dx = (float)(weight_left * delta);
			key->left.dy = key->left.dx * slope_left;
		} else {
			key->left.dx = 0.0f;
			key->left.dy = 0.0f;
		}

		if (next_time > key->time) {
			double delta = next_time - key->time;
			key->right.dx = (float)(weight_right * delta);
			key->right.dy = key->right.dx * slope_right;
		} else {
			key->right.dx = 0.0f;
			key->right.dy = 0.0f;
		}

		slope_left = next_slope_left;
		weight_left = next_weight_left;
		prev_time = key->time;
	}

	ufbxi_check(data == data_end);

	return 1;
}

// Recursion limited as it is further called only for `name="T"/"R"/"S"` and
// cannot enter the `name=="Transform"` branch.
ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_take_prop_channel(ufbxi_context *uc, ufbxi_node *node, uint64_t target_fbx_id, uint64_t layer_fbx_id, ufbx_string name)
	ufbxi_recursive_function(int, ufbxi_read_take_prop_channel, (uc, node, target_fbx_id, layer_fbx_id, name), 2,
		(ufbxi_context *uc, ufbxi_node *node, uint64_t target_fbx_id, uint64_t layer_fbx_id, ufbx_string name))
{
	if (name.data == ufbxi_Transform) {
		// Pre-7000 have transform keyframes in a deeply nested structure,
		// flatten it to make it resemble post-7000 structure a bit closer:
		// old: Model: { Channel: "Transform" { Channel: "T" { Channel "X": { ... } } } }
		// new: Model: { Channel: "Lcl Translation" { Channel "X": { ... } } }

		ufbxi_for(ufbxi_node, child, node->children, node->num_children) {
			if (child->name != ufbxi_Channel) continue;

			const char *old_name = NULL;
			ufbxi_check(ufbxi_get_val1(child, "C", (char**)&old_name));

			ufbx_string new_name;
			if (old_name == ufbxi_T) { new_name.data = ufbxi_Lcl_Translation; new_name.length = sizeof(ufbxi_Lcl_Translation) - 1; }
			else if (old_name == ufbxi_R) { new_name.data = ufbxi_Lcl_Rotation; new_name.length = sizeof(ufbxi_Lcl_Rotation) - 1; }
			else if (old_name == ufbxi_S) { new_name.data = ufbxi_Lcl_Scaling; new_name.length = sizeof(ufbxi_Lcl_Scaling) - 1; }
			else {
				continue;
			}

			// Read child as a top-level property channel
			ufbxi_check(ufbxi_read_take_prop_channel(uc, child, target_fbx_id, layer_fbx_id, new_name));
		}

	} else {

		// Pre-6000 FBX files store blend shape keys with a " (Shape)" suffix
		if (uc->version < 6000) {
			const char *const suffix = " (Shape)";
			size_t suffix_len = strlen(suffix);
			if (name.length > suffix_len && !memcmp(name.data + name.length - suffix_len, suffix, suffix_len)) {
				name.length -= suffix_len;
				ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &name, false));
			}
		}

		// Find 1-3 channel nodes that contain a `Key:` node
		ufbxi_node *channel_nodes[3] = { 0 };
		const char *channel_names[3] = { 0 };
		size_t num_channel_nodes = 0;

		if (ufbxi_find_child(node, ufbxi_Key) || ufbxi_find_child(node, ufbxi_Default)) {
			// Channel has only a single curve
			channel_nodes[0] = node;
			channel_names[0] = name.data;
			num_channel_nodes = 1;
		} else {
			// Channel is a compound of multiple curves
			ufbxi_for(ufbxi_node, child, node->children, node->num_children) {
				if (child->name != ufbxi_Channel) continue;
				if (!ufbxi_find_child(child, ufbxi_Key) && !ufbxi_find_child(child, ufbxi_Default)) continue;
				if (!ufbxi_get_val1(child, "C", (char**)&channel_names[num_channel_nodes])) continue;
				channel_nodes[num_channel_nodes] = child;
				if (++num_channel_nodes == 3) break;
			}
		}

		// Early return: No valid channels found, not an error
		if (num_channel_nodes == 0) return 1;

		uint64_t value_fbx_id = 0;
		ufbx_anim_value *value = ufbxi_push_synthetic_element(uc, &value_fbx_id, node, name.data, ufbx_anim_value, UFBX_ELEMENT_ANIM_VALUE);

		// Add a "virtual" connection between the animated property and the layer/target
		ufbxi_check(ufbxi_connect_oo(uc, value_fbx_id, layer_fbx_id));
		ufbxi_check(ufbxi_connect_op(uc, value_fbx_id, target_fbx_id, name));

		for (size_t i = 0; i < num_channel_nodes; i++) {
			ufbxi_check(ufbxi_read_take_anim_channel(uc, channel_nodes[i], value_fbx_id, channel_names[i], &value->default_value.v[i]));
		}
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_take_object(ufbxi_context *uc, ufbxi_node *node, uint64_t layer_fbx_id)
{
	// Takes are used only in pre-7000 FBX versions so objects are identified
	// by their unique Type::Name pair that we use as unique IDs through the
	// pooled interned string pointers.
	const char *type_and_name = NULL;
	ufbxi_check(ufbxi_get_val1(node, "c", (char**)&type_and_name));
	uint64_t target_fbx_id = ufbxi_synthetic_id_from_string(type_and_name);

	// Add all suitable Channels as animated properties
	ufbxi_for(ufbxi_node, child, node->children, node->num_children) {
		ufbx_string name;
		if (child->name != ufbxi_Channel) continue;
		if (!ufbxi_get_val1(child, "S", &name)) continue;

		ufbxi_check(ufbxi_read_take_prop_channel(uc, child, target_fbx_id, layer_fbx_id, name));
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_take(ufbxi_context *uc, ufbxi_node *node)
{
	ufbx_prop tmp_props[4];
	uint32_t num_props = 0;
	memset(tmp_props, 0, sizeof(tmp_props));

	int64_t start = 0, stop = 0;
	if (ufbxi_find_val2(node, ufbxi_LocalTime, "LL", &start, &stop)) {
		ufbxi_init_synthetic_int_prop(&tmp_props[num_props++], ufbxi_LocalStart, start, UFBX_PROP_INTEGER);
		ufbxi_init_synthetic_int_prop(&tmp_props[num_props++], ufbxi_LocalStop, stop, UFBX_PROP_INTEGER);
	}
	if (ufbxi_find_val2(node, ufbxi_ReferenceTime, "LL", &start, &stop)) {
		ufbxi_init_synthetic_int_prop(&tmp_props[num_props++], ufbxi_ReferenceStart, start, UFBX_PROP_INTEGER);
		ufbxi_init_synthetic_int_prop(&tmp_props[num_props++], ufbxi_ReferenceStop, stop, UFBX_PROP_INTEGER);
	}

	const char *name;
	ufbxi_check(ufbxi_get_val1(node, "C", (char**)&name));

	// Hack: For post-7000 files we are only interested in the animation times
	// for fallback in case the information is missing in the stacks.
	if (uc->version >= 7000) {
		uint32_t hash = ufbxi_hash_ptr(name);
		ufbxi_tmp_anim_stack *entry = ufbxi_map_find(&uc->anim_stack_map, ufbxi_tmp_anim_stack, hash, &name);

		if (entry) {
			ufbx_anim_stack *stack = entry->stack;
			if (stack->props.props.count == 0) {
				stack->props.props.count = num_props;
				stack->props.props.data = ufbxi_push_copy(&uc->result, ufbx_prop, num_props, tmp_props);
				ufbxi_check(stack->props.props.data);
			}
		}

		return 1;
	}

	uint64_t stack_fbx_id = 0, layer_fbx_id = 0;

	// Treat the Take as a post-7000 version animation stack and layer.
	ufbx_anim_stack *stack = ufbxi_push_synthetic_element(uc, &stack_fbx_id, node, name, ufbx_anim_stack, UFBX_ELEMENT_ANIM_STACK);
	ufbxi_check(stack);

	stack->props.props.count = num_props;
	stack->props.props.data = ufbxi_push_copy(&uc->result, ufbx_prop, num_props, tmp_props);
	ufbxi_check(stack->props.props.data);

	ufbx_anim_layer *layer = ufbxi_push_synthetic_element(uc, &layer_fbx_id, node, ufbxi_BaseLayer, ufbx_anim_layer, UFBX_ELEMENT_ANIM_LAYER);
	ufbxi_check(layer);

	ufbxi_check(ufbxi_connect_oo(uc, layer_fbx_id, stack_fbx_id));

	// Read all properties of objects included in the take
	ufbxi_for(ufbxi_node, child, node->children, node->num_children) {
		// TODO: Do some object types have another name?
		if (child->name != ufbxi_Model) continue;

		ufbxi_check(ufbxi_read_take_object(uc, child, layer_fbx_id));
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_takes(ufbxi_context *uc)
{
	for (;;) {
		ufbxi_node *node;
		ufbxi_check(ufbxi_parse_toplevel_child(uc, &node, NULL));
		if (!node) break;

		if (node->name == ufbxi_Take) {
			ufbxi_check(ufbxi_read_take(uc, node));
		}
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_settings(ufbxi_context *uc, ufbxi_node *node)
{
	if (uc->read_legacy_settings) return 1;
	uc->read_legacy_settings = true;

	ufbx_prop tmp_props[2];
	uint32_t num_props = 0;
	memset(tmp_props, 0, sizeof(tmp_props));

	ufbxi_node *frame_rate = ufbxi_find_child_strcmp(node, "FrameRate");
	if (frame_rate) {
		double fps = 0.0;
		if (!ufbxi_get_val1(frame_rate, "D", &fps)) {
			ufbx_string str;
			if (ufbxi_get_val1(frame_rate, "S", &str)) {
				char *end;
				double val = ufbxi_parse_double(str.data, str.length, &end, uc->double_parse_flags);
				if (end == str.data + str.length) {
					fps = val;
				}
			}
		}
		if (fps > 0.0) {
			ufbxi_init_synthetic_real_prop(&tmp_props[num_props++], ufbxi_CustomFrameRate, (ufbx_real)fps, UFBX_PROP_NUMBER);
			ufbxi_init_synthetic_real_prop(&tmp_props[num_props++], ufbxi_TimeMode, UFBX_TIME_MODE_CUSTOM, UFBX_PROP_INTEGER);
		}
	}

	if (num_props > 0) {
		ufbx_props *props = &uc->scene.settings.props;
		size_t num_existing = props->props.count;

		size_t new_count = num_props + num_existing;
		ufbx_prop *new_props = ufbxi_push(&uc->result, ufbx_prop, new_count);
		ufbxi_check(new_props);

		memcpy(new_props, tmp_props, num_props * sizeof(ufbx_prop));
		if (num_existing > 0) {
			memcpy(new_props + num_props, props->props.data, num_existing * sizeof(ufbx_prop));
		}

		ufbxi_check(ufbxi_sort_properties(uc, new_props, new_count));
		props->props.data = new_props;
		props->props.count = new_count;
		ufbxi_deduplicate_properties(&props->props);

		ufbxi_check(uc->scene.settings.props.props.data);
	}

	return 1;
}

ufbxi_noinline static ufbx_matrix ufbxi_unscaled_transform_to_matrix(const ufbx_transform *t)
{
	ufbx_transform transform = *t;
	transform.scale.x = 1.0f;
	transform.scale.y = 1.0f;
	transform.scale.z = 1.0f;
	return ufbx_transform_to_matrix(&transform);
}

ufbxi_noinline static void ufbxi_setup_root_node(ufbxi_context *uc, ufbx_node *root)
{
	if (uc->opts.use_root_transform) {
		root->local_transform = uc->opts.root_transform;
		root->node_to_parent = ufbx_transform_to_matrix(&uc->opts.root_transform);
	} else {
		root->local_transform = ufbx_identity_transform;
		root->node_to_parent = ufbx_identity_matrix;
	}
	root->is_root = true;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_root(ufbxi_context *uc)
{
	// FBXHeaderExtension: Some metadata (optional)
	ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_FBXHeaderExtension));
	ufbxi_check(ufbxi_read_header_extension(uc));

	// The ASCII exporter version is stored in top-level
	if (uc->exporter == UFBX_EXPORTER_BLENDER_ASCII) {
		ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Creator));
		if (uc->top_node) {
			ufbxi_ignore(ufbxi_get_val1(uc->top_node, "S", &uc->scene.metadata.creator));
		}
	}

	// Resolve the exporter before continuing
	ufbxi_check(ufbxi_match_exporter(uc));
	if (uc->version < 7000) {
		ufbxi_check(ufbxi_init_node_prop_names(uc));
	}
	// Don't allow changing version from this point onwards
	uc->ascii.found_version = true;

	// Document: Read root ID
	if (uc->version >= 7000) {
		ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Documents));
		ufbxi_check(ufbxi_read_document(uc));
	} else {
		// Pre-7000: Root node has a specific type-name pair "Model::Scene"
		// (or reversed in binary). Use the interned name as ID as usual.
		const char *root_name = uc->from_ascii ? "Model::Scene" : "Scene\x00\x01Model";
		root_name = ufbxi_push_string_imp(&uc->string_pool, root_name, 12, NULL, false, true);
		ufbxi_check(root_name);
		uc->root_id = ufbxi_synthetic_id_from_string(root_name);
	}

	// Add a nameless root node with the root ID
	{
		ufbxi_element_info root_info = { uc->root_id };
		root_info.name = ufbx_empty_string;
		ufbx_node *root = ufbxi_push_element(uc, &root_info, ufbx_node, UFBX_ELEMENT_NODE);
		ufbxi_check(root);
		ufbxi_setup_root_node(uc, root);
		ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &root->element.element_id));
	}

	// Definitions: Object type counts and property templates (optional)
	ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Definitions));
	ufbxi_check(ufbxi_read_definitions(uc));

	// Objects: Actual scene data
	ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Objects));
	if (!uc->sure_fbx) {
		// If the file is a bit iffy about being a real FBX file reject it if
		// even the objects are not found.
		ufbxi_check_msg(uc->top_node, "Not an FBX file");
	}
	if (uc->thread_pool.enabled) {
		ufbxi_check(ufbxi_read_objects_threaded(uc));
	} else {
		ufbxi_check(ufbxi_read_objects(uc));
	}

	// Connections: Relationships between nodes
	ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Connections));
	ufbxi_check(ufbxi_read_connections(uc));

	// Takes: Pre-7000 animation data
	ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Takes));
	ufbxi_check(ufbxi_read_takes(uc));

	// Check if there's a top-level GlobalSettings that we skimmed over
	ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_GlobalSettings));
	if (uc->top_node) {
		ufbxi_check(ufbxi_read_global_settings(uc, uc->top_node));
	}

	// Version5: Pre-6000 settings
	ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Version5));
	if (uc->top_node) {
		ufbxi_node *settings = ufbxi_find_child_strcmp(uc->top_node, "Settings");
		if (settings) {
			ufbxi_check(ufbxi_read_legacy_settings(uc, settings));
		}
	}

	// Force parsing all the nodes by parsing a toplevel that cannot be found
	if (uc->opts.retain_dom) {
		ufbxi_check(ufbxi_parse_toplevel(uc, NULL));
	}

	return 1;
}

typedef struct {
	const char *prop_name;
	ufbx_prop_type prop_type;
	const char *node_name;
	const char *node_fmt;
} ufbxi_legacy_prop;

// Must be alphabetically sorted!
static const ufbxi_legacy_prop ufbxi_legacy_light_props[] = {
	{ ufbxi_CastLight, UFBX_PROP_BOOLEAN, ufbxi_CastLight, "L" },
	{ ufbxi_CastShadows, UFBX_PROP_BOOLEAN, ufbxi_CastShadows, "L" },
	{ ufbxi_Color, UFBX_PROP_COLOR, ufbxi_Color, "RRR" },
	{ ufbxi_ConeAngle, UFBX_PROP_NUMBER, ufbxi_ConeAngle, "R" },
	{ ufbxi_HotSpot, UFBX_PROP_NUMBER, ufbxi_HotSpot, "R" },
	{ ufbxi_Intensity, UFBX_PROP_NUMBER, ufbxi_Intensity, "R" },
	{ ufbxi_LightType, UFBX_PROP_INTEGER, ufbxi_LightType, "L" },
};

// Must be alphabetically sorted!
static const ufbxi_legacy_prop ufbxi_legacy_camera_props[] = {
	{ ufbxi_ApertureMode, UFBX_PROP_INTEGER, ufbxi_ApertureMode, "L" },
	{ ufbxi_AspectH, UFBX_PROP_NUMBER, ufbxi_AspectH, "R" },
	{ ufbxi_AspectRatioMode, UFBX_PROP_INTEGER, "AspectType", "L" },
	{ ufbxi_AspectW, UFBX_PROP_NUMBER, ufbxi_AspectW, "R" },
	{ ufbxi_FieldOfView, UFBX_PROP_NUMBER, "Aperture", "R" },
	{ ufbxi_FieldOfViewX, UFBX_PROP_NUMBER, "FieldOfViewXProperty", "R" },
	{ ufbxi_FieldOfViewY, UFBX_PROP_NUMBER, "FieldOfViewYProperty", "R" },
	{ ufbxi_FilmHeight, UFBX_PROP_NUMBER, "CameraAperture", "_R" },
	{ ufbxi_FilmSqueezeRatio, UFBX_PROP_NUMBER, "SqueezeRatio", "R" },
	{ ufbxi_FilmWidth, UFBX_PROP_NUMBER, "CameraAperture", "R_" },
	{ ufbxi_FocalLength, UFBX_PROP_NUMBER, ufbxi_FocalLength, "R" },
};

// Must be alphabetically sorted!
static const ufbxi_legacy_prop ufbxi_legacy_bone_props[] = {
	{ ufbxi_Size, UFBX_PROP_NUMBER, ufbxi_Size, "R" },
};

// Must be alphabetically sorted!
static const ufbxi_legacy_prop ufbxi_legacy_material_props[] = {
	{ ufbxi_AmbientColor, UFBX_PROP_COLOR, "Ambient", "RRR" },
	{ ufbxi_DiffuseColor, UFBX_PROP_COLOR, "Diffuse", "RRR" },
	{ ufbxi_EmissiveColor, UFBX_PROP_COLOR, "Emissive", "RRR" },
	{ ufbxi_ShadingModel, UFBX_PROP_COLOR, ufbxi_ShadingModel, "S" },
	{ ufbxi_Shininess, UFBX_PROP_NUMBER, "Shininess", "R" },
	{ ufbxi_SpecularColor, UFBX_PROP_COLOR, "Specular", "RRR" },
};

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_prop(ufbxi_node *node, ufbx_prop *prop, const ufbxi_legacy_prop *legacy_prop)
{
	size_t value_ix = 0;
	uint32_t flags = 0;

	const char *fmt = legacy_prop->node_fmt;
	for (size_t fmt_ix = 0; fmt[fmt_ix]; fmt_ix++) {
		char c = fmt[fmt_ix];
		switch (c) {
		case 'L':
			ufbx_assert(value_ix == 0);
			if (!ufbxi_get_val_at(node, fmt_ix, 'L', &prop->value_int)) return 0;
			prop->value_real = (ufbx_real)prop->value_int;
			prop->value_real_arr[1] = 0.0f;
			prop->value_real_arr[2] = 0.0f;
			prop->value_real_arr[3] = 0.0f;
			prop->value_str = ufbx_empty_string;
			prop->value_blob = ufbx_empty_blob;
			flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_INT;
			value_ix++;
			break;
		case 'R':
			ufbx_assert(value_ix < 4);
			if (!ufbxi_get_val_at(node, fmt_ix, 'R', &prop->value_real_arr[value_ix])) return 0;
			if (value_ix == 0) {
				prop->value_int = ufbxi_f64_to_i64(prop->value_real);
				prop->value_real_arr[1] = 0.0f;
				prop->value_real_arr[2] = 0.0f;
				prop->value_real_arr[3] = 0.0f;
				prop->value_str = ufbx_empty_string;
				prop->value_blob = ufbx_empty_blob;
			}
			flags &= ~(uint32_t)(UFBX_PROP_FLAG_VALUE_REAL|UFBX_PROP_FLAG_VALUE_VEC2|UFBX_PROP_FLAG_VALUE_VEC3|UFBX_PROP_FLAG_VALUE_VEC4);
			flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_REAL << value_ix;
			value_ix++;
			break;
		case 'S':
			ufbx_assert(value_ix == 0);
			if (!ufbxi_get_val_at(node, fmt_ix, 'S', &prop->value_str)) return 0;
			if (prop->value_str.length > 0) {
				int found = ufbxi_get_val_at(node, fmt_ix, 'b', &prop->value_blob);
				ufbxi_ignore(found);
				ufbx_assert(found);
			} else {
				prop->value_blob = ufbx_empty_blob;
			}
			prop->value_real = 0.0f;
			prop->value_real_arr[1] = 0.0f;
			prop->value_real_arr[2] = 0.0f;
			prop->value_real_arr[3] = 0.0f;
			prop->value_int = 0;
			flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_STR;
			value_ix++;
			break;
		case '_':
			break;
		default:
			ufbxi_unreachable("Unhandled legacy fmt");
		}
	}

	prop->flags = (ufbx_prop_flags)flags;

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static size_t ufbxi_read_legacy_props(ufbxi_node *node, ufbx_prop *props, const ufbxi_legacy_prop *legacy_props, size_t num_legacy)
{
	size_t num_props = 0;
	for (size_t legacy_ix = 0; legacy_ix < num_legacy; legacy_ix++) {
		const ufbxi_legacy_prop *legacy_prop = &legacy_props[legacy_ix];
		ufbx_prop *prop = &props[num_props];

		ufbxi_node *n = ufbxi_find_child_strcmp(node, legacy_prop->node_name);
		if (!n) continue;
		if (!ufbxi_read_legacy_prop(n, prop, legacy_prop)) continue;

		prop->name.data = legacy_prop->prop_name;
		prop->name.length = strlen(legacy_prop->prop_name);
		prop->_internal_key = ufbxi_get_name_key(prop->name.data, prop->name.length);
		prop->flags = (ufbx_prop_flags)0;
		prop->type = legacy_prop->prop_type;
		num_props++;
	}

	return num_props;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_material(ufbxi_context *uc, ufbxi_node *node, uint64_t *p_fbx_id, const char *name)
{
	ufbx_material *ufbxi_restrict material = ufbxi_push_synthetic_element(uc, p_fbx_id, node, name, ufbx_material, UFBX_ELEMENT_MATERIAL);
	ufbxi_check(material);

	ufbx_prop tmp_props[ufbxi_arraycount(ufbxi_legacy_material_props)];
	size_t num_props = ufbxi_read_legacy_props(node, tmp_props, ufbxi_legacy_material_props, ufbxi_arraycount(ufbxi_legacy_material_props));

	material->shading_model_name = ufbx_empty_string;
	material->props.props.count = num_props;
	material->props.props.data = ufbxi_push_copy(&uc->result, ufbx_prop, num_props, tmp_props);
	ufbxi_check(material->props.props.data);

	material->shader_prop_prefix = ufbx_empty_string;

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_link(ufbxi_context *uc, ufbxi_node *node, uint64_t *p_fbx_id, const char *name)
{
	ufbx_skin_cluster *ufbxi_restrict cluster = ufbxi_push_synthetic_element(uc, p_fbx_id, node, name, ufbx_skin_cluster, UFBX_ELEMENT_SKIN_CLUSTER);
	ufbxi_check(cluster);

	// TODO: Merge with ufbxi_read_skin_cluster(), at least partially?
	ufbxi_value_array *indices = ufbxi_find_array(node, ufbxi_Indexes, 'i');
	ufbxi_value_array *weights = ufbxi_find_array(node, ufbxi_Weights, 'r');

	if (indices && weights) {
		ufbxi_check(indices->size == weights->size);
		cluster->num_weights = indices->size;
		cluster->vertices.data = (uint32_t*)indices->data;
		cluster->weights.data = (ufbx_real*)weights->data;
		cluster->vertices.count = cluster->num_weights;
		cluster->weights.count = cluster->num_weights;
	}

	ufbxi_value_array *transform = ufbxi_find_array(node, ufbxi_Transform, 'r');
	ufbxi_value_array *transform_link = ufbxi_find_array(node, ufbxi_TransformLink, 'r');
	if (transform && transform_link) {
		ufbxi_check(transform->size >= 16);
		ufbxi_check(transform_link->size >= 16);

		ufbxi_read_transform_matrix(&cluster->mesh_node_to_bone, (ufbx_real*)transform->data);
		ufbxi_read_transform_matrix(&cluster->bind_to_world, (ufbx_real*)transform_link->data);
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_light(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_light *ufbxi_restrict light = ufbxi_push_element(uc, info, ufbx_light, UFBX_ELEMENT_LIGHT);
	ufbxi_check(light);

	ufbx_prop tmp_props[ufbxi_arraycount(ufbxi_legacy_light_props)];
	size_t num_props = ufbxi_read_legacy_props(node, tmp_props, ufbxi_legacy_light_props, ufbxi_arraycount(ufbxi_legacy_light_props));

	light->props.props.count = num_props;
	light->props.props.data = ufbxi_push_copy(&uc->result, ufbx_prop, num_props, tmp_props);
	ufbxi_check(light->props.props.data);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_camera(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_camera *ufbxi_restrict camera = ufbxi_push_element(uc, info, ufbx_camera, UFBX_ELEMENT_CAMERA);
	ufbxi_check(camera);

	ufbx_prop tmp_props[ufbxi_arraycount(ufbxi_legacy_camera_props)];
	size_t num_props = ufbxi_read_legacy_props(node, tmp_props, ufbxi_legacy_camera_props, ufbxi_arraycount(ufbxi_legacy_camera_props));

	camera->props.props.count = num_props;
	camera->props.props.data = ufbxi_push_copy(&uc->result, ufbx_prop, num_props, tmp_props);
	ufbxi_check(camera->props.props.data);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_limb_node(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	ufbx_bone *ufbxi_restrict bone = ufbxi_push_element(uc, info, ufbx_bone, UFBX_ELEMENT_BONE);
	ufbxi_check(bone);

	ufbx_prop tmp_props[ufbxi_arraycount(ufbxi_legacy_bone_props)];
	size_t num_props = 0;

	ufbxi_node *prop_node = ufbxi_find_child_strcmp(node, "Properties");
	if (prop_node) {
		num_props = ufbxi_read_legacy_props(prop_node, tmp_props, ufbxi_legacy_bone_props, ufbxi_arraycount(ufbxi_legacy_bone_props));
	}

	bone->props.props.count = num_props;
	bone->props.props.data = ufbxi_push_copy(&uc->result, ufbx_prop, num_props, tmp_props);
	ufbxi_check(bone->props.props.data);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_mesh(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{
	// Only read polygon meshes, ignore eg. NURBS without error
	ufbxi_node *node_vertices = ufbxi_find_child(node, ufbxi_Vertices);
	ufbxi_node *node_indices = ufbxi_find_child(node, ufbxi_PolygonVertexIndex);
	if (!node_vertices || !node_indices) return 1;

	ufbx_mesh *ufbxi_restrict mesh = ufbxi_push_element(uc, info, ufbx_mesh, UFBX_ELEMENT_MESH);
	ufbxi_check(mesh);

	ufbxi_check(ufbxi_read_synthetic_blend_shapes(uc, node, info));

	ufbxi_patch_mesh_reals(mesh);

	if (uc->opts.ignore_geometry) return 1;

	ufbxi_value_array *vertices = ufbxi_get_array(node_vertices, 'r');
	ufbxi_value_array *indices = ufbxi_get_array(node_indices, 'i');
	ufbxi_check(vertices && indices);
	ufbxi_check(vertices->size % 3 == 0);

	mesh->num_vertices = vertices->size / 3;
	mesh->num_indices = indices->size;

	uint32_t *index_data = (uint32_t*)indices->data;

	// Duplicate `index_data` for modification if we retain DOM
	if (uc->opts.retain_dom) {
		index_data = ufbxi_push_copy(&uc->result, uint32_t, indices->size, index_data);
		ufbxi_check(index_data);
	}

	mesh->vertices.data = (ufbx_vec3*)vertices->data;
	mesh->vertex_indices.data = index_data;
	mesh->vertices.count = mesh->num_vertices;
	mesh->vertex_indices.count = mesh->num_indices;

	mesh->vertex_position.exists = true;
	mesh->vertex_position.values.data = (ufbx_vec3*)vertices->data;
	mesh->vertex_position.values.count = mesh->num_vertices;
	mesh->vertex_position.indices.data = index_data;
	mesh->vertex_position.indices.count = mesh->num_indices;
	mesh->vertex_position.unique_per_vertex = true;

	// Check/make sure that the last index is negated (last of polygon)
	if (mesh->num_indices > 0) {
		if ((int32_t)index_data[mesh->num_indices - 1] >= 0) {
			if (uc->opts.strict) ufbxi_fail("Non-negated last index");
			index_data[mesh->num_indices - 1] = ~index_data[mesh->num_indices - 1];
		}
	}

	ufbxi_check(ufbxi_process_indices(uc, mesh, index_data));

	// Normals are either per-vertex or per-index in legacy FBX files?
	// If the version is 5000 prefer per-vertex, otherwise per-index...
	ufbxi_value_array *normals = ufbxi_find_array(node, ufbxi_Normals, 'r');
	if (normals) {
		size_t num_normals = normals->size / 3;
		bool per_vertex = num_normals == mesh->num_vertices;
		bool per_index = num_normals == mesh->num_indices;
		if (per_vertex && (!per_index || uc->version == 5000)) {
			mesh->vertex_normal.exists = true;
			mesh->vertex_normal.values.count = num_normals;
			mesh->vertex_normal.indices.count = mesh->num_indices;
			mesh->vertex_normal.unique_per_vertex = true;
			mesh->vertex_normal.values.data = (ufbx_vec3*)normals->data;
			mesh->vertex_normal.indices.data = mesh->vertex_indices.data;
		} else if (per_index) {
			uc->max_consecutive_indices = ufbxi_max_sz(uc->max_consecutive_indices, mesh->num_indices);
			mesh->vertex_normal.exists = true;
			mesh->vertex_normal.values.count = num_normals;
			mesh->vertex_normal.indices.count = mesh->num_indices;
			mesh->vertex_normal.unique_per_vertex = false;
			mesh->vertex_normal.values.data = (ufbx_vec3*)normals->data;
			mesh->vertex_normal.indices.data = (uint32_t*)ufbxi_sentinel_index_consecutive;
		}
	}

	// Optional UV values are stored pretty much like a modern vertex element
	ufbxi_node *uv_info = ufbxi_find_child(node, ufbxi_GeometryUVInfo);
	if (uv_info) {
		ufbx_uv_set *set = ufbxi_push_zero(&uc->result, ufbx_uv_set, 1);
		ufbxi_check(set);
		set->index = 0;
		set->name.data = ufbxi_empty_char;
		ufbxi_check(ufbxi_read_vertex_element(uc, mesh, uv_info, (ufbx_vertex_attrib*)&set->vertex_uv,
			ufbxi_TextureUV, ufbxi_TextureUVVerticeIndex, NULL, 'r', 2));

		mesh->uv_sets.data = set;
		mesh->uv_sets.count = 1;
		mesh->vertex_uv = set->vertex_uv;
	}

	// Material indices
	{
		const char *mapping = NULL;
		ufbxi_check(ufbxi_find_val1(node, ufbxi_MaterialAssignation, "C", (char**)&mapping));
		if (mapping == ufbxi_ByPolygon) {
			ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->face_material.data, &mesh->face_material.count, node, ufbxi_Materials, 'i', mesh->num_faces));
		} else if (mapping == ufbxi_AllSame) {
			ufbxi_value_array *arr = ufbxi_find_array(node, ufbxi_Materials, 'i');
			uint32_t material = 0;
			if (arr && arr->size >= 1) {
				material = ((uint32_t*)arr->data)[0];
			}

			mesh->face_material.count = mesh->num_faces;
			if (material == 0) {
				mesh->face_material.data = (uint32_t*)ufbxi_sentinel_index_zero;
			} else {
				mesh->face_material.data = ufbxi_push(&uc->result, uint32_t, mesh->num_faces);
				ufbxi_check(mesh->face_material.data);
				ufbxi_for_list(uint32_t, p_mat, mesh->face_material) {
					*p_mat = material;
				}
			}
		}
	}

	uint64_t skin_fbx_id = 0;
	ufbx_skin_deformer *skin = NULL;

	// Materials, Skin Clusters
	ufbxi_for(ufbxi_node, child, node->children, node->num_children) {
		if (child->name == ufbxi_Material) {
			uint64_t fbx_id = 0;
			ufbx_string type_and_name, type, name;
			ufbxi_check(ufbxi_get_val1(child, "s", &type_and_name));
			ufbxi_check(ufbxi_split_type_and_name(uc, type_and_name, &type, &name));
			ufbxi_check(ufbxi_read_legacy_material(uc, child, &fbx_id, name.data));
			ufbxi_check(ufbxi_connect_oo(uc, fbx_id, info->fbx_id));
		} else if (child->name == ufbxi_Link) {
			uint64_t fbx_id = 0;
			ufbx_string type_and_name, type, name;
			ufbxi_check(ufbxi_get_val1(child, "s", &type_and_name));
			ufbxi_check(ufbxi_split_type_and_name(uc, type_and_name, &type, &name));
			ufbxi_check(ufbxi_read_legacy_link(uc, child, &fbx_id, name.data));

			uint64_t node_fbx_id = ufbxi_synthetic_id_from_string(type_and_name.data);
			ufbxi_check(ufbxi_connect_oo(uc, node_fbx_id, fbx_id));
			if (!skin) {
				skin = ufbxi_push_synthetic_element(uc, &skin_fbx_id, NULL, info->name.data, ufbx_skin_deformer, UFBX_ELEMENT_SKIN_DEFORMER);
				ufbxi_check(skin);
				ufbxi_check(ufbxi_connect_oo(uc, skin_fbx_id, info->fbx_id));
			}
			ufbxi_check(ufbxi_connect_oo(uc, fbx_id, skin_fbx_id));
		}
	}

	mesh->skinned_is_local = true;
	mesh->skinned_position = mesh->vertex_position;
	mesh->skinned_normal = mesh->vertex_normal;

	ufbxi_patch_mesh_reals(mesh);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_media(ufbxi_context *uc, ufbxi_node *node)
{
	ufbxi_node *videos = ufbxi_find_child(node, ufbxi_Video);
	if (videos) {
		ufbxi_for(ufbxi_node, child, videos->children, videos->num_children) {
			ufbxi_element_info video_info = { 0 };
			ufbxi_check(ufbxi_get_val1(child, "S", &video_info.name));
			ufbxi_check(ufbxi_push_synthetic_id(uc, &video_info.fbx_id));
			video_info.dom_node = ufbxi_get_dom_node(uc, node);

			ufbxi_check(ufbxi_read_video(uc, child, &video_info));
		}
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_model(ufbxi_context *uc, ufbxi_node *node)
{
	ufbx_string type_and_name, type, name;
	ufbxi_check(ufbxi_get_val1(node, "s", &type_and_name));
	ufbxi_check(ufbxi_split_type_and_name(uc, type_and_name, &type, &name));

	ufbxi_element_info info = { 0 };
	info.fbx_id = ufbxi_synthetic_id_from_string(type_and_name.data);
	info.name = name;
	info.dom_node = ufbxi_get_dom_node(uc, node);

	ufbx_node *elem_node = ufbxi_push_element(uc, &info, ufbx_node, UFBX_ELEMENT_NODE);
	ufbxi_check(elem_node);
	ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &elem_node->element.element_id));

	ufbxi_element_info attrib_info = { 0 };
	ufbxi_check(ufbxi_push_synthetic_id(uc, &attrib_info.fbx_id));
	attrib_info.name = name;
	attrib_info.dom_node = info.dom_node;

	// If we make unused connections it doesn't matter..
	ufbxi_check(ufbxi_connect_oo(uc, attrib_info.fbx_id, info.fbx_id));

	const char *attrib_type = ufbxi_empty_char;
	ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Type, "C", (char**)&attrib_type));

	bool has_attrib = true;
	if (attrib_type == ufbxi_Light) {
		ufbxi_check(ufbxi_read_legacy_light(uc, node, &attrib_info));
	} else if (attrib_type == ufbxi_Camera) {
		ufbxi_check(ufbxi_read_legacy_camera(uc, node, &attrib_info));
	} else if (attrib_type == ufbxi_LimbNode) {
		ufbxi_check(ufbxi_read_legacy_limb_node(uc, node, &attrib_info));
	} else if (ufbxi_find_child(node, ufbxi_Vertices)) {
		ufbxi_check(ufbxi_read_legacy_mesh(uc, node, &attrib_info));
	} else {
		has_attrib = false;
	}

	// Mark the node as having an attribute so property connections can be forwarded
	if (has_attrib) {
		ufbxi_check(ufbxi_insert_fbx_attr(uc, info.fbx_id, attrib_info.fbx_id));
	}

	// Children are represented as an array of strings
	ufbxi_value_array *children = ufbxi_find_array(node, ufbxi_Children, 's');
	if (children) {
		ufbx_string *names = (ufbx_string*)children->data;
		for (size_t i = 0; i < children->size; i++) {
			uint64_t child_fbx_id = ufbxi_synthetic_id_from_string(names[i].data);
			ufbxi_check(ufbxi_connect_oo(uc, child_fbx_id, info.fbx_id));
		}
	}

	// Non-take animation channels
	ufbxi_for(ufbxi_node, child, node->children, node->num_children) {
		if (child->name == ufbxi_Channel) {
			ufbx_string channel_name;
			if (ufbxi_get_val1(child, "S", &channel_name)) {
				if (uc->legacy_implicit_anim_layer_id == 0) {
					// Defer creation so we won't be the first animation stack..
					ufbxi_check(ufbxi_push_synthetic_id(uc, &uc->legacy_implicit_anim_layer_id));
				}
				ufbxi_check(ufbxi_read_take_prop_channel(uc, child, info.fbx_id, uc->legacy_implicit_anim_layer_id, channel_name));
			}
		}
	}

	return 1;
}

// Read a pre-6000 FBX file where everything is stored at the root level
ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_legacy_root(ufbxi_context *uc)
{
	ufbxi_check(ufbxi_init_node_prop_names(uc));

	// Some legacy FBX files have an `Fbx_Root` node that could be used as the
	// root node. However no other formats have root node with transforms so it
	// might be better to leave it as-is and create an empty one.
	{
		ufbx_node *root = ufbxi_push_synthetic_element(uc, &uc->root_id, NULL, ufbxi_empty_char, ufbx_node, UFBX_ELEMENT_NODE);
		ufbxi_check(root);
		ufbxi_setup_root_node(uc, root);
		ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &root->element.element_id));
	}

	// NOTE: `ufbxi_read_header_extension()` is optional so use default KTime definition
	uc->ktime_sec = 46186158000;
	uc->ktime_sec_double = (double)uc->ktime_sec;

	for (;;) {
		ufbxi_check(ufbxi_parse_legacy_toplevel(uc));
		if (!uc->top_node) break;

		ufbxi_node *node = uc->top_node;
		if (node->name == ufbxi_FBXHeaderExtension) {
			ufbxi_check(ufbxi_read_header_extension(uc));
		} else if (node->name == ufbxi_Media) {
			ufbxi_check(ufbxi_read_legacy_media(uc, node));
		} else if (node->name == ufbxi_Takes) {
			ufbxi_check(ufbxi_read_takes(uc));
		} else if (node->name == ufbxi_Model) {
			ufbxi_check(ufbxi_read_legacy_model(uc, node));
		} else if (!strcmp(node->name, "Settings")) {
			ufbxi_check(ufbxi_read_legacy_settings(uc, node));
		}
	}

	if (uc->opts.retain_dom) {
		ufbxi_check(ufbxi_retain_toplevel(uc, NULL));
	}

	// Create the implicit animation stack if necessary
	if (uc->legacy_implicit_anim_layer_id) {
		ufbxi_element_info layer_info = { 0 };
		layer_info.fbx_id = uc->legacy_implicit_anim_layer_id;
		layer_info.name.data = "(internal)";
		layer_info.name.length = strlen(layer_info.name.data);
		ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &layer_info.name, true));
		ufbx_anim_layer *layer = ufbxi_push_element(uc, &layer_info, ufbx_anim_layer, UFBX_ELEMENT_ANIM_LAYER);
		ufbxi_check(layer);

		ufbxi_element_info stack_info = layer_info;
		ufbxi_check(ufbxi_push_synthetic_id(uc, &stack_info.fbx_id));
		ufbx_anim_stack *stack = ufbxi_push_element(uc, &stack_info, ufbx_anim_stack, UFBX_ELEMENT_ANIM_STACK);
		ufbxi_check(stack);

		ufbxi_check(ufbxi_connect_oo(uc, layer_info.fbx_id, stack_info.fbx_id));
	}

	return 1;
}

// Filename manipulation

ufbxi_nodiscard ufbxi_noinline static size_t ufbxi_trim_delimiters(ufbxi_context *uc, const char *data, size_t length)
{
	for (; length > 0; length--) {
		char c = data[length - 1];
		bool is_separator = c == '/' || c == uc->opts.path_separator;
		if (is_separator) {
			length--;
			break;
		}
	}
	return length;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_init_file_paths(ufbxi_context *uc)
{
	if (uc->opts.filename.length > 0) {
		uc->scene.metadata.filename = uc->opts.filename;
	} else if (uc->opts.raw_filename.size > 0) {
		uc->scene.metadata.filename.data = (const char*)uc->opts.raw_filename.data;
		uc->scene.metadata.filename.length = uc->opts.raw_filename.size;
	}

	if (uc->opts.raw_filename.size > 0) {
		uc->scene.metadata.raw_filename = uc->opts.raw_filename;
	} else if (uc->opts.filename.length > 0) {
		uc->scene.metadata.raw_filename.data = uc->opts.filename.data;
		uc->scene.metadata.raw_filename.size = uc->opts.filename.length;
	}

	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &uc->scene.metadata.filename, false));
	ufbxi_check(ufbxi_push_string_place_blob(&uc->string_pool, &uc->scene.metadata.raw_filename, true));

	uc->scene.metadata.relative_root.data = uc->scene.metadata.filename.data;
	uc->scene.metadata.relative_root.length = ufbxi_trim_delimiters(uc, uc->scene.metadata.filename.data, uc->scene.metadata.filename.length);

	uc->scene.metadata.raw_relative_root.data = uc->scene.metadata.raw_filename.data;
	uc->scene.metadata.raw_relative_root.size = ufbxi_trim_delimiters(uc, (const char*)uc->scene.metadata.raw_filename.data, uc->scene.metadata.raw_filename.size);

	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &uc->scene.metadata.relative_root, false));
	ufbxi_check(ufbxi_push_string_place_blob(&uc->string_pool, &uc->scene.metadata.raw_relative_root, true));

	return 1;
}

typedef union {
	ufbx_string str;
	ufbx_blob blob;
} ufbxi_strblob;

static ufbxi_noinline void ufbxi_strblob_set(ufbxi_strblob *dst, const char *data, size_t length, bool raw)
{
	if (raw) {
		dst->blob.data = data;
		dst->blob.size = length;
	} else {
		dst->str.data = length == 0 ? ufbxi_empty_char : data;
		dst->str.length = length;
	}
}

static ufbxi_forceinline const char *ufbxi_strblob_data(const ufbxi_strblob *strblob, bool raw)
{
	return raw ? (const char*)strblob->blob.data : strblob->str.data;
}

static ufbxi_forceinline size_t ufbxi_strblob_length(const ufbxi_strblob *strblob, bool raw)
{
	return raw ? strblob->blob.size : strblob->str.length;
}

ufbxi_nodiscard ufbxi_noinline static bool ufbxi_is_absolute_path(const char *path, size_t length)
{
	if (length > 0 && (path[0] == '/' || path[0] == '\\')) {
		return true;
	} else if (length > 2 && path[1] == ':' && (path[2] == '\\' || path[2] == '/')) {
		return true;
	}
	return false;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_relative_filename(ufbxi_context *uc, ufbxi_strblob *p_dst, const ufbxi_strblob *p_src, bool raw)
{
	const char *src = ufbxi_strblob_data(p_src, raw);
	size_t src_length = ufbxi_strblob_length(p_src, raw);

	// Skip leading directory separators and early return if the relative path is empty
	while (src_length > 0 && (src[0] == '/' || src[0] == '\\')) {
		src++;
		src_length--;
	}
	if (src_length == 0) {
		ufbxi_strblob_set(p_dst, NULL, 0, raw);
		return 1;
	}

	const char *prefix_data;
	size_t prefix_length;
	if (raw) {
		prefix_data = (const char*)uc->scene.metadata.raw_relative_root.data;
		prefix_length = uc->scene.metadata.raw_relative_root.size;
	} else {
		prefix_data = (const char*)uc->scene.metadata.relative_root.data;
		prefix_length = uc->scene.metadata.relative_root.length;
	}

	// Retain absolute paths
	if (ufbxi_is_absolute_path(src, src_length)) {
		prefix_length = 0;
	}

	// Undo directories from `prefix` for every `..`
	while (prefix_length > 0 && src_length >= 3 && src[0] == '.' && src[1] == '.' && (src[2] == '/' || src[2] == '\\')) {
		size_t part_start = prefix_length;
		while (part_start > 0 && !(prefix_data[part_start - 1] == '/' || prefix_data[part_start - 1] == '\\')) {
			part_start--;
		}
		size_t part_len = prefix_length - part_start;

		if (part_len == 2 && prefix_data[part_start] == '.' && prefix_data[part_start + 1] == '.') {
			// Prefix itself ends in `..`, cannot cancel out a leading `../`
			break;
		}

		// Eat the leading '/' before the part segment
		prefix_length = part_start > 0 ? part_start - 1 : 0;

		if (part_len == 1 && prefix_data[part_start] == '.') {
			// Single '.' -> remove and continue without cancelling out a leading `../`
			continue;
		}

		src += 3;
		src_length -= 3;
	}

	size_t result_cap = prefix_length + src_length + 1;
	char *result = ufbxi_push(&uc->tmp_stack, char, result_cap);
	ufbxi_check(result);
	char *ptr = result;

	// Copy prefix and suffix converting separators in the process
	if (prefix_length > 0) {
		memcpy(ptr, prefix_data, prefix_length);
		ptr[prefix_length] = uc->opts.path_separator;
		ptr += prefix_length + 1;
	}
	for (size_t i = 0; i < src_length; i++) {
		char c = src[i];
		if (c == '/' || c == '\\') {
			c = uc->opts.path_separator;
		}
		*ptr++ = c;
	}

	// Intern the string and pop the temporary buffer
	ufbx_string dst = { result, ufbxi_to_size(ptr - result) };
	ufbx_assert(dst.length <= result_cap);
	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &dst, raw));
	ufbxi_pop(&uc->tmp_stack, char, result_cap, NULL);

	ufbxi_strblob_set(p_dst, dst.data, dst.length, raw);

	return 1;
}

// Open file utility

static ufbxi_noinline bool ufbxi_open_file(const ufbx_open_file_cb *cb, ufbx_stream *stream, const char *path, size_t path_len, const ufbx_blob *original_filename, ufbxi_allocator *ator, ufbx_open_file_type type)
{
	if (!cb || !cb->fn) return false;

	ufbx_open_file_info info; // ufbxi_uninit
	info.context = (uintptr_t)ator;
	if (original_filename) {
		info.original_filename = *original_filename;
	} else {
		info.original_filename.data = path;
		info.original_filename.size = path_len;
	}
	info.type = type;

	return cb->fn(cb->user, stream, path, path_len, &info);
}

#define ufbxi_patch_zero(dst, src) do { \
		ufbx_assert((dst) == 0 || (dst) == (src)); \
		(dst) = (src); \
	} while (0)

static void ufbxi_update_vertex_first_index(ufbx_mesh *mesh)
{
	ufbxi_for_list(uint32_t, p_vx_ix, mesh->vertex_first_index) {
		*p_vx_ix = UFBX_NO_INDEX;
	}

	uint32_t num_vertices = (uint32_t)mesh->num_vertices;
	for (size_t ix = 0; ix < mesh->num_indices; ix++) {
		uint32_t vx = mesh->vertex_indices.data[ix];
		if (vx < num_vertices && mesh->vertex_first_index.data[vx] == UFBX_NO_INDEX) {
			mesh->vertex_first_index.data[vx] = (uint32_t)ix;
		}
	}
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_mesh(ufbxi_buf *buf, ufbx_error *error, ufbx_mesh *mesh)
{
	if (mesh->vertices.count == 0) {
		mesh->vertices = mesh->vertex_position.values;
	}
	if (mesh->vertex_indices.count == 0) {
		mesh->vertex_indices = mesh->vertex_position.indices;
	}

	ufbxi_patch_zero(mesh->num_vertices, mesh->vertices.count);
	ufbxi_patch_zero(mesh->num_indices, mesh->vertex_indices.count);
	ufbxi_patch_zero(mesh->num_faces, mesh->faces.count);

	if (mesh->num_triangles == 0 || mesh->max_face_triangles == 0) {
		size_t num_triangles = 0;
		size_t max_face_triangles = 0;
		size_t num_bad_faces[3] = { 0 };
		ufbxi_nounroll ufbxi_for_list(ufbx_face, face, mesh->faces) {
			if (face->num_indices >= 3) {
				size_t tris = face->num_indices - 2;
				num_triangles += tris;
				max_face_triangles = ufbxi_max_sz(max_face_triangles, tris);
			} else {
				num_bad_faces[face->num_indices]++;
			}
		}

		ufbxi_patch_zero(mesh->num_triangles, num_triangles);
		ufbxi_patch_zero(mesh->max_face_triangles, max_face_triangles);
		ufbxi_patch_zero(mesh->num_empty_faces, num_bad_faces[0]);
		ufbxi_patch_zero(mesh->num_point_faces, num_bad_faces[1]);
		ufbxi_patch_zero(mesh->num_line_faces, num_bad_faces[2]);
	}

	if (!mesh->skinned_position.exists) {
		mesh->skinned_is_local = true;
		mesh->skinned_position = mesh->vertex_position;
		mesh->skinned_normal = mesh->vertex_normal;
	}

	if (mesh->vertex_first_index.count == 0) {
		mesh->vertex_first_index.count = mesh->num_vertices;
		mesh->vertex_first_index.data = ufbxi_push(buf, uint32_t, mesh->num_vertices);
		ufbxi_check_err(error, mesh->vertex_first_index.data);
		ufbxi_update_vertex_first_index(mesh);
	}

	if (mesh->uv_sets.count == 0 && mesh->vertex_uv.exists) {
		ufbx_uv_set *uv_set = ufbxi_push_zero(buf, ufbx_uv_set, 1);
		ufbxi_check_err(error, uv_set);

		uv_set->name.data = ufbxi_empty_char;
		uv_set->vertex_uv = mesh->vertex_uv;
		uv_set->vertex_tangent = mesh->vertex_tangent;
		uv_set->vertex_bitangent = mesh->vertex_bitangent;

		mesh->uv_sets.data = uv_set;
		mesh->uv_sets.count = 1;
	}

	if (mesh->color_sets.count == 0 && mesh->vertex_color.exists) {
		ufbx_color_set *color_set = ufbxi_push_zero(buf, ufbx_color_set, 1);
		ufbxi_check_err(error, color_set);

		color_set->name.data = ufbxi_empty_char;
		color_set->vertex_color = mesh->vertex_color;

		mesh->color_sets.data = color_set;
		mesh->color_sets.count = 1;
	}

	ufbxi_patch_mesh_reals(mesh);

	return 1;
}

// -- .obj file

#if UFBXI_FEATURE_FORMAT_OBJ

static const uint8_t ufbxi_obj_attrib_stride[] = {
	3, 2, 3, 4,
};

ufbx_static_assert(obj_attrib_strides, ufbxi_arraycount(ufbxi_obj_attrib_stride) == UFBXI_OBJ_NUM_ATTRIBS_EXT);

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_pop_props(ufbxi_context *uc, ufbx_prop_list *dst, size_t count)
{
	ufbx_prop_list props; // ufbxi_uninit
	props.count = count;
	props.data = ufbxi_push_pop(&uc->result, &uc->obj.tmp_props, ufbx_prop, count);
	ufbxi_check(props.data);

	ufbxi_for_list(ufbx_prop, prop, props) {
		prop->_internal_key = ufbxi_get_name_key(prop->name.data, prop->name.length);
		if (prop->value_str.length == 0) {
			prop->value_str.data = ufbxi_empty_char;
		}
		if (!prop->value_int) {
			prop->value_int = ufbxi_f64_to_i64(prop->value_real);
		}
		if (prop->value_blob.size == 0 && prop->value_str.length > 0) {
			prop->value_blob.data = prop->value_str.data;
			prop->value_blob.size = prop->value_str.length;
		}
	}

	if (props.count > 1) {
		ufbxi_check(ufbxi_sort_properties(uc, props.data, props.count));
		ufbxi_deduplicate_properties(&props);
	}

	*dst = props;
	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_push_mesh(ufbxi_context *uc)
{
	ufbxi_obj_mesh *mesh = ufbxi_push_zero(&uc->obj.tmp_meshes, ufbxi_obj_mesh, 1);
	ufbxi_check(mesh);
	uc->obj.mesh = mesh;

	ufbxi_nounroll for (size_t i = 0; i < UFBXI_OBJ_NUM_ATTRIBS; i++) {
		mesh->vertex_range[i].min_ix = UINT64_MAX;
	}

	const char *name = "";
	if (uc->opts.obj_split_groups && uc->obj.group.length > 0) {
		name = uc->obj.group.data;
	} else if (!uc->opts.obj_merge_objects && uc->obj.object.length > 0) {
		name = uc->obj.object.data;
	} else if (!uc->opts.obj_merge_groups && uc->obj.group.length > 0) {
		name = uc->obj.group.data;
	}

	mesh->fbx_node = ufbxi_push_synthetic_element(uc, &mesh->fbx_node_id, NULL, name, ufbx_node, UFBX_ELEMENT_NODE);
	mesh->fbx_mesh = ufbxi_push_synthetic_element(uc, &mesh->fbx_mesh_id, NULL, name, ufbx_mesh, UFBX_ELEMENT_MESH);
	ufbxi_check(mesh->fbx_node && mesh->fbx_mesh);

	mesh->fbx_mesh->vertex_position.unique_per_vertex = true;

	ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &mesh->fbx_node->element_id));

	uc->obj.face_material = UFBX_NO_INDEX;
	uc->obj.face_group = 0;
	uc->obj.face_group_dirty = true;
	uc->obj.material_dirty = true;

	ufbxi_check(ufbxi_connect_oo(uc, mesh->fbx_mesh_id, mesh->fbx_node_id));
	ufbxi_check(ufbxi_connect_oo(uc, mesh->fbx_node_id, 0));

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_flush_mesh(ufbxi_context *uc)
{
	if (!uc->obj.mesh) return 1;

	size_t num_props = uc->obj.tmp_props.num_items;
	ufbxi_check(ufbxi_obj_pop_props(uc, &uc->obj.mesh->fbx_mesh->props.props, num_props));

	size_t num_groups = uc->obj.tmp_face_group_infos.num_items;
	ufbx_face_group *groups = ufbxi_push_pop(&uc->result, &uc->obj.tmp_face_group_infos, ufbx_face_group, num_groups);
	ufbxi_check(groups);

	uc->obj.mesh->fbx_mesh->face_groups.data = groups;
	uc->obj.mesh->fbx_mesh->face_groups.count = num_groups;

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_init(ufbxi_context *uc)
{
	uc->from_ascii = true;
	uc->obj.initialized = true;


	ufbxi_nounroll for (size_t i = 0; i < UFBXI_OBJ_NUM_ATTRIBS_EXT; i++) {
		uc->obj.tmp_vertices[i].ator = &uc->ator_tmp;
		uc->obj.tmp_indices[i].ator = &uc->ator_tmp;
	}
	uc->obj.tmp_color_valid.ator = &uc->ator_tmp;
	uc->obj.tmp_faces.ator = &uc->ator_tmp;
	uc->obj.tmp_face_material.ator = &uc->ator_tmp;
	uc->obj.tmp_face_smoothing.ator = &uc->ator_tmp;
	uc->obj.tmp_face_group.ator = &uc->ator_tmp;
	uc->obj.tmp_face_group_infos.ator = &uc->ator_tmp;
	uc->obj.tmp_meshes.ator = &uc->ator_tmp;
	uc->obj.tmp_props.ator = &uc->ator_tmp;

	// .obj parsing does its own yield logic
	uc->data_size += uc->yield_size;

	uc->obj.object.data = ufbxi_empty_char;
	uc->obj.group.data = ufbxi_empty_char;

	ufbxi_map_init(&uc->obj.group_map, &uc->ator_tmp, ufbxi_map_cmp_const_char_ptr, NULL);

	// Add a nameless root node with the root ID
	{
		ufbxi_element_info root_info = { uc->root_id };
		root_info.name = ufbx_empty_string;
		ufbx_node *root = ufbxi_push_element(uc, &root_info, ufbx_node, UFBX_ELEMENT_NODE);
		ufbxi_check(root);
		ufbxi_setup_root_node(uc, root);
		ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &root->element.element_id));
	}

	return 1;
}

static ufbxi_noinline void ufbxi_obj_free(ufbxi_context *uc)
{
	if (!uc->obj.initialized) return;

	ufbxi_nounroll for (size_t i = 0; i < UFBXI_OBJ_NUM_ATTRIBS_EXT; i++) {
		ufbxi_buf_free(&uc->obj.tmp_vertices[i]);
		ufbxi_buf_free(&uc->obj.tmp_indices[i]);
	}
	ufbxi_buf_free(&uc->obj.tmp_color_valid);
	ufbxi_buf_free(&uc->obj.tmp_faces);
	ufbxi_buf_free(&uc->obj.tmp_face_material);
	ufbxi_buf_free(&uc->obj.tmp_face_smoothing);
	ufbxi_buf_free(&uc->obj.tmp_face_group);
	ufbxi_buf_free(&uc->obj.tmp_face_group_infos);
	ufbxi_buf_free(&uc->obj.tmp_meshes);
	ufbxi_buf_free(&uc->obj.tmp_props);

	ufbxi_map_free(&uc->obj.group_map);

	ufbxi_free(&uc->ator_tmp, ufbx_string, uc->obj.tokens, uc->obj.tokens_cap);
	ufbxi_free(&uc->ator_tmp, ufbx_material*, uc->obj.tmp_materials, uc->obj.tmp_materials_cap);
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_read_line(ufbxi_context *uc)
{
	ufbxi_dev_assert(!uc->obj.eof);

	size_t offset = 0;

	for (;;) {
		const char *begin = ufbxi_add_ptr(uc->data, offset);
		const char *end = begin ? (const char*)memchr(begin, '\n', uc->data_size - offset) : NULL;
		if (!end) {
			if (uc->eof) {
				offset = uc->data_size;
				uc->obj.eof = true;
				break;
			} else {
				size_t new_cap = ufbxi_max_sz(1, uc->data_size * 2);
				ufbxi_check(ufbxi_refill(uc, new_cap, false));
				continue;
			}
		}

		offset += ufbxi_to_size(end - begin) + 1;

		// Handle line continuations
		const char *esc = end;
		if (esc > begin && esc[-1] == '\r') esc--;
		if (esc > begin && esc[-1] == '\\') {
			continue;
		}

		break;
	}

	size_t line_len = offset;

	uc->obj.line.data = uc->data;
	uc->obj.line.length = line_len;
	uc->data += line_len;
	uc->data_size -= line_len;

	uc->obj.read_progress += line_len;
	if (uc->obj.read_progress >= uc->progress_interval) {
		ufbxi_check(ufbxi_report_progress(uc));
		uc->obj.read_progress %= uc->progress_interval;
	}

	if (uc->obj.eof) {
		char *new_data = ufbxi_push(&uc->tmp, char, line_len + 1);
		ufbxi_check(new_data);
		memcpy(new_data, uc->obj.line.data, line_len);
		new_data[line_len] = '\n';
		uc->obj.line.data = new_data;
		uc->obj.line.length++;
	}

	return 1;
}

static ufbxi_noinline ufbx_string ufbxi_obj_span_token(ufbxi_context *uc, size_t start_token, size_t end_token)
{
	ufbx_assert(start_token < uc->obj.num_tokens);
	end_token = ufbxi_min_sz(end_token, uc->obj.num_tokens - 1);

	ufbx_assert(start_token <= end_token);
	ufbx_string start = uc->obj.tokens[start_token];
	ufbx_string end = uc->obj.tokens[end_token];
	size_t num_between = ufbxi_to_size(end.data - start.data);

	ufbx_string result;
	result.data = start.data;
	result.length = num_between + end.length;
	return result;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_tokenize(ufbxi_context *uc)
{
	const char *ptr = uc->obj.line.data, *end = ptr + uc->obj.line.length;
	uc->obj.num_tokens = 0;

	for (;;) {
		char c;

		// Skip whitespace
		for (;;) {
			c = *ptr;
			if (c == ' ' || c == '\t' || c == '\r') {
				ptr++;
				continue;
			}

			// Treat line continuations as whitespace
			if (c == '\\') {
				const char *p = ptr + 1;
				if (*p == '\r') p++;
				if (*p == '\n' && p < end - 1) {
					ptr = p + 1;
					continue;
				}
			}

			break;
		}

		c = *ptr;
		if (c == '\n') break;
		if (c == '#' && uc->obj.num_tokens > 0) break;

		size_t index = uc->obj.num_tokens++;
		ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->obj.tokens, &uc->obj.tokens_cap, index + 1));

		ufbx_string *tok = &uc->obj.tokens[index];
		tok->data = ptr;

		// Treat comment start as a single token
		if (c == '#') {
			ptr++;
			tok->length = 1;
			continue;
		}

		for (;;) {
			c = *++ptr;

			if (ufbxi_is_space(c)) {
				break;
			}

			if (c == '\\') {
				const char *p = ptr + 1;
				if (*p == '\r') p++;
				if (*p == '\n' && p < end - 1) {
					break;
				}
			}
		}

		tok->length = ufbxi_to_size(ptr - tok->data);
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_tokenize_line(ufbxi_context *uc)
{
	ufbxi_check(ufbxi_obj_read_line(uc));
	ufbxi_check(ufbxi_obj_tokenize(uc));
	return 1;
}

static ufbxi_noinline int ufbxi_obj_parse_vertex(ufbxi_context *uc, ufbxi_obj_attrib attrib, size_t offset)
{
	if (uc->opts.ignore_geometry) return 1;

	ufbxi_buf *dst = &uc->obj.tmp_vertices[attrib];
	size_t num_values = ufbxi_obj_attrib_stride[attrib];
	uc->obj.vertex_count[attrib]++;

	size_t read_values = num_values;
	if (attrib == UFBXI_OBJ_ATTRIB_COLOR) {
		if (offset + read_values > uc->obj.num_tokens) {
			read_values = 3;
		}
	}
	ufbxi_check(offset + read_values <= uc->obj.num_tokens);

	uint32_t parse_flags = uc->double_parse_flags;
	ufbx_real *vals = ufbxi_push_fast(dst, ufbx_real, num_values);
	ufbxi_check(vals);
	for (size_t i = 0; i < read_values; i++) {
		ufbx_string str = uc->obj.tokens[offset + i];
		char *end; // ufbxi_uninit
		double val = ufbxi_parse_double(str.data, str.length, &end, parse_flags);
		ufbxi_check(end == str.data + str.length);
		vals[i] = (ufbx_real)val;
	}

	if (read_values < num_values) {
		ufbx_assert(read_values + 1 == num_values);
		ufbx_assert(attrib == UFBXI_OBJ_ATTRIB_COLOR);
		vals[read_values] = 1.0f;
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_index(ufbxi_context *uc, ufbx_string *s, uint32_t attrib)
{
	const char *ptr = s->data, *end = ptr + s->length;

	bool negative = false;
	if (*ptr == '-') {
		negative = true;
		ptr++;
	}

	// As .obj indices are never zero we can detect missing indices
	// by simply not writing to it.
	uint64_t index = 0;
	for (; ptr != end; ptr++) {
		char c = *ptr;
		if (c >= '0' && c <= '9') {
			ufbxi_check(index < UINT64_MAX / 10 - 10);
			index = index * 10 + (uint64_t)(c - '0');
		} else if (c == '/') {
			ptr++;
			break;
		}
	}

	if (negative) {
		size_t count = uc->obj.vertex_count[attrib];
		index = index <= count ? count - index : UINT64_MAX;
	} else {
		// Corrects to zero based indices and wraps 0 to UINT64_MAX (missing)
		index -= 1;
	}

	ufbxi_obj_fast_indices *fast_indices = &uc->obj.fast_indices[attrib];
	if (fast_indices->num_left == 0) {
		size_t num_push = 128;
		uint64_t *dst = ufbxi_push(&uc->obj.tmp_indices[attrib], uint64_t, num_push);
		ufbxi_check(dst);
		uc->obj.fast_indices[attrib].indices = dst;
		uc->obj.fast_indices[attrib].num_left = num_push;
	}

	*fast_indices->indices++ = index;
	fast_indices->num_left--;

	ufbxi_obj_mesh *mesh = uc->obj.mesh;

	if (index != UINT64_MAX) {
		ufbxi_obj_index_range *range = &mesh->vertex_range[attrib];
		range->min_ix = ufbxi_min64(range->min_ix, index);
		range->max_ix = ufbxi_max64(range->max_ix, index);
	}

	s->data = ptr;
	s->length = ufbxi_to_size(end - ptr);

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_indices(ufbxi_context *uc, size_t token_begin, size_t num_tokens)
{
	bool flush_mesh = false;
	if (uc->obj.object_dirty) {
		if (!uc->opts.obj_merge_objects) {
			flush_mesh = true;
		}
		uc->obj.object_dirty = false;
	}

	if (uc->obj.group_dirty) {
		if (((uc->obj.object.length == 0 || uc->opts.obj_merge_objects) && !uc->opts.obj_merge_groups) || uc->opts.obj_split_groups) {
			flush_mesh = true;
		}
		uc->obj.group_dirty = false;
		uc->obj.face_group_dirty = true;
	}

	if (!uc->obj.mesh || flush_mesh) {
		ufbxi_check(ufbxi_obj_flush_mesh(uc));
		ufbxi_check(ufbxi_obj_push_mesh(uc));
	}
	ufbxi_obj_mesh *mesh = uc->obj.mesh;

	if (uc->obj.material_dirty) {
		if (uc->obj.usemtl_fbx_id != 0) {
			ufbxi_fbx_id_entry *entry = ufbxi_find_fbx_id(uc, uc->obj.usemtl_fbx_id);
			ufbx_assert(entry);
			if (mesh->usemtl_base == 0 || entry->user_id < mesh->usemtl_base) {
				ufbxi_check(ufbxi_connect_oo(uc, uc->obj.usemtl_fbx_id, mesh->fbx_node_id));

				uint32_t index = ++uc->obj.usemtl_index;
				ufbxi_check(index < UINT32_MAX);
				entry->user_id = index;

				if (mesh->usemtl_base == 0) {
					mesh->usemtl_base = index;
				}
				uc->obj.face_material = index - mesh->usemtl_base;
			}
			uc->obj.face_material = entry->user_id - mesh->usemtl_base;
		}
	}

	// EARLY RETURN: Rest of the function should only be related to geometry!
	if (uc->opts.ignore_geometry) return 1;

	if (num_tokens == 0 && !uc->opts.allow_empty_faces) {
		ufbxi_check(ufbxi_warnf(UFBX_WARNING_EMPTY_FACE_REMOVED, "Empty face has been removed"));
		return 1;
	}

	if (uc->obj.face_group_dirty) {
		ufbx_string name = ufbx_empty_string;
		if (uc->obj.group.length > 0 && (uc->obj.object.length > 0 || uc->opts.obj_merge_groups) && !uc->opts.obj_split_groups) {
			name = uc->obj.group;
		}

		uint32_t hash = ufbxi_hash_ptr(name.data);
		ufbxi_obj_group_entry *entry = ufbxi_map_find(&uc->obj.group_map, ufbxi_obj_group_entry, hash, &name.data);
		if (!entry) {
			entry = ufbxi_map_insert(&uc->obj.group_map, ufbxi_obj_group_entry, hash, &name.data);
			ufbxi_check(entry);
			entry->name = name.data;
			entry->mesh_id = 0;
			entry->local_id = 0;
		}

		uint32_t mesh_id = mesh->fbx_mesh->element_id;
		if (entry->mesh_id != mesh_id) {
			uint32_t id = mesh->num_groups++;
			entry->mesh_id = mesh_id;
			entry->local_id = id;

			ufbx_face_group *group = ufbxi_push_zero(&uc->obj.tmp_face_group_infos, ufbx_face_group, 1);
			ufbxi_check(group);
			group->id = 0;
			group->name = name;
		}

		uc->obj.face_group = entry->local_id;

		if (!uc->obj.has_face_group) {
			uc->obj.has_face_group = true;
			ufbxi_check(ufbxi_push_zero(&uc->obj.tmp_face_group, uint32_t, uc->obj.tmp_faces.num_items));
		}

		uc->obj.face_group_dirty = false;
	}

	size_t num_indices = num_tokens;
	ufbxi_check(UINT32_MAX - mesh->num_indices >= num_indices);

	ufbx_face *face = ufbxi_push_fast(&uc->obj.tmp_faces, ufbx_face, 1);
	ufbxi_check(face);

	face->index_begin = (uint32_t)mesh->num_indices;
	face->num_indices = (uint32_t)num_indices;

	mesh->num_faces++;
	mesh->num_indices += num_indices;

	uint32_t *p_face_mat = ufbxi_push_fast(&uc->obj.tmp_face_material, uint32_t, 1);
	ufbxi_check(p_face_mat);
	*p_face_mat = uc->obj.face_material;

	if (uc->obj.has_face_smoothing) {
		bool *p_face_smooth = ufbxi_push_fast(&uc->obj.tmp_face_smoothing, bool, 1);
		ufbxi_check(p_face_smooth);
		*p_face_smooth = uc->obj.face_smoothing;
	}

	if (uc->obj.has_face_group) {
		uint32_t *p_face_group = ufbxi_push_fast(&uc->obj.tmp_face_group, uint32_t, 1);
		ufbxi_check(p_face_group);
		*p_face_group = uc->obj.face_group;
	}

	for (size_t ix = 0; ix < num_indices; ix++) {
		ufbx_string tok = uc->obj.tokens[token_begin + ix];
		for (uint32_t attrib = 0; attrib < UFBXI_OBJ_NUM_ATTRIBS; attrib++) {
			ufbxi_check(ufbxi_obj_parse_index(uc, &tok, attrib));
		}
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_multi_indices(ufbxi_context *uc, size_t window)
{
	for (size_t begin = 1; begin + window <= uc->obj.num_tokens; begin++) {
		ufbxi_check(ufbxi_obj_parse_indices(uc, begin, window));
	}
	return 1;
}

static ufbxi_noinline uint32_t ufbxi_parse_hex(const char *digits, size_t length)
{
	uint32_t value = 0;

	for (size_t i = 0; i < length; i++) {
		char c =  digits[i];
		uint32_t v = 0;
		if (c >= '0' && c <= '9') {
			v = (uint32_t)(c - '0');
		} else if (c >= 'A' && c <= 'F') {
			v = (uint32_t)(c - 'A') + 10;
		} else if (c >= 'a' && c <= 'f') {
			v = (uint32_t)(c - 'a') + 10;
		}
		value = (value << 4) | v;
	}

	return value;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_comment(ufbxi_context *uc)
{
	if (uc->obj.num_tokens >= 3 && ufbxi_str_equal(uc->obj.tokens[1], ufbxi_str_c("MRGB"))) {
		size_t num_color = uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_COLOR];

		// Pop standard vertex colors and replace them with MRGB colors
		if (num_color > uc->obj.mrgb_vertex_count) {
			size_t num_pop = num_color - uc->obj.mrgb_vertex_count;
			ufbxi_pop(&uc->obj.tmp_color_valid, bool, num_pop, NULL);
			ufbxi_pop(&uc->obj.tmp_vertices[UFBXI_OBJ_ATTRIB_COLOR], ufbx_real, num_pop * 4, NULL);
			uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_COLOR] -= num_pop;
		}

		ufbx_string mrgb = uc->obj.tokens[2];
		for (size_t i = 0; i + 8 <= mrgb.length; i += 8) {
			ufbx_real *p_rgba = ufbxi_push(&uc->obj.tmp_vertices[UFBXI_OBJ_ATTRIB_COLOR], ufbx_real, 4);
			bool *p_valid = ufbxi_push(&uc->obj.tmp_color_valid, bool, 1);
			ufbxi_check(p_rgba && p_valid);
			*p_valid = true;

			uint32_t hex = ufbxi_parse_hex(mrgb.data + i, 8);
			p_rgba[0] = (ufbx_real)((hex >> 16u) & 0xff) / 255.0f;
			p_rgba[1] = (ufbx_real)((hex >>  8u) & 0xff) / 255.0f;
			p_rgba[2] = (ufbx_real)((hex >>  0u) & 0xff) / 255.0f;
			p_rgba[3] = (ufbx_real)((hex >> 24u) & 0xff) / 255.0f;
		}

		uc->obj.has_vertex_color = true;
	}

	if (!uc->opts.disable_quirks) {
		if (ufbxi_match(&uc->obj.line, "\\s*#\\s*File exported by ZBrush.*")) {
			if (!uc->obj.mesh) {
				uc->opts.obj_merge_groups = true;
			}
		}
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_material(ufbxi_context *uc)
{
	ufbxi_check(uc->obj.num_tokens >= 2);
	ufbx_string name = ufbxi_obj_span_token(uc, 1, SIZE_MAX);

	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &name, false));

	uint64_t fbx_id = ufbxi_synthetic_id_from_string(name.data);

	ufbxi_fbx_id_entry *entry = ufbxi_find_fbx_id(uc, fbx_id);

	uc->obj.usemtl_fbx_id = fbx_id;
	uc->obj.usemtl_name = name;

	if (!entry) {
		ufbxi_element_info info = { 0 };
		info.fbx_id = fbx_id;
		info.name = name;

		ufbx_material *material = ufbxi_push_element(uc, &info, ufbx_material, UFBX_ELEMENT_MATERIAL);
		ufbxi_check(material);

		material->shader_type = UFBX_SHADER_WAVEFRONT_MTL;
		material->shading_model_name.data = ufbxi_empty_char;
		material->shader_prop_prefix.data = ufbxi_empty_char;

		size_t id = material->element_id;
		ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->obj.tmp_materials, &uc->obj.tmp_materials_cap, id + 1));
		uc->obj.tmp_materials[id] = material;
	}

	uc->obj.material_dirty = true;

	return 1;
}

#define ufbxi_obj_cmd1(a) ((uint32_t)(a)<<24u)
#define ufbxi_obj_cmd2(a,b) ((uint32_t)(a)<<24u | (uint32_t)(b)<<16)
#define ufbxi_obj_cmd3(a,b,c) ((uint32_t)(a)<<24u | (uint32_t)(b)<<16 | (uint32_t)(c)<<8u)

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_pop_vertices(ufbxi_context *uc, ufbx_real_list *dst, uint32_t attrib, uint64_t min_index)
{
	size_t stride = ufbxi_obj_attrib_stride[attrib];
	ufbxi_check(min_index < uc->obj.tmp_vertices[attrib].num_items / stride);

	size_t count = uc->obj.tmp_vertices[attrib].num_items - (size_t)min_index * stride;
	ufbx_real *data = ufbxi_push(&uc->result, ufbx_real, count + 4);
	ufbxi_check(data);

	data[0] = 0.0f;
	data[1] = 0.0f;
	data[2] = 0.0f;
	data[3] = 0.0f;
	data += 4;

	ufbxi_pop(&uc->obj.tmp_vertices[attrib], ufbx_real, count, data);

	dst->data = data;
	dst->count = count;
	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_setup_attrib(ufbxi_context *uc, ufbxi_obj_mesh *mesh, uint64_t *tmp_indices,
	ufbx_vertex_attrib *dst, const ufbx_real_list *p_data, uint32_t attrib, bool non_disjoint, bool required)
{
	ufbx_real_list data = *p_data;

	size_t num_indices = mesh->num_indices;
	size_t stride = ufbxi_obj_attrib_stride[attrib];
	size_t num_values = data.count / stride;

	uint64_t mesh_min_ix = mesh->vertex_range[attrib].min_ix;
	if (num_indices == 0 || num_values == 0 || mesh_min_ix == UINT64_MAX) {
		ufbxi_check(num_indices == 0 || !required);

		// Pop indices without copying if the attribute is not used
		ufbxi_pop(&uc->obj.tmp_indices[attrib], uint64_t, num_indices, NULL);
		return 1;
	}

	uint64_t min_index = non_disjoint ? 0 : mesh_min_ix;

	ufbxi_pop(&uc->obj.tmp_indices[attrib], uint64_t, num_indices, tmp_indices);

	uint32_t *dst_indices = ufbxi_push(&uc->result, uint32_t, num_indices);
	ufbxi_check(dst_indices);

	dst->exists = true;

	dst->values.data = data.data;
	dst->values.count = num_values;

	dst->indices.data = dst_indices;
	dst->indices.count = num_indices;

	ufbxi_nounroll for (size_t i = 0; i < num_indices; i++) {
		uint64_t ix = tmp_indices[i];
		if (ix != UINT64_MAX) {
			ix -= min_index;
			ufbxi_check(ix < UINT32_MAX);
		}
		if (ix < num_values) {
			dst_indices[i] = (uint32_t)ix;
		} else {
			ufbxi_check(ufbxi_fix_index(uc, &dst_indices[i], (uint32_t)ix, num_values));
		}
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_pad_colors(ufbxi_context *uc, size_t num_vertices)
{
	if (uc->opts.ignore_geometry) return 1;

	size_t num_colors = uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_COLOR];
	if (num_vertices > num_colors) {
		size_t num_pad = num_vertices - num_colors;
		ufbxi_check(ufbxi_push_zero(&uc->obj.tmp_vertices[UFBXI_OBJ_ATTRIB_COLOR], ufbx_real, num_pad * 4));
		ufbxi_check(ufbxi_push_zero(&uc->obj.tmp_color_valid, bool, num_pad));
		uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_COLOR] += num_pad;
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_pop_meshes(ufbxi_context *uc)
{
	size_t num_meshes = uc->obj.tmp_meshes.num_items;
	ufbxi_obj_mesh *meshes = ufbxi_push_pop(&uc->tmp, &uc->obj.tmp_meshes, ufbxi_obj_mesh, num_meshes);
	ufbxi_check(meshes);

	if (uc->obj.has_vertex_color) {
		ufbxi_check(ufbxi_obj_pad_colors(uc, uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_POSITION]));
	}

	// Pop unused fast indices
	for (size_t i = 0; i < UFBXI_OBJ_NUM_ATTRIBS; i++) {
		ufbxi_pop(&uc->obj.tmp_indices[i], uint64_t, uc->obj.fast_indices[i].num_left, NULL);
	}

	// Check if the file has disjoint vertices
	bool non_disjoint[UFBXI_OBJ_NUM_ATTRIBS] = { 0 };
	uint64_t next_min[UFBXI_OBJ_NUM_ATTRIBS] = { 0 };
	ufbx_real_list vertices[UFBXI_OBJ_NUM_ATTRIBS_EXT] = { 0 };
	bool *color_valid = NULL;

	size_t max_indices = 0;

	for (size_t i = 0; i < num_meshes; i++) {
		ufbxi_obj_mesh *mesh = &meshes[i];
		max_indices = ufbxi_max_sz(max_indices, mesh->num_indices);
		ufbxi_nounroll for (uint32_t attrib = 0; attrib < UFBXI_OBJ_NUM_ATTRIBS; attrib++) {
			ufbxi_obj_index_range range = mesh->vertex_range[attrib];
			if (range.min_ix > range.max_ix) continue;
			if (range.min_ix < next_min[attrib]) {
				non_disjoint[attrib] = true;
			}
			next_min[attrib] = range.max_ix + 1;
		}
	}

	uint64_t *tmp_indices = ufbxi_push(&uc->tmp, uint64_t, max_indices);
	ufbxi_check(tmp_indices);

	ufbxi_nounroll for (uint32_t attrib = 0; attrib < UFBXI_OBJ_NUM_ATTRIBS; attrib++) {
		if (!non_disjoint[attrib]) continue;
		ufbxi_check(ufbxi_obj_pop_vertices(uc, &vertices[attrib], attrib, 0));
	}
	if (uc->obj.has_vertex_color && non_disjoint[UFBXI_OBJ_ATTRIB_POSITION]) {
		ufbxi_check(ufbxi_obj_pop_vertices(uc, &vertices[UFBXI_OBJ_ATTRIB_COLOR], UFBXI_OBJ_ATTRIB_COLOR, 0));
		color_valid = ufbxi_push_pop(&uc->tmp, &uc->obj.tmp_color_valid, bool, vertices[UFBXI_OBJ_ATTRIB_COLOR].count / 4);
		ufbxi_check(color_valid);
	}

	for (size_t i = num_meshes; i > 0; i--) {
		ufbxi_obj_mesh *mesh = &meshes[i - 1];

		ufbx_mesh *fbx_mesh = mesh->fbx_mesh;

		size_t num_faces = mesh->num_faces;

		if (!uc->opts.ignore_geometry) {
			ufbxi_nounroll for (uint32_t attrib = 0; attrib < UFBXI_OBJ_NUM_ATTRIBS; attrib++) {
				if (non_disjoint[attrib]) continue;
				uint64_t min_ix = mesh->vertex_range[attrib].min_ix;
				if (min_ix < UINT64_MAX) {
					ufbxi_check(ufbxi_obj_pop_vertices(uc, &vertices[attrib], attrib, min_ix));
				}
			}
			if (uc->obj.has_vertex_color && !non_disjoint[UFBXI_OBJ_ATTRIB_POSITION]) {
				uint64_t min_ix = mesh->vertex_range[UFBXI_OBJ_ATTRIB_POSITION].min_ix;
				ufbxi_check(min_ix < UINT64_MAX);
				ufbxi_check(ufbxi_obj_pop_vertices(uc, &vertices[UFBXI_OBJ_ATTRIB_COLOR], UFBXI_OBJ_ATTRIB_COLOR, min_ix));
				color_valid = ufbxi_push_pop(&uc->tmp, &uc->obj.tmp_color_valid, bool, vertices[UFBXI_OBJ_ATTRIB_COLOR].count / 4);
				ufbxi_check(color_valid);
			}

			fbx_mesh->faces.count = num_faces;
			fbx_mesh->face_material.count = num_faces;

			fbx_mesh->faces.data = ufbxi_push_pop(&uc->result, &uc->obj.tmp_faces, ufbx_face, num_faces);
			fbx_mesh->face_material.data = ufbxi_push_pop(&uc->result, &uc->obj.tmp_face_material, uint32_t, num_faces);

			ufbxi_check(fbx_mesh->faces.data);
			ufbxi_check(fbx_mesh->face_material.data);

			if (uc->obj.has_face_smoothing) {
				fbx_mesh->face_smoothing.count = num_faces;
				fbx_mesh->face_smoothing.data = ufbxi_push_pop(&uc->result, &uc->obj.tmp_face_smoothing, bool, num_faces);
				ufbxi_check(fbx_mesh->face_smoothing.data);
			}

			if (uc->obj.has_face_group) {
				if (mesh->num_groups > 1) {
					fbx_mesh->face_group.count = num_faces;
					fbx_mesh->face_group.data = ufbxi_push_pop(&uc->result, &uc->obj.tmp_face_group, uint32_t, num_faces);
					ufbxi_check(fbx_mesh->face_group.data);
				} else {
					ufbxi_pop(&uc->obj.tmp_face_group, uint32_t, num_faces, NULL);
				}
			}

			ufbxi_check(ufbxi_obj_setup_attrib(uc, mesh, tmp_indices, (ufbx_vertex_attrib*)&fbx_mesh->vertex_position,
				&vertices[UFBXI_OBJ_ATTRIB_POSITION], UFBXI_OBJ_ATTRIB_POSITION, non_disjoint[UFBXI_OBJ_ATTRIB_POSITION], true));

			ufbxi_check(ufbxi_obj_setup_attrib(uc, mesh, tmp_indices, (ufbx_vertex_attrib*)&fbx_mesh->vertex_uv,
				&vertices[UFBXI_OBJ_ATTRIB_UV], UFBXI_OBJ_ATTRIB_UV, non_disjoint[UFBXI_OBJ_ATTRIB_UV], false));

			ufbxi_check(ufbxi_obj_setup_attrib(uc, mesh, tmp_indices, (ufbx_vertex_attrib*)&fbx_mesh->vertex_normal,
				&vertices[UFBXI_OBJ_ATTRIB_NORMAL], UFBXI_OBJ_ATTRIB_NORMAL, non_disjoint[UFBXI_OBJ_ATTRIB_NORMAL], false));

			if (uc->obj.has_vertex_color) {
				ufbx_assert(color_valid);
				bool has_color = false;
				bool all_valid = true;
				size_t max_index = fbx_mesh->vertex_position.values.count;
				ufbxi_for_list(uint32_t, p_ix, fbx_mesh->vertex_position.indices) {
					if (*p_ix < max_index) {
						if (color_valid[*p_ix]) {
							has_color = true;
						} else {
							all_valid = false;
						}
					}
				}

				if (has_color) {
					fbx_mesh->vertex_color.exists = true;
					fbx_mesh->vertex_color.values.data = (ufbx_vec4*)vertices[UFBXI_OBJ_ATTRIB_COLOR].data;
					fbx_mesh->vertex_color.values.count = vertices[UFBXI_OBJ_ATTRIB_COLOR].count / 4;
					fbx_mesh->vertex_color.indices = fbx_mesh->vertex_position.indices;
					fbx_mesh->vertex_color.unique_per_vertex = true;

					if (!all_valid) {
						uint32_t *indices = fbx_mesh->vertex_color.indices.data;
						indices = ufbxi_push_copy(&uc->result, uint32_t, mesh->num_indices, indices);
						ufbxi_check(indices);

						size_t num_values = fbx_mesh->vertex_color.values.count;
						ufbxi_for(uint32_t, p_ix, indices, mesh->num_indices) {
							if (*p_ix >= num_values || !color_valid[*p_ix]) {
								ufbxi_check(ufbxi_fix_index(uc, p_ix, *p_ix, num_values));
							}
						}

						fbx_mesh->vertex_color.indices.data = indices;
					}
				}
			}
		}

		ufbxi_check(ufbxi_finalize_mesh(&uc->result, &uc->error, fbx_mesh));

		if (uc->retain_mesh_parts) {
			fbx_mesh->face_group_parts.count = mesh->num_groups;
			fbx_mesh->face_group_parts.data = ufbxi_push_zero(&uc->result, ufbx_mesh_part, mesh->num_groups);
			ufbxi_check(fbx_mesh->face_group_parts.data);
		}

		if (mesh->num_groups > 1) {
			ufbxi_check(ufbxi_update_face_groups(&uc->result, &uc->error, fbx_mesh, false));
		} else if (mesh->num_groups == 1) {
			fbx_mesh->face_group.data = (uint32_t*)ufbxi_sentinel_index_zero;
			fbx_mesh->face_group.count = num_faces;
			// NOTE: Consecutive and zero indices are always allocated so we can skip doing it here,
			// see HACK(consecutiv-faces)..
			if (fbx_mesh->face_group_parts.count > 0) {
				ufbx_mesh_part *part = &fbx_mesh->face_group_parts.data[0];
				part->num_faces = fbx_mesh->num_faces;
				part->num_faces = num_faces;
				part->num_empty_faces = fbx_mesh->num_empty_faces;
				part->num_point_faces = fbx_mesh->num_point_faces;
				part->num_line_faces = fbx_mesh->num_line_faces;
				part->num_triangles = fbx_mesh->num_triangles;
				part->face_indices.data = (uint32_t*)ufbxi_sentinel_index_consecutive;
				part->face_indices.count = num_faces;
			}
		}

		// HACK(consecutive-faces): Prepare for finalize to re-use a consecutive/zero
		// index buffer for face materials..
		uc->max_zero_indices = ufbxi_max_sz(uc->max_zero_indices, num_faces);
		uc->max_consecutive_indices = ufbxi_max_sz(uc->max_consecutive_indices, num_faces);
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_file(ufbxi_context *uc)
{
	while (!uc->obj.eof) {
		ufbxi_check(ufbxi_obj_tokenize_line(uc));
		size_t num_tokens = uc->obj.num_tokens;
		if (num_tokens == 0) continue;

		ufbx_string cmd = uc->obj.tokens[0];
		uint32_t key = ufbxi_get_name_key(cmd.data, cmd.length);
		if (key == ufbxi_obj_cmd1('v')) {
			ufbxi_check(ufbxi_obj_parse_vertex(uc, UFBXI_OBJ_ATTRIB_POSITION, 1));
			if (num_tokens >= 7) {
				size_t num_vertices = uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_POSITION];
				uc->obj.has_vertex_color = true;
				ufbxi_check(ufbxi_obj_pad_colors(uc, num_vertices - 1));
				if (uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_COLOR] < num_vertices) {
					ufbx_assert(uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_COLOR] == num_vertices - 1);
					ufbxi_check(ufbxi_obj_parse_vertex(uc, UFBXI_OBJ_ATTRIB_COLOR, 4));
					bool *valid = ufbxi_push(&uc->obj.tmp_color_valid, bool, 1);
					ufbxi_check(valid);
					*valid = true;
				}
			}
		} else if (key == ufbxi_obj_cmd2('v','t')) {
			ufbxi_check(ufbxi_obj_parse_vertex(uc, UFBXI_OBJ_ATTRIB_UV, 1));
		} else if (key == ufbxi_obj_cmd2('v','n')) {
			ufbxi_check(ufbxi_obj_parse_vertex(uc, UFBXI_OBJ_ATTRIB_NORMAL, 1));
		} else if (key == ufbxi_obj_cmd1('f')) {
			ufbxi_check(ufbxi_obj_parse_indices(uc, 1, uc->obj.num_tokens - 1));
		} else if (key == ufbxi_obj_cmd1('p')) {
			ufbxi_check(ufbxi_obj_parse_multi_indices(uc, 1));
		} else if (key == ufbxi_obj_cmd1('l')) {
			ufbxi_check(ufbxi_obj_parse_multi_indices(uc, 2));
		} else if (key == ufbxi_obj_cmd1('s')) {
			if (num_tokens >= 2) {
				uc->obj.has_face_smoothing = true;
				uc->obj.face_smoothing = !ufbxi_str_equal(uc->obj.tokens[1], ufbxi_str_c("off"));

				// Fill in previously missed face smoothing data
				if (uc->obj.tmp_face_smoothing.num_items == 0 && uc->obj.tmp_faces.num_items > 0) {
					ufbxi_check(ufbxi_push_zero(&uc->obj.tmp_face_smoothing, bool, uc->obj.tmp_faces.num_items));
				}
			}
		} else if (key == ufbxi_obj_cmd1('o')) {
			if (num_tokens >= 2) {
				uc->obj.object = ufbxi_obj_span_token(uc, 1, SIZE_MAX);
				ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &uc->obj.object, false));
				uc->obj.object_dirty = true;
			}
		} else if (key == ufbxi_obj_cmd1('g')) {
			if (num_tokens >= 2) {
				uc->obj.group = ufbxi_obj_span_token(uc, 1, SIZE_MAX);
				ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &uc->obj.group, false));
				uc->obj.group_dirty = true;
			} else {
				uc->obj.group = ufbx_empty_string;
				uc->obj.group_dirty = true;
			}
		} else if (key == ufbxi_obj_cmd1('#')) {
			ufbxi_check(ufbxi_obj_parse_comment(uc));
		} else if (ufbxi_str_equal(cmd, ufbxi_str_c("mtllib"))) {
			ufbxi_check(uc->obj.num_tokens >= 2);
			ufbx_string lib = ufbxi_obj_span_token(uc, 1, SIZE_MAX);
			lib.data = ufbxi_push_copy(&uc->tmp, char, lib.length + 1, lib.data);
			ufbxi_check(lib.data);
			uc->obj.mtllib_relative_path.data = lib.data;
			uc->obj.mtllib_relative_path.size = lib.length;
		} else if (ufbxi_str_equal(cmd, ufbxi_str_c("usemtl"))) {
			ufbxi_check(ufbxi_obj_parse_material(uc));
		} else {
			ufbxi_check(ufbxi_warnf(UFBX_WARNING_UNKNOWN_OBJ_DIRECTIVE, "Unknown .obj directive, skipped line"));
		}
	}

	ufbxi_check(ufbxi_obj_flush_mesh(uc));
	ufbxi_check(ufbxi_obj_pop_meshes(uc));

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_flush_material(ufbxi_context *uc)
{
	if (uc->obj.usemtl_fbx_id == 0) return 1;

	ufbxi_fbx_id_entry *entry = ufbxi_find_fbx_id(uc, uc->obj.usemtl_fbx_id);
	ufbx_assert(entry);
	ufbx_material *material = uc->obj.tmp_materials[entry->element_id];

	size_t num_props = uc->obj.tmp_props.num_items;
	ufbxi_check(ufbxi_obj_pop_props(uc, &material->props.props, num_props));

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_prop(ufbxi_context *uc, ufbx_string name, size_t start, bool include_rest, size_t *p_next)
{
	if (start >= uc->obj.num_tokens) {
		if (p_next) {
			*p_next = start;
		}
		return 1;
	}

	ufbx_prop *prop = ufbxi_push_zero(&uc->obj.tmp_props, ufbx_prop, 1);
	ufbxi_check(prop);
	prop->name = name;

	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &prop->name, false));

	uint32_t flags = UFBX_PROP_FLAG_VALUE_STR;

	size_t num_reals = 0;
	for (; num_reals < 4; num_reals++) {
		if (start + num_reals >= uc->obj.num_tokens) break;
		ufbx_string tok = uc->obj.tokens[start + num_reals];

		char *end; // ufbxi_uninit
		double val = ufbxi_parse_double(tok.data, tok.length, &end, uc->double_parse_flags);
		if (end != tok.data + tok.length) break;

		prop->value_real_arr[num_reals] = (ufbx_real)val;
		if (num_reals == 0) {
			prop->value_int = ufbxi_f64_to_i64(val);
			flags |= UFBX_PROP_FLAG_VALUE_INT;
		}
	}

	size_t num_args = 0;
	if (!include_rest) {
		for (; start + num_args < uc->obj.num_tokens - 1; num_args++) {
			if (ufbxi_match(&uc->obj.tokens[start + num_args], "-[A-Za-z][\\-A-Za-z0-9_]*")) break;
		}
	}

	if (num_args > 0 || include_rest) {
		ufbx_string span = ufbxi_obj_span_token(uc, start, include_rest ? SIZE_MAX : start + num_args - 1);
		prop->value_str = span;
		prop->value_blob.data = span.data;
		prop->value_blob.size = span.length;

		ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &prop->value_str, false));
		ufbxi_check(ufbxi_push_string_place_blob(&uc->string_pool, &prop->value_blob, true));
	} else {
		prop->value_str.data = ufbxi_empty_char;
	}

	if (num_reals > 0) {
		flags = (uint32_t)UFBX_PROP_FLAG_VALUE_REAL << (num_reals - 1);
	} else {
		if (!strcmp(prop->value_str.data, "on")) {
			prop->value_int = 1;
			prop->value_real = 1.0f;
			flags |= UFBX_PROP_FLAG_VALUE_INT;
		} else if (!strcmp(prop->value_str.data, "off")) {
			prop->value_int = 0;
			prop->value_real = 0.0f;
			flags |= UFBX_PROP_FLAG_VALUE_INT;
		}
	}

	prop->flags = (ufbx_prop_flags)flags;

	if (p_next) {
		*p_next = start + num_args;
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_mtl_map(ufbxi_context *uc, size_t prefix_len)
{
	if (uc->obj.num_tokens < 2) return 1;

	size_t num_props = 1;
	ufbxi_check(ufbxi_obj_parse_prop(uc, ufbxi_str_c("obj|args"), 1, true, NULL));

	size_t start = 1;
	for (; start + 1 < uc->obj.num_tokens; ) {
		ufbx_string tok = uc->obj.tokens[start];
		if (ufbxi_match(&tok, "-[A-Za-z][\\-A-Za-z0-9_]*")) {
			tok.data += 1;
			tok.length -= 1;
			ufbxi_check(ufbxi_obj_parse_prop(uc, tok, start + 1, false, &start));
			num_props++;
		} else {
			break;
		}
	}

	ufbx_string tex_str = ufbxi_obj_span_token(uc, start, SIZE_MAX);
	ufbx_blob tex_raw = { tex_str.data, tex_str.length };

	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &tex_str, false));
	ufbxi_check(ufbxi_push_string_place_blob(&uc->string_pool, &tex_raw, true));

	uint64_t fbx_id = 0;
	ufbx_texture *texture = ufbxi_push_synthetic_element(uc, &fbx_id, NULL, "", ufbx_texture, UFBX_ELEMENT_TEXTURE);
	ufbxi_check(texture);

	texture->filename.data = ufbxi_empty_char;
	texture->absolute_filename.data = ufbxi_empty_char;
	texture->uv_set.data = ufbxi_empty_char;

	texture->relative_filename = tex_str;
	texture->raw_relative_filename = tex_raw;

	ufbxi_check(ufbxi_obj_pop_props(uc, &texture->props.props, num_props));

	ufbx_string prop = uc->obj.tokens[0];
	ufbx_assert(prop.length >= prefix_len);
	prop.data += prefix_len;
	prop.length -= prefix_len;
	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &prop, false));

	if (uc->obj.usemtl_fbx_id != 0) {
		ufbxi_check(ufbxi_connect_op(uc, fbx_id, uc->obj.usemtl_fbx_id, prop));
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_mtl(ufbxi_context *uc)
{
	uc->obj.mesh = NULL;
	uc->obj.usemtl_fbx_id = 0;

	while (!uc->obj.eof) {
		ufbxi_check(ufbxi_obj_tokenize_line(uc));
		size_t num_tokens = uc->obj.num_tokens;
		if (num_tokens == 0) continue;

		ufbx_string cmd = uc->obj.tokens[0];
		if (ufbxi_str_equal(cmd, ufbxi_str_c("newmtl"))) {
			// HACK: Reuse mesh material parsing
			ufbxi_check(ufbxi_obj_flush_material(uc));
			ufbxi_check(ufbxi_obj_parse_material(uc));
		} else if (cmd.length > 4 && !memcmp(cmd.data, "map_", 4)) {
			ufbxi_check(ufbxi_obj_parse_mtl_map(uc, 4));
		} else if (cmd.length == 4 && (!memcmp(cmd.data, "bump", 4) || !memcmp(cmd.data, "disp", 4) || !memcmp(cmd.data, "norm", 4))) {
			ufbxi_check(ufbxi_obj_parse_mtl_map(uc, 0));
		} else if (cmd.length == 1 && cmd.data[0] == '#') {
			// Implement .mtl magic comment handling here if necessary
		} else {
			ufbxi_check(ufbxi_obj_parse_prop(uc, uc->obj.tokens[0], 1, true, NULL));
		}
	}

	ufbxi_check(ufbxi_obj_flush_material(uc));

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_load_mtl(ufbxi_context *uc)
{
	// HACK: Reset everything and switch to loading the .mtl file globally
	if (uc->close_fn) {
		uc->close_fn(uc->read_user);
	}

	uc->read_fn = NULL;
	uc->close_fn = NULL;
	uc->read_user = NULL;
	uc->data_begin = NULL;
	uc->data = NULL;
	uc->data_size = 0;
	uc->yield_size = 0;
	uc->eof = false;
	uc->obj.eof = false;

	if (uc->opts.obj_mtl_data.size > 0) {
		uc->data_begin = uc->data = (const char*)uc->opts.obj_mtl_data.data;
		uc->data_size = uc->opts.obj_mtl_data.size;
		ufbxi_check(ufbxi_obj_parse_mtl(uc));
		return 1;
	}

	ufbx_stream stream = { 0 };
	bool has_stream = false;
	bool needs_stream = false;
	ufbx_blob stream_path = { 0 };

	if (uc->opts.open_file_cb.fn) {
		if (uc->opts.obj_mtl_path.length > 0) {
			has_stream = ufbxi_open_file(&uc->opts.open_file_cb, &stream, uc->opts.obj_mtl_path.data, uc->opts.obj_mtl_path.length, NULL, &uc->ator_tmp, UFBX_OPEN_FILE_OBJ_MTL);
			stream_path.data = uc->opts.obj_mtl_path.data;
			stream_path.size = uc->opts.obj_mtl_path.length;
			needs_stream = true;
			if (!has_stream) {
				ufbxi_check(ufbxi_warnf(UFBX_WARNING_MISSING_EXTERNAL_FILE, "Could not open .mtl file: %s", uc->opts.obj_mtl_path.data));
			}
		}

		if (!has_stream && uc->opts.load_external_files && uc->obj.mtllib_relative_path.size > 0) {
			ufbx_blob dst; // ufbxi_uninit
			ufbxi_check(ufbxi_resolve_relative_filename(uc, (ufbxi_strblob*)&dst, (const ufbxi_strblob*)&uc->obj.mtllib_relative_path, true));
			has_stream = ufbxi_open_file(&uc->opts.open_file_cb, &stream, (const char*)dst.data, dst.size, &uc->obj.mtllib_relative_path, &uc->ator_tmp, UFBX_OPEN_FILE_OBJ_MTL);
			stream_path = uc->obj.mtllib_relative_path;
			needs_stream = true;
			if (!has_stream) {
				ufbxi_check(ufbxi_warnf(UFBX_WARNING_MISSING_EXTERNAL_FILE, "Could not open .mtl file: %s", dst.data));
			}
		}

		ufbx_string path = uc->scene.metadata.filename;
		if (!has_stream && uc->opts.load_external_files && uc->opts.obj_search_mtl_by_filename && path.length > 4) {
			ufbx_string ext = { path.data + path.length - 4, 4 };
			if (ufbxi_match(&ext, "\\c.obj")) {
				ufbxi_analysis_assert(path.length < SIZE_MAX - 1);
				char *copy = ufbxi_push_copy(&uc->tmp, char, path.length + 1, path.data);
				ufbxi_check(copy);
				copy[path.length - 3] = copy[path.length - 3] == 'O' ? 'M' : 'm';
				copy[path.length - 2] = copy[path.length - 2] == 'B' ? 'T' : 't';
				copy[path.length - 1] = copy[path.length - 1] == 'J' ? 'L' : 'l';
				has_stream = ufbxi_open_file(&uc->opts.open_file_cb, &stream, copy, path.length, NULL, &uc->ator_tmp, UFBX_OPEN_FILE_OBJ_MTL);
				if (has_stream) {
					ufbxi_check(ufbxi_warnf(UFBX_WARNING_IMPLICIT_MTL, "Opened .mtl file derived from .obj filename: %s", copy));
				}
			}
		}
	}

	if (has_stream) {
		// Adopt `stream` to ufbx read callbacks
		uc->read_fn = stream.read_fn;
		uc->close_fn = stream.close_fn;
		uc->read_user = stream.user;

		int ok = ufbxi_obj_parse_mtl(uc);

		if (uc->close_fn) {
			uc->close_fn(uc->read_user);
		}
		uc->read_fn = NULL;
		uc->close_fn = NULL;
		uc->read_user = NULL;

		ufbxi_check(ok);
	} else if (needs_stream && !uc->opts.ignore_missing_external_files) {
		ufbxi_set_err_info(&uc->error, (const char*)stream_path.data, stream_path.size);
		ufbxi_fail_msg("ufbxi_obj_load_mtl()", "External file not found");
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_load(ufbxi_context *uc)
{
	ufbxi_check(ufbxi_obj_init(uc));
	ufbxi_check(ufbxi_obj_parse_file(uc));
	ufbxi_check(ufbxi_init_file_paths(uc));
	ufbxi_check(ufbxi_obj_load_mtl(uc));

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_mtl_load(ufbxi_context *uc)
{
	ufbxi_check(ufbxi_obj_init(uc));
	ufbxi_check(ufbxi_init_file_paths(uc));
	ufbxi_check(ufbxi_obj_parse_mtl(uc));

	return 1;
}

#else
ufbxi_nodiscard static ufbxi_forceinline int ufbxi_obj_load(ufbxi_context *uc)
{
	ufbxi_fmt_err_info(&uc->error, "UFBX_ENABLE_FORMAT_OBJ");
	ufbxi_fail_msg("UFBXI_FEATURE_FORMAT_OBJ", "Feature disabled");
}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_mtl_load(ufbxi_context *uc)
{
	ufbxi_fmt_err_info(&uc->error, "UFBX_ENABLE_FORMAT_OBJ");
	ufbxi_fail_msg("UFBXI_FEATURE_FORMAT_OBJ", "Feature disabled");
}

static ufbxi_forceinline void ufbxi_obj_free(ufbxi_context *uc)
{
}
#endif

// -- Scene pre-processing

typedef struct {
	ufbx_element *src, *dst;
} ufbxi_pre_connection;

typedef struct {
	bool has_constant_scale;
	bool has_recursive_scale_helper;
	bool has_skin_deformer;
	ufbx_vec3 constant_scale;
	uint32_t element_id;
	uint32_t first_child;
	uint32_t next_child;
	uint32_t parent;
} ufbxi_pre_node;

typedef struct {
	bool has_skin_deformer;
} ufbxi_pre_mesh;

typedef struct {
	bool has_constant_value;
	ufbx_vec3 constant_value;
} ufbxi_pre_anim_value;

// Called between parsing and `ufbxi_finalize_scene()`.
// This is a very messy function reminiscent of the _old_ ufbx, where we do
// multiple passes over connections without having a proper scene graph.
// This, however gives us the advantage of allowing us to modify elements
// and connections. We can, for example, add new helper nodes and redirect
// animated properties from source nodes to the helpers. The rest of ufbx
// will treat these as if they were a part of the source file.
ufbxi_nodiscard ufbxi_noinline static int ufbxi_pre_finalize_scene(ufbxi_context *uc)
{
	bool required = false;
	if (uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_HELPER_NODES || uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY) required = true;
	if (uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_HELPER_NODES || uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE || uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE_NO_FALLBACK) required = true;
	if (uc->opts.pivot_handling == UFBX_PIVOT_HANDLING_ADJUST_TO_PIVOT) required = true;
#if defined(UFBX_REGRESSION)
	required = true;
#endif

	if (!required) return 1;

	uint32_t num_elements = uc->num_elements;
	size_t num_nodes = uc->tmp_node_ids.num_items;
	ufbx_element **elements = ufbxi_push_pop(&uc->tmp_parse, &uc->tmp_element_ptrs, ufbx_element*, num_elements);
	ufbxi_check(elements);

	size_t num_connections = uc->tmp_connections.num_items;
	ufbxi_tmp_connection *tmp_connections = ufbxi_push_peek(&uc->tmp_parse, &uc->tmp_connections, ufbxi_tmp_connection, num_connections);
	ufbxi_check(tmp_connections);

	ufbxi_pre_connection *pre_connections = ufbxi_push(&uc->tmp_parse, ufbxi_pre_connection, num_connections);
	ufbxi_check(pre_connections);

	uint32_t *instance_counts = ufbxi_push_zero(&uc->tmp_parse, uint32_t, num_elements);
	ufbxi_check(instance_counts);

	bool *modify_not_supported = ufbxi_push_zero(&uc->tmp_parse, bool, num_elements);
	ufbxi_check(modify_not_supported);

	bool *has_unscaled_children = ufbxi_push_zero(&uc->tmp_parse, bool, num_nodes);
	ufbxi_check(has_unscaled_children);

	bool *has_scale_animation = ufbxi_push_zero(&uc->tmp_parse, bool, num_nodes);
	ufbxi_check(has_scale_animation);

	ufbxi_pre_node *pre_nodes = ufbxi_push_zero(&uc->tmp_parse, ufbxi_pre_node, num_nodes);
	ufbxi_check(pre_nodes);

	size_t num_meshes = uc->tmp_typed_element_offsets[UFBX_ELEMENT_MESH].num_items;
	ufbxi_pre_mesh *pre_meshes = ufbxi_push_zero(&uc->tmp_parse, ufbxi_pre_mesh, num_meshes);
	ufbxi_check(pre_meshes);

	size_t num_anim_values = uc->tmp_typed_element_offsets[UFBX_ELEMENT_ANIM_VALUE].num_items;
	ufbxi_pre_anim_value *pre_anim_values = ufbxi_push_zero(&uc->tmp_parse, ufbxi_pre_anim_value, num_anim_values);
	ufbxi_check(pre_anim_values);

	uint64_t *fbx_ids = ufbxi_push_pop(&uc->tmp_parse, &uc->tmp_element_fbx_ids, uint64_t, num_elements);
	ufbxi_check(fbx_ids);

	// TODO
	const ufbx_real scale_epsilon = 0.001f;
	const ufbx_real pivot_epsilon = 0.001f;
	const ufbx_real compensate_epsilon = 0.01f;

	for (size_t i = 0; i < num_elements; i++) {
		ufbx_element *element = elements[i];
		uint32_t id = element->typed_id;

		if (element->type == UFBX_ELEMENT_NODE) {
			ufbxi_pre_node *pre_node = &pre_nodes[id];
			pre_node->has_constant_scale = true;
			pre_node->constant_scale = ufbxi_find_vec3(&element->props, ufbxi_Lcl_Scaling, 1.0f, 1.0f, 1.0f);
			pre_node->element_id = element->element_id;
			pre_node->first_child = ~0u;
			pre_node->next_child = ~0u;
			pre_node->parent = ~0u;
		} if (element->type == UFBX_ELEMENT_ANIM_VALUE) {
			ufbxi_pre_anim_value *pre_value = &pre_anim_values[id];
			pre_value->has_constant_value = true;
			pre_value->constant_value.x = ufbxi_find_real(&element->props, ufbxi_X, UFBX_NAN);
			pre_value->constant_value.x = ufbxi_find_real(&element->props, ufbxi_d_X, pre_value->constant_value.x);
			pre_value->constant_value.y = ufbxi_find_real(&element->props, ufbxi_Y, UFBX_NAN);
			pre_value->constant_value.y = ufbxi_find_real(&element->props, ufbxi_d_Y, pre_value->constant_value.y);
			pre_value->constant_value.z = ufbxi_find_real(&element->props, ufbxi_Z, UFBX_NAN);
			pre_value->constant_value.z = ufbxi_find_real(&element->props, ufbxi_d_Z, pre_value->constant_value.z);
		}
	}

	for (size_t i = 0; i < num_connections; i++) {
		ufbxi_tmp_connection *tmp = &tmp_connections[i];
		ufbxi_pre_connection *pre = &pre_connections[i];

		ufbxi_fbx_id_entry *src_entry = ufbxi_find_fbx_id(uc, tmp->src);
		ufbxi_fbx_id_entry *dst_entry = ufbxi_find_fbx_id(uc, tmp->dst);

		ufbx_element *src = src_entry ? elements[src_entry->element_id] : NULL;
		ufbx_element *dst = dst_entry ? elements[dst_entry->element_id] : NULL;
		pre->src = src;
		pre->dst = dst;
		if (!src || !dst) continue;

		if (tmp->src_prop.length == 0 && tmp->dst_prop.length == 0) {
			// Count number of instances of each attribute
			if (dst->type == UFBX_ELEMENT_NODE) {
				ufbx_node *dst_node = (ufbx_node*)dst;

				if (src->type >= UFBX_ELEMENT_TYPE_FIRST_ATTRIB && src->type <= UFBX_ELEMENT_TYPE_LAST_ATTRIB) {
					++instance_counts[src->element_id];

					// These must match what can be trasnsformed in `ufbxi_modify_geometry()`
					switch (src->type) {
					case UFBX_ELEMENT_MESH:
					case UFBX_ELEMENT_LINE_CURVE:
					case UFBX_ELEMENT_NURBS_CURVE:
					case UFBX_ELEMENT_NURBS_SURFACE:
						break; // Nop, supported
					default:
						modify_not_supported[dst->element_id] = true;
						break;
					}
				}

				if (src->type == UFBX_ELEMENT_NODE) {
					ufbx_node *src_node = (ufbx_node*)src;
					ufbxi_pre_node *pre_dst = &pre_nodes[dst_node->typed_id];
					ufbxi_pre_node *pre_src = &pre_nodes[src_node->typed_id];

					// Remember parent and add children into a linked list
					if (pre_src->parent == ~0u) {
						pre_src->parent = dst_node->typed_id;
						pre_src->next_child = pre_dst->first_child;
						pre_dst->first_child = src_node->typed_id;
					}

					if (uc->opts.inherit_mode_handling != UFBX_INHERIT_MODE_HANDLING_PRESERVE) {
						if (!dst_node->is_root && src_node->original_inherit_mode != UFBX_INHERIT_MODE_NORMAL) {
							has_unscaled_children[dst->typed_id] = true;
						}
					}
				}
			} else if (dst->type == UFBX_ELEMENT_MESH) {
				if (src->type == UFBX_ELEMENT_SKIN_DEFORMER) {
					ufbxi_pre_mesh *pre_mesh = &pre_meshes[dst->typed_id];
					pre_mesh->has_skin_deformer = true;
				}
			}
		} else if (tmp->src_prop.length == 0 && tmp->dst_prop.length != 0) {
			const char *dst_prop = tmp->dst_prop.data;
			if (dst->type == UFBX_ELEMENT_ANIM_VALUE && src->type == UFBX_ELEMENT_ANIM_CURVE) {
				ufbx_anim_curve *src_curve = (ufbx_anim_curve*)src;
				uint32_t index = 0;
				if (dst_prop == ufbxi_Y || dst_prop == ufbxi_d_Y) {
					index = 1;
				} else if (dst_prop == ufbxi_Z || dst_prop == ufbxi_d_Z) {
					index = 2;
				}

				ufbxi_pre_anim_value *pre_value = &pre_anim_values[dst->typed_id];
				if (src_curve->max_value - src_curve->min_value >= scale_epsilon) {
					pre_value->has_constant_value = false;
				} else {
					ufbx_real constant_value = (src_curve->min_value + src_curve->max_value) * 0.5f;
					if (ufbx_isnan(pre_value->constant_value.v[index])) {
						pre_value->constant_value.v[index] = constant_value;
					}
					if ((ufbx_real)ufbx_fabs(pre_value->constant_value.v[index] - constant_value) > scale_epsilon) {
						pre_value->has_constant_value = false;
					}
				}
			}
		}
	}

	for (size_t i = 0; i < num_connections; i++) {
		ufbxi_tmp_connection *tmp = &tmp_connections[i];
		ufbxi_pre_connection *pre = &pre_connections[i];
		ufbx_element *src = pre->src, *dst = pre->dst;
		if (!src || !dst) continue;

		if (tmp->src_prop.length == 0 && tmp->dst_prop.length == 0) {
			// Count maximum number of instanced attributes in a node
			if (dst->type == UFBX_ELEMENT_NODE) {
				if (src->type >= UFBX_ELEMENT_TYPE_FIRST_ATTRIB && src->type <= UFBX_ELEMENT_TYPE_LAST_ATTRIB) {
					instance_counts[dst->element_id] = ufbxi_max32(instance_counts[dst->element_id], instance_counts[src->element_id]);
					if (src->type == UFBX_ELEMENT_MESH) {
						ufbxi_pre_mesh *pre_mesh = &pre_meshes[src->typed_id];
						if (pre_mesh->has_skin_deformer) {
							pre_nodes[dst->typed_id].has_skin_deformer = true;
						}
					}
				} else if (src->type == UFBX_ELEMENT_SKIN_DEFORMER) {
					pre_nodes[dst->typed_id].has_skin_deformer = true;
				}
			}
		} else if (tmp->src_prop.length == 0 && tmp->dst_prop.length != 0) {
			if (dst->type == UFBX_ELEMENT_NODE) {
				if (src->type == UFBX_ELEMENT_ANIM_VALUE) {
					if (tmp->dst_prop.data == ufbxi_Lcl_Scaling) {
						ufbxi_pre_node *pre_node = &pre_nodes[dst->typed_id];
						if (pre_node->has_constant_scale) {
							ufbxi_pre_anim_value *pre_value = &pre_anim_values[src->typed_id];
							if (!pre_value->has_constant_value) {
								pre_node->has_constant_scale = false;
							} else {
								ufbx_real error = 0.0f;
								error += (ufbx_real)ufbx_fabs(pre_value->constant_value.x - pre_node->constant_scale.x);
								error += (ufbx_real)ufbx_fabs(pre_value->constant_value.y - pre_node->constant_scale.y);
								error += (ufbx_real)ufbx_fabs(pre_value->constant_value.z - pre_node->constant_scale.z);
								if (error >= scale_epsilon) {
									pre_node->has_constant_scale = false;
								}
							}
						}
					}
				}
			}
		}
	}

	if (uc->opts.pivot_handling == UFBX_PIVOT_HANDLING_ADJUST_TO_PIVOT) {
		for (size_t i = 0; i < num_nodes; i++) {
			ufbxi_pre_node *pre_node = &pre_nodes[i];
			ufbx_node *node = (ufbx_node*)elements[pre_node->element_id];
			ufbx_vec3 rotation_pivot = ufbxi_find_vec3(&node->props, ufbxi_RotationPivot, 0.0f, 0.0f, 0.0f);
			ufbx_vec3 scaling_pivot = ufbxi_find_vec3(&node->props, ufbxi_ScalingPivot, 0.0f, 0.0f, 0.0f);
			if (!ufbxi_is_vec3_zero(rotation_pivot)) {
				ufbx_real err = 0.0f;
				err += (ufbx_real)ufbx_fabs(rotation_pivot.x - scaling_pivot.x);
				err += (ufbx_real)ufbx_fabs(rotation_pivot.y - scaling_pivot.y);
				err += (ufbx_real)ufbx_fabs(rotation_pivot.z - scaling_pivot.z);

				bool can_modify_geometry_transform = true;
				if (uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY_NO_FALLBACK) {
					if (instance_counts[node->element_id] > 1 || modify_not_supported[node->element_id]) {
						can_modify_geometry_transform = false;
					}
				}
				// Currently, geometry transform messes up skinning
				if (pre_node->has_skin_deformer) {
					can_modify_geometry_transform = false;
				}

				if (err <= pivot_epsilon && can_modify_geometry_transform) {
					size_t num_props = node->props.props.count;
					ufbx_prop *new_props = ufbxi_push_zero(&uc->result, ufbx_prop, num_props + 3);
					ufbxi_check(new_props);
					memcpy(new_props, node->props.props.data, num_props * sizeof(ufbx_prop));

					ufbx_vec3 geometric_translation = ufbxi_find_vec3(&node->props, ufbxi_GeometricTranslation, 0.0f, 0.0f, 0.0f);
					geometric_translation.x -= rotation_pivot.x;
					geometric_translation.y -= rotation_pivot.y;
					geometric_translation.z -= rotation_pivot.z;

					ufbx_prop *dst = new_props + num_props;
					ufbxi_init_synthetic_vec3_prop(&dst[0], ufbxi_RotationPivot, &ufbx_zero_vec3, UFBX_PROP_VECTOR);
					ufbxi_init_synthetic_vec3_prop(&dst[1], ufbxi_ScalingPivot, &ufbx_zero_vec3, UFBX_PROP_VECTOR);
					ufbxi_init_synthetic_vec3_prop(&dst[2], ufbxi_GeometricTranslation, &geometric_translation, UFBX_PROP_VECTOR);

					node->props.props.data = new_props;
					node->props.props.count = num_props + 3;
					ufbxi_check(ufbxi_sort_properties(uc, node->props.props.data, node->props.props.count));
					ufbxi_deduplicate_properties(&node->props.props);

					node->adjust_pre_translation = ufbxi_add3(node->adjust_pre_translation, rotation_pivot);
					node->has_adjust_transform = true;
					uint32_t ix = pre_node->first_child;
					while (ix != ~0u) {
						ufbxi_pre_node *pre_child = &pre_nodes[ix];
						ufbx_node *child = (ufbx_node*)elements[pre_child->element_id];

						child->adjust_pre_translation = ufbxi_sub3(child->adjust_pre_translation, rotation_pivot);
						child->has_adjust_transform = true;

						ix = pre_child->next_child;
					}
				}
			}
		}
	}

	for (size_t i = 0; i < num_elements; i++) {
		ufbx_element *element = elements[i];
		uint64_t fbx_id = fbx_ids[i];

		if (element->type == UFBX_ELEMENT_NODE) {
			ufbx_node *node = (ufbx_node*)element;
			bool requires_helper_node = false;
			if (uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_HELPER_NODES) {
				requires_helper_node = true;
			} else if (uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY) {
				// Setup a geometry transform helper for nodes that have instanced attributes
				requires_helper_node = instance_counts[i] > 1 || modify_not_supported[i];
			}
			if (requires_helper_node) {
				ufbxi_check(ufbxi_setup_geometry_transform_helper(uc, node, fbx_id));
			}
		}
	}

	for (size_t i = 0; i < num_elements; i++) {
		ufbx_element *element = elements[i];
		uint64_t fbx_id = fbx_ids[i];

		if (element->type == UFBX_ELEMENT_NODE) {
			ufbx_node *node = (ufbx_node*)element;
			if (has_unscaled_children[node->typed_id] && !node->scale_helper) {
				ufbxi_pre_node *pre_node = &pre_nodes[node->typed_id];
				ufbx_real ref = uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE
					? pre_node->constant_scale.x : (ufbx_real)1.0f;
				ufbx_vec3 scale = pre_node->constant_scale;
				ufbx_real dx = (ufbx_real)ufbx_fabs(scale.x - ref);
				ufbx_real dy = (ufbx_real)ufbx_fabs(scale.y - ref);
				ufbx_real dz = (ufbx_real)ufbx_fabs(scale.z - ref);
				if ((dx + dy + dz >= scale_epsilon || !pre_node->has_constant_scale || (ufbx_real)ufbx_fabs(scale.x) <= compensate_epsilon)
						&& uc->opts.inherit_mode_handling != UFBX_INHERIT_MODE_HANDLING_COMPENSATE_NO_FALLBACK) {
					ufbxi_check(ufbxi_setup_scale_helper(uc, node, fbx_id));

					// If we added a geometry transform helper that may scale further helpers
					// recursively for all child nodes using `UFBX_INHERIT_MODE_COMPONENTWISE_SCALE`
					// This is guaranteed to terminate as `ufbxi_pre_node` may only have one parent,
					// meaning any cycles must contain `node` itself.
					uint32_t ix = pre_node->first_child;
					while (ix != ~0u && ix != node->typed_id) {
						ufbxi_pre_node *pre_child = &pre_nodes[ix];
						ufbx_node *child = (ufbx_node*)elements[pre_child->element_id];

						if (pre_child->parent != node->typed_id || child->original_inherit_mode == UFBX_INHERIT_MODE_COMPONENTWISE_SCALE) {
							if (!pre_child->has_recursive_scale_helper && child->original_inherit_mode != UFBX_INHERIT_MODE_NORMAL) {
								pre_child->has_recursive_scale_helper = true;

								uint64_t child_fbx_id = fbx_ids[pre_child->element_id];
								ufbxi_check(ufbxi_setup_scale_helper(uc, child, child_fbx_id));
								child->is_scale_compensate_parent = false;

								// Traverse to children if any
								if (pre_child->first_child != ~0u) {
									ix = pre_child->first_child;
									continue;
								}
							}
						}

						// Move to next child, popping parents until we find one
						while (pre_child->next_child == ~0u) {
							ix = pre_child->parent;
							if (ix == node->typed_id) break;
							pre_child = &pre_nodes[ix];
						}
						if (ix != node->typed_id) {
							ix = pre_child->next_child;
						}
					}

				} else if (uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE || uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE_NO_FALLBACK) {
					if ((ufbx_real)ufbx_fabs(scale.x - 1.0f) >= scale_epsilon) {
						node->is_scale_compensate_parent = true;
					}
				}
			}
		}
	}

	return 1;
}

// -- Scene processing

static ufbxi_noinline ufbx_element *ufbxi_find_element_by_fbx_id(ufbxi_context *uc, uint64_t fbx_id)
{
	ufbxi_fbx_id_entry *entry = ufbxi_find_fbx_id(uc, fbx_id);
	if (entry) {
		return uc->scene.elements.data[entry->element_id];
	}
	return NULL;
}

ufbxi_forceinline static bool ufbxi_cmp_name_element_less(const ufbx_name_element *a, const ufbx_name_element *b)
{
	if (a->_internal_key != b->_internal_key) return a->_internal_key < b->_internal_key;
	int cmp = strcmp(a->name.data, b->name.data);
	if (cmp != 0) return cmp < 0;
	return a->type < b->type;
}

ufbxi_forceinline static bool ufbxi_cmp_name_element_less_ref(const ufbx_name_element *a, ufbx_string name, ufbx_element_type type, uint32_t key)
{
	if (a->_internal_key != key) return a->_internal_key < key;
	int cmp = ufbxi_str_cmp(a->name, name);
	if (cmp != 0) return cmp < 0;
	return a->type < type;
}

ufbxi_forceinline static bool ufbxi_cmp_prop_less_ref(const ufbx_prop *a, ufbx_string name, uint32_t key)
{
	if (a->_internal_key != key) return a->_internal_key < key;
	return ufbxi_str_less(a->name, name);
}

ufbxi_forceinline static bool ufbxi_cmp_prop_less_concat(const ufbx_prop *a, const ufbx_string *parts, size_t num_parts, uint32_t key)
{
	if (a->_internal_key != key) return a->_internal_key < key;
	return ufbxi_concat_str_cmp(&a->name, parts, num_parts) < 0;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_name_elements(ufbxi_context *uc, ufbx_name_element *name_elems, size_t count)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_name_element)));
	ufbxi_macro_stable_sort(ufbx_name_element, 32, name_elems, uc->tmp_arr, count,
		( ufbxi_cmp_name_element_less(a, b) ) );
	return 1;
}

ufbxi_noinline static bool ufbxi_cmp_node_less(ufbx_node *a, ufbx_node *b)
{
	if (a->node_depth != b->node_depth) return a->node_depth < b->node_depth;
	if (a->parent && b->parent) {
		uint32_t a_pid = a->parent->element.element_id, b_pid = b->parent->element.element_id;
		if (a_pid != b_pid) return a_pid < b_pid;
	} else {
		ufbx_assert(a->parent == NULL && b->parent == NULL);
	}
	if (a->is_geometry_transform_helper != b->is_geometry_transform_helper) {
		// Sort geometry transform helpers always before rest of the children.
		return (unsigned)a->is_geometry_transform_helper > (unsigned)b->is_geometry_transform_helper;
	}
	if (a->is_scale_helper != b->is_scale_helper) {
		// Sort scale helpers after geometry transform helpers.
		return (unsigned)a->is_scale_helper > (unsigned)b->is_scale_helper;
	}
	return a->element.element_id < b->element.element_id;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_node_ptrs(ufbxi_context *uc, ufbx_node **nodes, size_t count)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_node*)));
	ufbxi_macro_stable_sort(ufbx_node*, 32, nodes, uc->tmp_arr, count,
		( ufbxi_cmp_node_less(*a, *b) ) );
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_cmp_tmp_material_texture_less(const ufbxi_tmp_material_texture *a, const ufbxi_tmp_material_texture *b)
{
	if (a->material_id != b->material_id) return a->material_id < b->material_id;
	if (a->texture_id != b->texture_id) return a->texture_id < b->texture_id;
	return ufbxi_str_less(a->prop_name, b->prop_name);
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_tmp_material_textures(ufbxi_context *uc, ufbxi_tmp_material_texture *mat_texs, size_t count)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbxi_tmp_material_texture)));
	ufbxi_macro_stable_sort(ufbxi_tmp_material_texture, 32, mat_texs, uc->tmp_arr, count,
		( ufbxi_cmp_tmp_material_texture_less(a, b) ));
	return 1;
}

// We need to be able to assume no padding!
ufbx_static_assert(connection_size, sizeof(ufbx_connection) == sizeof(ufbx_element*)*2 + sizeof(ufbx_string)*2);

ufbxi_forceinline static bool ufbxi_cmp_connection_less(ufbx_connection *a, ufbx_connection *b, size_t index)
{
	ufbx_element *a_elem = (&a->src)[index], *b_elem = (&b->src)[index];
	if (a_elem != b_elem) return a_elem < b_elem;
	int cmp = strcmp((&a->src_prop)[index].data, (&b->src_prop)[index].data);
	if (cmp != 0) return cmp < 0;
	cmp = strcmp((&a->src_prop)[index ^ 1].data, (&b->src_prop)[index ^ 1].data);
	return cmp < 0;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_connections(ufbxi_context *uc, ufbx_connection *connections, size_t count, size_t index)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_connection)));
	ufbxi_macro_stable_sort(ufbx_connection, 32, connections, uc->tmp_arr, count, ( ufbxi_cmp_connection_less(a, b, index) ));
	return 1;
}

static uint64_t ufbxi_find_attribute_fbx_id(ufbxi_context *uc, uint64_t node_fbx_id)
{
	uint32_t hash = ufbxi_hash64(node_fbx_id);
	ufbxi_fbx_attr_entry *entry = ufbxi_map_find(&uc->fbx_attr_map, ufbxi_fbx_attr_entry, hash, &node_fbx_id);
	if (entry) {
		return entry->attr_fbx_id;
	}
	return node_fbx_id;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_connections(ufbxi_context *uc)
{
	size_t num_connections = uc->tmp_connections.num_items;
	ufbxi_tmp_connection *tmp_connections = ufbxi_push_pop(&uc->tmp, &uc->tmp_connections, ufbxi_tmp_connection, num_connections);
	ufbxi_buf_free(&uc->tmp_connections);
	ufbxi_check(tmp_connections);

	// NOTE: We truncate this array in case not all connections are resolved
	uc->scene.connections_src.data = ufbxi_push(&uc->result, ufbx_connection, num_connections);
	ufbxi_check(uc->scene.connections_src.data);

	// HACK: Translate property connections from node to attribute if
	// the property name is not included in the known node properties.
	if (uc->version > 0 && uc->version < 7000) {
		ufbxi_for(ufbxi_tmp_connection, tmp_conn, tmp_connections, num_connections) {
			if (tmp_conn->src_prop.length > 0 && !ufbxi_is_node_property(uc, tmp_conn->src_prop.data)) {
				tmp_conn->src = ufbxi_find_attribute_fbx_id(uc, tmp_conn->src);
			}
			if (tmp_conn->dst_prop.length > 0 && !ufbxi_is_node_property(uc, tmp_conn->dst_prop.data)) {
				tmp_conn->dst = ufbxi_find_attribute_fbx_id(uc, tmp_conn->dst);
			}
		}
	}

	ufbxi_for(ufbxi_tmp_connection, tmp_conn, tmp_connections, num_connections) {
		ufbx_element *src = ufbxi_find_element_by_fbx_id(uc, tmp_conn->src);
		ufbx_element *dst = ufbxi_find_element_by_fbx_id(uc, tmp_conn->dst);
		if (!src || !dst) continue;

		if (!uc->opts.disable_quirks) {
			// Some exporters connect arbitrary non-nodes to root breaking further code, ignore those connections here!
			if (dst->type == UFBX_ELEMENT_NODE && src->type != UFBX_ELEMENT_NODE && ((ufbx_node*)dst)->is_root) {
				ufbxi_check(ufbxi_warnf_tag(UFBX_WARNING_BAD_ELEMENT_CONNECTED_TO_ROOT, src->element_id, "Non-node element connected to root"));
				continue;
			}
		}

		// Remap connections to geometry transform helpers if necessary, see `ufbxi_setup_geometry_transform_helper()` for how these are setup.
		if (uc->has_geometry_transform_nodes) {
			if (dst->type == UFBX_ELEMENT_NODE && src->type >= UFBX_ELEMENT_TYPE_FIRST_ATTRIB && src->type <= UFBX_ELEMENT_TYPE_LAST_ATTRIB) {
				ufbx_node *node = (ufbx_node*)dst;
				if (node->has_geometry_transform) {
					ufbxi_node_extra *extra = (ufbxi_node_extra*)ufbxi_get_element_extra(uc, node->element_id);
					ufbx_assert(extra);
					dst = uc->scene.elements.data[extra->geometry_helper_id];
					ufbx_assert(dst->type == UFBX_ELEMENT_NODE && ((ufbx_node*)dst)->is_geometry_transform_helper);
				}
			}
		}

		// Remap connections to scale helpers if necessary, see `ufbxi_setup_scale_helper()` for how these are setup.
		if (uc->has_scale_helper_nodes) {
			if (dst->type == UFBX_ELEMENT_NODE) {
				ufbx_node *dst_node = (ufbx_node*)dst;
				if (dst_node->scale_helper) {
					if (src->type == UFBX_ELEMENT_NODE) {
						ufbx_node *src_node = (ufbx_node*)src;
						if (!src_node->is_scale_helper && src_node->original_inherit_mode == UFBX_INHERIT_MODE_NORMAL) {
							dst = &dst_node->scale_helper->element;
						}
					} else if (src->type == UFBX_ELEMENT_ANIM_VALUE) {
						if (tmp_conn->dst_prop.data == ufbxi_Lcl_Scaling) {
							dst = &dst_node->scale_helper->element;
						}
					} else {
						dst = &dst_node->scale_helper->element;
					}
				}
			} else if (src->type == UFBX_ELEMENT_NODE) {
				ufbx_node *src_node = (ufbx_node*)src;
				if (src_node->scale_helper) {
					if (dst->type == UFBX_ELEMENT_SKIN_CLUSTER) {
						src = &src_node->scale_helper->element;
					}
				}
			}
		}

		// Translate deformers to point to the geometry in 6100, we don't need to worry about
		// blend shapes here as they're always connected synthetically in older files.
		if (uc->version > 0 && uc->version < 7000 && dst->type == UFBX_ELEMENT_NODE) {
			if (src->type == UFBX_ELEMENT_SKIN_DEFORMER || src->type == UFBX_ELEMENT_CACHE_DEFORMER) {
				uint64_t dst_id = ufbxi_find_attribute_fbx_id(uc, tmp_conn->dst);
				ufbx_element *dst_elem = ufbxi_find_element_by_fbx_id(uc, dst_id);
				if (dst_elem) {
					dst = dst_elem;
				}
			}
		}

		ufbx_connection *conn = &uc->scene.connections_src.data[uc->scene.connections_src.count++];
		conn->src = src;
		conn->dst = dst;
		conn->src_prop = tmp_conn->src_prop;
		conn->dst_prop = tmp_conn->dst_prop;
	}

	uc->scene.connections_dst.count = uc->scene.connections_src.count;
	uc->scene.connections_dst.data = ufbxi_push_copy(&uc->result, ufbx_connection,
		uc->scene.connections_src.count, uc->scene.connections_src.data);
	ufbxi_check(uc->scene.connections_dst.data);

	ufbxi_check(ufbxi_sort_connections(uc, uc->scene.connections_src.data, uc->scene.connections_src.count, 0));
	ufbxi_check(ufbxi_sort_connections(uc, uc->scene.connections_dst.data, uc->scene.connections_dst.count, 1));

	// We don't need the temporary connections at this point anymore
	ufbxi_buf_free(&uc->tmp_connections);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_add_connections_to_elements(ufbxi_context *uc)
{
	ufbx_connection *conn_src = uc->scene.connections_src.data;
	ufbx_connection *conn_src_end = ufbxi_add_ptr(conn_src, uc->scene.connections_src.count);
	ufbx_connection *conn_dst = uc->scene.connections_dst.data;
	ufbx_connection *conn_dst_end = ufbxi_add_ptr(conn_dst, uc->scene.connections_dst.count);

	ufbxi_for_ptr(ufbx_element, p_elem, uc->scene.elements.data, uc->scene.elements.count) {
		ufbx_element *elem = *p_elem;
		uint32_t id = elem->element_id;

		while (conn_src < conn_src_end && conn_src->src->element_id < id) conn_src++;
		while (conn_dst < conn_dst_end && conn_dst->dst->element_id < id) conn_dst++;
		ufbx_connection *src_end = conn_src, *dst_end = conn_dst;

		while (src_end < conn_src_end && src_end->src->element_id == id) src_end++;
		while (dst_end < conn_dst_end && dst_end->dst->element_id == id) dst_end++;

		elem->connections_src.data = conn_src;
		elem->connections_src.count = ufbxi_to_size(src_end - conn_src);
		elem->connections_dst.data = conn_dst;
		elem->connections_dst.count = ufbxi_to_size(dst_end - conn_dst);

		// Setup animated properties
		// TODO: It seems we're invalidating a lot of properties here actually, maybe they
		// should be initially pushed to `tmp` instead of result if this happens so much..
		{
			ufbx_prop *prop = elem->props.props.data, *prop_end = ufbxi_add_ptr(prop, elem->props.props.count);
			ufbx_prop *copy_start = prop;
			bool needs_copy = false;
			size_t num_animated = 0, num_synthetic = 0;

			for (;;) {
				// Scan to the next animation connection
				for (; conn_dst < dst_end; conn_dst++) {
					if (conn_dst->dst_prop.length == 0) continue;
					if (conn_dst->src_prop.length > 0) break;
					if (conn_dst->src->type == UFBX_ELEMENT_ANIM_VALUE) break;
				}

				ufbx_string name = ufbx_empty_string;
				if (conn_dst < dst_end) {
					name = conn_dst->dst_prop;
				}
				if (name.length == 0) break;

				// NOTE: "Animated" properties also include connected ones as we need
				// to resolve them during evaluation
				num_animated++;

				ufbx_anim_value *anim_value = NULL;
				uint32_t flags = 0;
				for (; conn_dst < dst_end && conn_dst->dst_prop.data == name.data; conn_dst++) {
					if (conn_dst->src_prop.length > 0) {
						flags |= UFBX_PROP_FLAG_CONNECTED;
					} else if (conn_dst->src->type == UFBX_ELEMENT_ANIM_VALUE) {
						anim_value = (ufbx_anim_value*)conn_dst->src;
						flags |= UFBX_PROP_FLAG_ANIMATED;
					}
				}

				uint32_t key = ufbxi_get_name_key(name.data, name.length);
				while (prop != prop_end && ufbxi_name_key_less(prop, name.data, name.length, key)) prop++;

				if (prop != prop_end && prop->name.data == name.data) {
					prop->flags = (ufbx_prop_flags)((uint32_t)prop->flags | flags);
				} else {
					// Animated property that is not in the element property list
					// Copy the preceding properties to the stack, then push a
					// synthetic property for the animated property.
					ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_prop, ufbxi_to_size(prop - copy_start), copy_start));
					copy_start = prop;
					needs_copy = true;

					// Let's hope we can find the property in the defaults at least
					ufbx_prop anim_def_prop;
					ufbx_prop *def_prop = NULL;
					if (elem->props.defaults) {
						def_prop = ufbxi_find_prop_with_key(elem->props.defaults, name.data, key);
					} else if (anim_value) {
						memset(&anim_def_prop, 0, sizeof(anim_def_prop));
						// Hack a couple of common types
						ufbx_prop_type type = UFBX_PROP_UNKNOWN;
						if (name.data == ufbxi_Lcl_Translation) type = UFBX_PROP_TRANSLATION;
						else if (name.data == ufbxi_Lcl_Rotation) type = UFBX_PROP_ROTATION;
						else if (name.data == ufbxi_Lcl_Scaling) {
							type = UFBX_PROP_SCALING;
							anim_def_prop.value_vec3.x = 1.0f;
							anim_def_prop.value_vec3.y = 1.0f;
							anim_def_prop.value_vec3.z = 1.0f;
						}
						// Property values are only defined in anim_props on legacy files
						if (uc->version < 6000) {
							anim_def_prop.value_vec3 = anim_value->default_value;
						}
						anim_def_prop.type = type;
						def_prop = &anim_def_prop;
					} else {
						flags |= UFBX_PROP_FLAG_NO_VALUE;
					}

					ufbx_prop *new_prop = ufbxi_push_zero(&uc->tmp_stack, ufbx_prop, 1);
					ufbxi_check(new_prop);
					if (def_prop) *new_prop = *def_prop;
					flags |= (uint32_t)new_prop->flags;
					new_prop->flags = (ufbx_prop_flags)(UFBX_PROP_FLAG_ANIMATABLE | UFBX_PROP_FLAG_SYNTHETIC | flags);
					new_prop->name = name;
					new_prop->_internal_key = key;
					new_prop->value_str = ufbx_empty_string;
					new_prop->value_blob = ufbx_empty_blob;
					num_synthetic++;
				}
			}

			// Copy the properties if necessary
			if (needs_copy) {
				size_t num_new_props = elem->props.props.count + num_synthetic;
				ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_prop, ufbxi_to_size(prop_end - copy_start), copy_start));
				elem->props.props.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_prop, num_new_props);
				ufbxi_check(elem->props.props.data);
				elem->props.props.count = num_new_props;
			}
			elem->props.num_animated = num_animated;
		}

		conn_src = src_end;
		conn_dst = dst_end;
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_linearize_nodes(ufbxi_context *uc)
{
	size_t num_nodes = uc->tmp_node_ids.num_items;
	uint32_t *node_ids = ufbxi_push_pop(&uc->tmp, &uc->tmp_node_ids, uint32_t, num_nodes);
	ufbxi_buf_free(&uc->tmp_node_ids);
	ufbxi_check(node_ids);

	ufbx_node **node_ptrs = ufbxi_push(&uc->tmp_stack, ufbx_node*, num_nodes);
	ufbxi_check(node_ptrs);

	// Fetch the node pointers
	for (size_t i = 0; i < num_nodes; i++) {
		node_ptrs[i] = (ufbx_node*)uc->scene.elements.data[node_ids[i]];
		ufbx_assert(node_ptrs[i]->element.type == UFBX_ELEMENT_NODE);
	}

	uc->scene.root_node = node_ptrs[0];

	size_t *node_offsets = ufbxi_push_pop(&uc->tmp_stack, &uc->tmp_typed_element_offsets[UFBX_ELEMENT_NODE], size_t, num_nodes);
	ufbxi_check(node_offsets);

	// Hook up the parent nodes, we'll assume that there's no cycles at this point
	ufbxi_for_ptr(ufbx_node, p_node, node_ptrs, num_nodes) {
		ufbx_node *node = *p_node;

		// Pre-6000 files don't have any explicit root connections so they must always
		// be connected to the root..
		if (node->parent == NULL && !(uc->opts.allow_nodes_out_of_root && uc->version >= 6000)) {
			if (node != uc->scene.root_node) {
				node->parent = uc->scene.root_node;
			}
		}

		ufbxi_for_list(ufbx_connection, conn, node->element.connections_dst) {
			if (conn->src_prop.length > 0 || conn->dst_prop.length > 0) continue;
			if (conn->src->type != UFBX_ELEMENT_NODE) continue;
			((ufbx_node*)conn->src)->parent = node;
		}
	}

	// Count the parent depths and child amounts
	ufbxi_for_ptr(ufbx_node, p_node, node_ptrs, num_nodes) {
		ufbx_node *node = *p_node;
		uint32_t depth = 0;

		for (ufbx_node *p = node->parent; p; p = p->parent) {
			depth += p->node_depth + 1;
			if (p->node_depth > 0) break;
			ufbxi_check_msg(depth <= num_nodes, "Cyclic node hierarchy");
		}

		if (uc->opts.node_depth_limit > 0) {
			ufbxi_check_msg(depth <= uc->opts.node_depth_limit, "Node depth limit exceeded");
		}
		node->node_depth = depth;

		// Second pass to cache the depths to avoid O(n^2)
		for (ufbx_node *p = node->parent; p; p = p->parent) {
			if (--depth <= p->node_depth) break;
			p->node_depth = depth;
		}
	}

	ufbxi_check(ufbxi_sort_node_ptrs(uc, node_ptrs, num_nodes));

	for (uint32_t i = 0; i < num_nodes; i++) {
		size_t *p_offset = ufbxi_push(&uc->tmp_typed_element_offsets[UFBX_ELEMENT_NODE], size_t, 1);
		ufbxi_check(p_offset);
		ufbx_node *node = node_ptrs[i];

		uint32_t original_id = node->element.typed_id;
		node->element.typed_id = i;
		*p_offset = node_offsets[original_id];
	}

	// Pop the temporary arrays
	ufbxi_pop(&uc->tmp_stack, size_t, num_nodes, NULL);
	ufbxi_pop(&uc->tmp_stack, ufbx_node*, num_nodes, NULL);

	return 1;
}


ufbxi_nodiscard ufbxi_noinline static ufbx_connection_list ufbxi_find_dst_connections(ufbx_element *element, const char *prop)
{
	if (!prop) prop = ufbxi_empty_char;

	size_t begin = element->connections_dst.count, end = begin;

	ufbxi_macro_lower_bound_eq(ufbx_connection, 32, &begin,
		element->connections_dst.data, 0, element->connections_dst.count,
		(strcmp(a->dst_prop.data, prop) < 0),
		(a->dst_prop.data == prop && a->src_prop.length == 0));

	ufbxi_macro_upper_bound_eq(ufbx_connection, 32, &end,
		element->connections_dst.data, begin, element->connections_dst.count,
		(a->dst_prop.data == prop && a->src_prop.length == 0));

	ufbx_connection_list result = { element->connections_dst.data + begin, end - begin };
	return result;
}

ufbxi_nodiscard ufbxi_noinline static ufbx_connection_list ufbxi_find_src_connections(ufbx_element *element, const char *prop)
{
	if (!prop) prop = ufbxi_empty_char;

	size_t begin = element->connections_src.count, end = begin;

	ufbxi_macro_lower_bound_eq(ufbx_connection, 32, &begin,
		element->connections_src.data, 0, element->connections_src.count,
		(strcmp(a->src_prop.data, prop) < 0),
		(a->src_prop.data == prop && a->dst_prop.length == 0));

	ufbxi_macro_upper_bound_eq(ufbx_connection, 32, &end,
		element->connections_src.data, begin, element->connections_src.count,
		(a->src_prop.data == prop && a->dst_prop.length == 0));

	ufbx_connection_list result = { element->connections_src.data + begin, end - begin };
	return result;
}

ufbxi_nodiscard static ufbx_element *ufbxi_get_element_node(ufbx_element *element)
{
	if (!element) return NULL;
	if (element->type == UFBX_ELEMENT_NODE) {
		ufbx_node *node = (ufbx_node*)element;
		if (node->is_geometry_transform_helper) return (ufbx_element*)node->parent;
		return NULL;
	} else {
		return element->instances.count > 0 ? &element->instances.data[0]->element : NULL;
	}
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_dst_elements(ufbxi_context *uc, void *p_dst_list, ufbx_element *element, bool search_node, bool ignore_duplicates, const char *prop, ufbx_element_type src_type)
{
	size_t num_elements = 0;

	do {
		ufbx_connection_list conns = ufbxi_find_dst_connections(element, prop);
		ufbxi_for_list(ufbx_connection, conn, conns) {
			if (conn->src->type == src_type) {
				if (ignore_duplicates) {
					uint32_t element_id = conn->src->element_id;
					if (uc->tmp_element_flag[element_id]) {
						ufbxi_check(ufbxi_warnf_tag(UFBX_WARNING_DUPLICATE_CONNECTION, element_id, "Duplicate connection to %u", element->element_id));
						continue;
					}
					uc->tmp_element_flag[element_id] = 1;
				}
				ufbx_element **p_elem = ufbxi_push(&uc->tmp_stack, ufbx_element*, 1);
				ufbxi_check(p_elem);
				*p_elem = conn->src;
				num_elements++;
			}
		}
	} while (search_node && (element = ufbxi_get_element_node(element)) != NULL);

	ufbx_element_list *list = (ufbx_element_list*)p_dst_list;
	list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_element*, num_elements);
	list->count = num_elements;
	ufbxi_check(list->data);

	if (ignore_duplicates) {
		ufbxi_for_ptr_list(ufbx_element, p_elem, *list) {
			uc->tmp_element_flag[(*p_elem)->element_id] = 0;
		}
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_src_elements(ufbxi_context *uc, void *p_dst_list, ufbx_element *element, bool search_node, bool ignore_duplicates, const char *prop, ufbx_element_type dst_type)
{
	size_t num_elements = 0;

	do {
		ufbx_connection_list conns = ufbxi_find_src_connections(element, prop);
		ufbxi_for_list(ufbx_connection, conn, conns) {
			if (conn->dst->type == dst_type) {
				if (ignore_duplicates) {
					uint32_t element_id = conn->dst->element_id;
					if (uc->tmp_element_flag[element_id]) {
						ufbxi_check(ufbxi_warnf_tag(UFBX_WARNING_DUPLICATE_CONNECTION, element_id, "Duplicate connection to %u", element->element_id));
						continue;
					}
					uc->tmp_element_flag[element_id] = 1;
				}
				ufbx_element **p_elem = ufbxi_push(&uc->tmp_stack, ufbx_element*, 1);
				ufbxi_check(p_elem);
				*p_elem = conn->dst;
				num_elements++;
			}
		}
	} while (search_node && (element = ufbxi_get_element_node(element)) != NULL);

	ufbx_element_list *list = (ufbx_element_list*)p_dst_list;
	list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_element*, num_elements);
	list->count = num_elements;
	ufbxi_check(list->data);

	if (ignore_duplicates) {
		ufbxi_for_ptr_list(ufbx_element, p_elem, *list) {
			uc->tmp_element_flag[(*p_elem)->element_id] = 0;
		}
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static ufbx_element *ufbxi_fetch_dst_element(ufbx_element *element, bool search_node, const char *prop, ufbx_element_type src_type)
{
	do {
		ufbx_connection_list conns = ufbxi_find_dst_connections(element, prop);
		ufbxi_for_list(ufbx_connection, conn, conns) {
			if (conn->src->type == src_type) {
				return conn->src;
			}
		}
	} while (search_node && (element = ufbxi_get_element_node(element)) != NULL);

	return NULL;
}

ufbxi_nodiscard ufbxi_noinline static ufbx_element *ufbxi_fetch_src_element(ufbx_element *element, bool search_node, const char *prop, ufbx_element_type dst_type)
{
	do {
		ufbx_connection_list conns = ufbxi_find_src_connections(element, prop);
		ufbxi_for_list(ufbx_connection, conn, conns) {
			if (conn->dst->type == dst_type) {
				return conn->dst;
			}
		}
	} while (search_node && (element = ufbxi_get_element_node(element)) != NULL);

	return NULL;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_textures(ufbxi_context *uc, ufbx_material_texture_list *list, ufbx_element *element, bool search_node)
{
	size_t num_textures = 0;

	do {
		ufbxi_for_list(ufbx_connection, conn, element->connections_dst) {
			if (conn->src_prop.length > 0) continue;
			if (conn->src->type == UFBX_ELEMENT_TEXTURE) {
				ufbx_material_texture *tex = ufbxi_push(&uc->tmp_stack, ufbx_material_texture, 1);
				ufbxi_check(tex);
				tex->shader_prop = tex->material_prop = conn->dst_prop;
				tex->texture = (ufbx_texture*)conn->src;
				num_textures++;
			}
		}
	} while (search_node && (element = ufbxi_get_element_node(element)) != NULL);

	list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_material_texture, num_textures);
	list->count = num_textures;
	ufbxi_check(list->data);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_mesh_materials(ufbxi_context *uc, ufbx_material_list *list, ufbx_element *element, bool search_node)
{
	size_t num_materials = 0;

	do {
		ufbx_connection_list conns = ufbxi_find_dst_connections(element, NULL);
		ufbxi_for_list(ufbx_connection, conn, conns) {
			if (conn->src->type == UFBX_ELEMENT_MATERIAL) {
				ufbx_material *mat = (ufbx_material*)conn->src;
				ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_material*, 1, &mat));
				num_materials++;
			}
		}

		if (num_materials > 0) break;
	} while (search_node && (element = ufbxi_get_element_node(element)) != NULL);

	list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_material*, num_materials);
	list->count = num_materials;
	ufbxi_check(list->data);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_deformers(ufbxi_context *uc,  ufbx_element_list *list, ufbx_element *element, bool search_node)
{
	size_t num_deformers = 0;

	do {
		ufbxi_for_list(ufbx_connection, conn, element->connections_dst) {
			if (conn->src_prop.length > 0) continue;
			ufbx_element_type type = conn->src->type;
			if (type == UFBX_ELEMENT_SKIN_DEFORMER || type == UFBX_ELEMENT_BLEND_DEFORMER || type == UFBX_ELEMENT_CACHE_DEFORMER) {
				ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_element*, 1, &conn->src));
				num_deformers++;
			}
		}
	} while (search_node && (element = ufbxi_get_element_node(element)) != NULL);

	list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_element*, num_deformers);
	list->count = num_deformers;
	ufbxi_check(list->data);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_blend_keyframes(ufbxi_context *uc, ufbx_blend_keyframe_list *list, ufbx_element *element)
{
	size_t num_keyframes = 0;

	ufbx_connection_list conns = ufbxi_find_dst_connections(element, NULL);
	ufbxi_for_list(ufbx_connection, conn, conns) {
		if (conn->src->type == UFBX_ELEMENT_BLEND_SHAPE) {
			ufbx_blend_keyframe key = { (ufbx_blend_shape*)conn->src };
			ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_blend_keyframe, 1, &key));
			num_keyframes++;
		}
	}

	list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_blend_keyframe, num_keyframes);
	list->count = num_keyframes;
	ufbxi_check(list->data);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_texture_layers(ufbxi_context *uc, ufbx_texture_layer_list *list, ufbx_element *element)
{
	size_t num_layers = 0;

	ufbx_connection_list conns = ufbxi_find_dst_connections(element, NULL);
	ufbxi_for_list(ufbx_connection, conn, conns) {
		if (conn->src->type == UFBX_ELEMENT_TEXTURE) {
			ufbx_texture *texture = (ufbx_texture*)conn->src;
			ufbx_texture_layer layer = { texture };
			layer.alpha = ufbxi_find_real(&texture->props, ufbxi_Texture_alpha, 1.0f);
			layer.blend_mode = (ufbx_blend_mode)ufbxi_find_enum(&texture->props, ufbxi_BlendMode, UFBX_BLEND_REPLACE, UFBX_BLEND_OVERLAY);
			ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_texture_layer, 1, &layer));
			num_layers++;
		}
	}

	list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_texture_layer, num_layers);
	list->count = num_layers;
	ufbxi_check(list->data);

	return 1;
}

static ufbxi_forceinline bool ufbxi_prop_connection_less(const ufbx_connection *a, const char *prop)
{
	int cmp = strcmp(a->dst_prop.data, prop);
	if (cmp != 0) return cmp < 0;
	return a->src_prop.length == 0;
}

ufbxi_nodiscard ufbxi_noinline static ufbx_connection *ufbxi_find_prop_connection(const ufbx_element *element, const char *prop)
{
	if (!prop) prop = ufbxi_empty_char;

	size_t index = SIZE_MAX;

	ufbxi_macro_lower_bound_eq(ufbx_connection, 32, &index,
		element->connections_dst.data, 0, element->connections_dst.count,
		(ufbxi_prop_connection_less(a, prop)),
		(a->dst_prop.data == prop && a->src_prop.length > 0));

	return index < SIZE_MAX ? &element->connections_dst.data[index] : NULL;
}

ufbxi_forceinline static void ufbxi_patch_index_pointer(ufbxi_context *uc, uint32_t **p_index)
{
	if (*p_index == ufbxi_sentinel_index_zero) {
		*p_index = uc->zero_indices;
	} else if (*p_index == ufbxi_sentinel_index_consecutive) {
		*p_index = uc->consecutive_indices;
	}
}

ufbxi_nodiscard static bool ufbxi_cmp_anim_prop_less(const ufbx_anim_prop *a, const ufbx_anim_prop *b)
{
	if (a->element != b->element) return a->element < b->element;
	if (a->_internal_key != b->_internal_key) return a->_internal_key < b->_internal_key;
	return ufbxi_str_less(a->prop_name, b->prop_name);
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_anim_props(ufbxi_context *uc, ufbx_anim_prop *aprops, size_t count)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_anim_prop)));
	ufbxi_macro_stable_sort(ufbx_anim_prop, 32, aprops, uc->tmp_arr, count, ( ufbxi_cmp_anim_prop_less(a, b) ));
	return 1;
}

ufbxi_noinline static bool ufbxi_material_texture_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_material_texture *a = (const ufbx_material_texture*)va, *b = (const ufbx_material_texture*)vb;
	return ufbxi_str_less(a->material_prop, b->material_prop);
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_material_textures(ufbxi_context *uc, ufbx_material_texture *textures, size_t count)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_material_texture)));
	ufbxi_stable_sort(sizeof(ufbx_material_texture), 32, textures, uc->tmp_arr, count, &ufbxi_material_texture_less, NULL);
	return 1;
}

static ufbxi_noinline bool ufbxi_bone_pose_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_bone_pose *a = (const ufbx_bone_pose *)va, *b = (const ufbx_bone_pose *)vb;
	return a->bone_node->typed_id < b->bone_node->typed_id;
}

ufbxi_nodiscard ufbxi_noinline static ufbx_anim_prop *ufbxi_find_anim_prop_start(ufbx_anim_layer *layer, const ufbx_element *element)
{
	size_t index = SIZE_MAX;
	ufbxi_macro_lower_bound_eq(ufbx_anim_prop, 16, &index, layer->anim_props.data, 0, layer->anim_props.count,
		(a->element < element), (a->element == element));
	return index != SIZE_MAX ? &layer->anim_props.data[index] : NULL;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_bone_poses(ufbxi_context *uc, ufbx_pose *pose)
{
	size_t count = pose->bone_poses.count;
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, pose->bone_poses.count * sizeof(ufbx_bone_pose)));
	ufbxi_stable_sort(sizeof(ufbx_bone_pose), 16, pose->bone_poses.data, uc->tmp_arr, count, &ufbxi_bone_pose_less, NULL);
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_skin_weights(ufbxi_context *uc, ufbx_skin_deformer *skin)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, skin->max_weights_per_vertex * sizeof(ufbx_skin_weight)));

	for (size_t i = 0; i < skin->vertices.count; i++) {
		ufbx_skin_vertex v = skin->vertices.data[i];
		ufbxi_macro_stable_sort(ufbx_skin_weight, 32, skin->weights.data + v.weight_begin, uc->tmp_arr, v.num_weights,
			( a->weight > b->weight ));
	}

	return 1;
}

ufbxi_noinline static bool ufbxi_blend_keyframe_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_blend_keyframe *a = (const ufbx_blend_keyframe*)va, *b = (const ufbx_blend_keyframe*)vb;
	return a->target_weight < b->target_weight;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_blend_keyframes(ufbxi_context *uc, ufbx_blend_keyframe *keyframes, size_t count)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_blend_keyframe)));
	ufbxi_stable_sort(sizeof(ufbx_blend_keyframe), 32, keyframes, uc->tmp_arr, count, &ufbxi_blend_keyframe_less, NULL);
	return 1;
}

// Material tables

typedef void (*ufbxi_mat_transform_fn)(ufbx_vec4 *a);

static void ufbxi_mat_transform_invert_x(ufbx_vec4 *v) { v->x = 1.0f - v->x; }
static void ufbxi_mat_transform_unknown_shininess(ufbx_vec4 *v) { if (v->x >= 0.0f) v->x = (ufbx_real)(1.0f - ufbx_sqrt(v->x) * (ufbx_real)0.1); if (!(v->x >= 0.0f)) v->x = 0.0f; }
static void ufbxi_mat_transform_blender_opacity(ufbx_vec4 *v) { v->x = 1.0f - v->x; }
static void ufbxi_mat_transform_blender_shininess(ufbx_vec4 *v) { if (v->x >= 0.0f) v->x = (ufbx_real)(1.0f - ufbx_sqrt(v->x) * (ufbx_real)0.1); if (!(v->x >= 0.0f)) v->x = 0.0f; }

typedef enum {
	UFBXI_MAT_TRANSFORM_IDENTITY,
	UFBXI_MAT_TRANSFORM_INVERT_X,
	UFBXI_MAT_TRANSFORM_UNKNOWN_SHININESS,
	UFBXI_MAT_TRANSFORM_BLENDER_OPACITY,
	UFBXI_MAT_TRANSFORM_BLENDER_SHININESS,

	UFBXI_MAT_TRANSFORM_COUNT,
} ufbxi_mat_transform;

typedef enum {
	// Set `value_vec4.w` (usually alpha) to 1.0 if not defined by the property
	UFBXI_SHADER_MAPPING_DEFAULT_W_1 = 0x1,
	// Widen values to RGB if only a single value is present.
	UFBXI_SHADER_MAPPING_WIDEN_TO_RGB = 0x2,
} ufbxi_shader_mapping_flag;

typedef enum {
	// Invert the feature flag
	UFBXI_SHADER_FEATURE_INVERTED = 0x1,
	// Enable the feature if the given property exists
	UFBXI_SHADER_FEATURE_IF_EXISTS = 0x2,
	// Enable the feature if the given property has a texture
	UFBXI_SHADER_FEATURE_IF_TEXTURE = 0x4,
	// Enable if the feature is in [0.5, 1.5], (ie. 2 won't enable this feature)
	UFBXI_SHADER_FEATURE_IF_AROUND_1 = 0x8,

	UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE = UFBXI_SHADER_FEATURE_IF_EXISTS|UFBXI_SHADER_FEATURE_IF_TEXTURE,
} ufbxi_shader_feature_flag;

static const ufbxi_mat_transform_fn ufbxi_mat_transform_fns[] = {
	NULL,
	&ufbxi_mat_transform_invert_x,
	&ufbxi_mat_transform_unknown_shininess,
	&ufbxi_mat_transform_blender_opacity,
	&ufbxi_mat_transform_blender_shininess,
};

ufbx_static_assert(transform_count, ufbxi_arraycount(ufbxi_mat_transform_fns) == UFBXI_MAT_TRANSFORM_COUNT);

typedef struct {
	uint8_t index;     // < `ufbx_material_(fbx|pbr)_map`
	uint8_t flags;     // < Combination of `ufbxi_shader_mapping_flag`
	uint8_t transform; // < `ufbxi_mat_transform`
	uint8_t prop_len;  // < Length of `prop` not including NULL terminator
	const char *prop;  // < Name of FBX material property or shader mapping
} ufbxi_shader_mapping;

typedef struct {
	const ufbxi_shader_mapping *data;
	size_t count;
	const ufbxi_shader_mapping *features;
	size_t feature_count;
	uint32_t default_features;
	ufbx_string texture_prefix;
	ufbx_string texture_suffix;
	ufbx_string texture_enabled_prefix;
	ufbx_string texture_enabled_suffix;
} ufbxi_shader_mapping_list;

#define ufbxi_mat_string(str) sizeof(str) - 1, str

static const ufbxi_shader_mapping ufbxi_base_fbx_mapping[] = {
	{ UFBX_MATERIAL_FBX_DIFFUSE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Diffuse") },
	{ UFBX_MATERIAL_FBX_DIFFUSE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("DiffuseColor") },
	{ UFBX_MATERIAL_FBX_DIFFUSE_FACTOR, 0, 0, ufbxi_mat_string("DiffuseFactor") },
	{ UFBX_MATERIAL_FBX_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Specular") },
	{ UFBX_MATERIAL_FBX_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("SpecularColor") },
	{ UFBX_MATERIAL_FBX_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("SpecularFactor") },
	{ UFBX_MATERIAL_FBX_SPECULAR_EXPONENT, 0, 0, ufbxi_mat_string("Shininess") },
	{ UFBX_MATERIAL_FBX_SPECULAR_EXPONENT, 0, 0, ufbxi_mat_string("ShininessExponent") },
	{ UFBX_MATERIAL_FBX_REFLECTION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Reflection") },
	{ UFBX_MATERIAL_FBX_REFLECTION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("ReflectionColor") },
	{ UFBX_MATERIAL_FBX_REFLECTION_FACTOR, 0, 0, ufbxi_mat_string("ReflectionFactor") },
	{ UFBX_MATERIAL_FBX_TRANSPARENCY_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Transparent") },
	{ UFBX_MATERIAL_FBX_TRANSPARENCY_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("TransparentColor") },
	{ UFBX_MATERIAL_FBX_TRANSPARENCY_FACTOR, 0, 0, ufbxi_mat_string("TransparentFactor") },
	{ UFBX_MATERIAL_FBX_TRANSPARENCY_FACTOR, 0, 0, ufbxi_mat_string("TransparencyFactor") },
	{ UFBX_MATERIAL_FBX_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Emissive") },
	{ UFBX_MATERIAL_FBX_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("EmissiveColor") },
	{ UFBX_MATERIAL_FBX_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("EmissiveFactor") },
	{ UFBX_MATERIAL_FBX_AMBIENT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Ambient") },
	{ UFBX_MATERIAL_FBX_AMBIENT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("AmbientColor") },
	{ UFBX_MATERIAL_FBX_AMBIENT_FACTOR, 0, 0, ufbxi_mat_string("AmbientFactor") },
	{ UFBX_MATERIAL_FBX_NORMAL_MAP, 0, 0, ufbxi_mat_string("NormalMap") },
	{ UFBX_MATERIAL_FBX_BUMP, 0, 0, ufbxi_mat_string("Bump") },
	{ UFBX_MATERIAL_FBX_BUMP_FACTOR, 0, 0, ufbxi_mat_string("BumpFactor") },
	{ UFBX_MATERIAL_FBX_DISPLACEMENT, 0, 0, ufbxi_mat_string("Displacement") },
	{ UFBX_MATERIAL_FBX_DISPLACEMENT_FACTOR, 0, 0, ufbxi_mat_string("DisplacementFactor") },
	{ UFBX_MATERIAL_FBX_VECTOR_DISPLACEMENT, 0, 0, ufbxi_mat_string("VectorDisplacement") },
	{ UFBX_MATERIAL_FBX_VECTOR_DISPLACEMENT_FACTOR, 0, 0, ufbxi_mat_string("VectorDisplacementFactor") },
};

static const ufbxi_shader_mapping ufbxi_obj_fbx_mapping[] = {
	{ UFBX_MATERIAL_FBX_AMBIENT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Ka") },
	{ UFBX_MATERIAL_FBX_DIFFUSE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Kd") },
	{ UFBX_MATERIAL_FBX_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Ks") },
	{ UFBX_MATERIAL_FBX_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Ke") },
	{ UFBX_MATERIAL_FBX_SPECULAR_EXPONENT, 0, 0, ufbxi_mat_string("Ns") },
	{ UFBX_MATERIAL_FBX_TRANSPARENCY_FACTOR, 0, UFBXI_MAT_TRANSFORM_INVERT_X, ufbxi_mat_string("d") },
	{ UFBX_MATERIAL_FBX_NORMAL_MAP, 0, 0, ufbxi_mat_string("norm") },
	{ UFBX_MATERIAL_FBX_DISPLACEMENT, 0, 0, ufbxi_mat_string("disp") },
	{ UFBX_MATERIAL_FBX_BUMP, 0, 0, ufbxi_mat_string("bump") },
};

static const ufbxi_shader_mapping ufbxi_fbx_lambert_shader_pbr_mapping[] = {
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Diffuse") },
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("DiffuseColor") },
	{ UFBX_MATERIAL_PBR_BASE_FACTOR, 0, 0, ufbxi_mat_string("DiffuseFactor") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Transparent") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("TransparentColor") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("TransparentFactor") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("TransparencyFactor") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Emissive") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("EmissiveColor") },
	{ UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("EmissiveFactor") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("NormalMap") },
};

static const ufbxi_shader_mapping ufbxi_fbx_phong_shader_pbr_mapping[] = {
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Diffuse") },
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("DiffuseColor") },
	{ UFBX_MATERIAL_PBR_BASE_FACTOR, 0, 0, ufbxi_mat_string("DiffuseFactor") },
	{ UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Specular") },
	{ UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("SpecularColor") },
	{ UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("SpecularFactor") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, UFBXI_MAT_TRANSFORM_UNKNOWN_SHININESS, ufbxi_mat_string("Shininess") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, UFBXI_MAT_TRANSFORM_UNKNOWN_SHININESS, ufbxi_mat_string("ShininessExponent") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Transparent") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("TransparentColor") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("TransparentFactor") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("TransparencyFactor") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Emissive") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("EmissiveColor") },
	{ UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("EmissiveFactor") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("NormalMap") },
};

static const ufbxi_shader_mapping ufbxi_osl_standard_shader_pbr_mapping[] = {
	{ UFBX_MATERIAL_PBR_BASE_FACTOR, 0, 0, ufbxi_mat_string("base") },
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("base_color") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("specular_roughness") },
	{ UFBX_MATERIAL_PBR_DIFFUSE_ROUGHNESS, 0, 0, ufbxi_mat_string("diffuse_roughness") },
	{ UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("metalness") },
	{ UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("specular") },
	{ UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("specular_color") },
	{ UFBX_MATERIAL_PBR_SPECULAR_IOR, 0, 0, ufbxi_mat_string("specular_IOR") },
	{ UFBX_MATERIAL_PBR_SPECULAR_ANISOTROPY, 0, 0, ufbxi_mat_string("specular_anisotropy") },
	{ UFBX_MATERIAL_PBR_SPECULAR_ROTATION, 0, 0, ufbxi_mat_string("specular_rotation") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("transmission") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("transmission_color") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_DEPTH, 0, 0, ufbxi_mat_string("transmission_depth") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("transmission_scatter") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER_ANISOTROPY, 0, 0, ufbxi_mat_string("transmission_scatter_anisotropy") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_DISPERSION, 0, 0, ufbxi_mat_string("transmission_dispersion") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_EXTRA_ROUGHNESS, 0, 0, ufbxi_mat_string("transmission_extra_roughness") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_FACTOR, 0, 0, ufbxi_mat_string("subsurface") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("subsurface_color") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_RADIUS, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("subsurface_radius") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_SCALE, 0, 0, ufbxi_mat_string("subsurface_scale") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_ANISOTROPY, 0, 0, ufbxi_mat_string("subsurface_anisotropy") },
	{ UFBX_MATERIAL_PBR_SHEEN_FACTOR, 0, 0, ufbxi_mat_string("sheen") },
	{ UFBX_MATERIAL_PBR_SHEEN_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("sheen_color") },
	{ UFBX_MATERIAL_PBR_SHEEN_ROUGHNESS, 0, 0, ufbxi_mat_string("sheen_roughness") },
	{ UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("coat") },
	{ UFBX_MATERIAL_PBR_COAT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("coat_color") },
	{ UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("coat_roughness") },
	{ UFBX_MATERIAL_PBR_COAT_IOR, 0, 0, ufbxi_mat_string("coat_IOR") },
	{ UFBX_MATERIAL_PBR_COAT_ANISOTROPY, 0, 0, ufbxi_mat_string("coat_anisotropy") },
	{ UFBX_MATERIAL_PBR_COAT_ROTATION, 0, 0, ufbxi_mat_string("coat_rotation") },
	{ UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("coat_normal") },
	{ UFBX_MATERIAL_PBR_COAT_AFFECT_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("coat_affect_color") },
	{ UFBX_MATERIAL_PBR_COAT_AFFECT_BASE_ROUGHNESS, 0, 0, ufbxi_mat_string("coat_affect_roughness") },
	{ UFBX_MATERIAL_PBR_THIN_FILM_THICKNESS, 0, 0, ufbxi_mat_string("thin_film_thickness") },
	{ UFBX_MATERIAL_PBR_THIN_FILM_IOR, 0, 0, ufbxi_mat_string("thin_film_IOR") },
	{ UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("emission") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emission_color") },
	{ UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("opacity") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("NormalMap") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("normalCamera") },
	{ UFBX_MATERIAL_PBR_TANGENT_MAP, 0, 0, ufbxi_mat_string("tangent") },
};

static const ufbxi_shader_mapping ufbxi_osl_standard_shader_features[] = {
	{ UFBX_MATERIAL_FEATURE_THIN_WALLED, 0, 0, ufbxi_mat_string("thin_walled") },
};

static const ufbxi_shader_mapping ufbxi_arnold_shader_pbr_mapping[] = {
	{ UFBX_MATERIAL_PBR_BASE_FACTOR, 0, 0, ufbxi_mat_string("base") },
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("baseColor") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("specularRoughness") },
	{ UFBX_MATERIAL_PBR_DIFFUSE_ROUGHNESS, 0, 0, ufbxi_mat_string("diffuseRoughness") },
	{ UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("metalness") },
	{ UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("specular") },
	{ UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("specularColor") },
	{ UFBX_MATERIAL_PBR_SPECULAR_IOR, 0, 0, ufbxi_mat_string("specularIOR") },
	{ UFBX_MATERIAL_PBR_SPECULAR_ANISOTROPY, 0, 0, ufbxi_mat_string("specularAnisotropy") },
	{ UFBX_MATERIAL_PBR_SPECULAR_ROTATION, 0, 0, ufbxi_mat_string("specularRotation") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("transmission") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("transmissionColor") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_DEPTH, 0, 0, ufbxi_mat_string("transmissionDepth") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("transmissionScatter") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER_ANISOTROPY, 0, 0, ufbxi_mat_string("transmissionScatterAnisotropy") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_DISPERSION, 0, 0, ufbxi_mat_string("transmissionDispersion") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_EXTRA_ROUGHNESS, 0, 0, ufbxi_mat_string("transmissionExtraRoughness") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_FACTOR, 0, 0, ufbxi_mat_string("subsurface") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("subsurfaceColor") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_RADIUS, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("subsurfaceRadius") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_SCALE, 0, 0, ufbxi_mat_string("subsurfaceScale") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_ANISOTROPY, 0, 0, ufbxi_mat_string("subsurfaceAnisotropy") },
	{ UFBX_MATERIAL_PBR_SHEEN_FACTOR, 0, 0, ufbxi_mat_string("sheen") },
	{ UFBX_MATERIAL_PBR_SHEEN_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("sheenColor") },
	{ UFBX_MATERIAL_PBR_SHEEN_ROUGHNESS, 0, 0, ufbxi_mat_string("sheenRoughness") },
	{ UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("coat") },
	{ UFBX_MATERIAL_PBR_COAT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("coatColor") },
	{ UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("coatRoughness") },
	{ UFBX_MATERIAL_PBR_COAT_IOR, 0, 0, ufbxi_mat_string("coatIOR") },
	{ UFBX_MATERIAL_PBR_COAT_ANISOTROPY, 0, 0, ufbxi_mat_string("coatAnisotropy") },
	{ UFBX_MATERIAL_PBR_COAT_ROTATION, 0, 0, ufbxi_mat_string("coatRotation") },
	{ UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("coatNormal") },
	{ UFBX_MATERIAL_PBR_THIN_FILM_THICKNESS, 0, 0, ufbxi_mat_string("thinFilmThickness") },
	{ UFBX_MATERIAL_PBR_THIN_FILM_IOR, 0, 0, ufbxi_mat_string("thinFilmIOR") },
	{ UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("emission") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emissionColor") },
	{ UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("opacity") },
	{ UFBX_MATERIAL_PBR_INDIRECT_DIFFUSE, 0, 0, ufbxi_mat_string("indirectDiffuse") },
	{ UFBX_MATERIAL_PBR_INDIRECT_SPECULAR, 0, 0, ufbxi_mat_string("indirectSpecular") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("NormalMap") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("normalCamera") },
	{ UFBX_MATERIAL_PBR_TANGENT_MAP, 0, 0, ufbxi_mat_string("tangent") },
	{ UFBX_MATERIAL_PBR_MATTE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("aiMatteColor") },
	{ UFBX_MATERIAL_PBR_MATTE_FACTOR, 0, 0, ufbxi_mat_string("aiMatteColorA") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_TYPE, 0, 0, ufbxi_mat_string("subsurfaceType") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_PRIORITY, 0, 0, ufbxi_mat_string("dielectricPriority") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_ENABLE_IN_AOV, 0, 0, ufbxi_mat_string("transmitAovs") },
};

static const ufbxi_shader_mapping ufbxi_arnold_shader_features[] = {
	{ UFBX_MATERIAL_FEATURE_MATTE, 0, 0, ufbxi_mat_string("aiEnableMatte") },
	{ UFBX_MATERIAL_FEATURE_THIN_WALLED, 0, 0, ufbxi_mat_string("thinWalled") },
	{ UFBX_MATERIAL_FEATURE_CAUSTICS, 0, 0, ufbxi_mat_string("caustics") },
	{ UFBX_MATERIAL_FEATURE_INTERNAL_REFLECTIONS, 0, 0, ufbxi_mat_string("internalReflections") },
	{ UFBX_MATERIAL_FEATURE_EXIT_TO_BACKGROUND, 0, 0, ufbxi_mat_string("exitToBackground") },
};

static const ufbxi_shader_mapping ufbxi_3ds_max_physical_material_pbr_mapping[] = {
	{ UFBX_MATERIAL_PBR_BASE_FACTOR, 0, 0, ufbxi_mat_string("base_weight") },
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("base_color") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("roughness") },
	{ UFBX_MATERIAL_PBR_DIFFUSE_ROUGHNESS, 0, 0, ufbxi_mat_string("diff_rough") },
	{ UFBX_MATERIAL_PBR_DIFFUSE_ROUGHNESS, 0, 0, ufbxi_mat_string("diff_roughness") },
	{ UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("metalness") },
	{ UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("reflectivity") },
	{ UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("refl_color") },
	{ UFBX_MATERIAL_PBR_SPECULAR_ANISOTROPY, 0, 0, ufbxi_mat_string("anisotropy") },
	{ UFBX_MATERIAL_PBR_SPECULAR_ROTATION, 0, 0, ufbxi_mat_string("aniso_angle") },
	{ UFBX_MATERIAL_PBR_SPECULAR_ROTATION, 0, 0, ufbxi_mat_string("anisoangle") },
	{ UFBX_MATERIAL_PBR_SPECULAR_IOR, 0, 0, ufbxi_mat_string("trans_ior") }, // NOTE: Not a typo, IOR is same for transparency/specular
	{ UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("transparency") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("trans_color") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_DEPTH, 0, 0, ufbxi_mat_string("trans_depth") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_ROUGHNESS, 0, 0, ufbxi_mat_string("trans_rough") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_ROUGHNESS, 0, 0, ufbxi_mat_string("trans_roughness") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_FACTOR, 0, 0, ufbxi_mat_string("scattering") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_TINT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("sss_color") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("sss_scatter_color") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_RADIUS, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("sss_depth") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_SCALE, 0, 0, ufbxi_mat_string("sss_scale") },
	{ UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("coat") },
	{ UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("coating") },
	{ UFBX_MATERIAL_PBR_COAT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("coat_color") },
	{ UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("coat_rough") },
	{ UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("coat_roughness") },
	{ UFBX_MATERIAL_PBR_COAT_IOR, 0, 0, ufbxi_mat_string("coat_ior") },
	{ UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("coat_bump") },
	{ UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("clearcoat_bump_map_amt") },
	{ UFBX_MATERIAL_PBR_COAT_AFFECT_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("coat_affect_color") },
	{ UFBX_MATERIAL_PBR_COAT_AFFECT_BASE_ROUGHNESS, 0, 0, ufbxi_mat_string("coat_affect_roughness") },
	{ UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("emission") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emit_color") },
	{ UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("cutout") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("bump") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("bump_map_amt") },
	{ UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement") },
	{ UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement_map_amt") },
	{ UFBX_MATERIAL_PBR_SUBSURFACE_TYPE, 0, 0, ufbxi_mat_string("subsurfaceType") },
};

static const ufbxi_shader_mapping ufbxi_3ds_max_physical_material_features[] = {
	{ UFBX_MATERIAL_FEATURE_THIN_WALLED, 0, 0, ufbxi_mat_string("thin_walled") },
	{ UFBX_MATERIAL_FEATURE_SPECULAR, 0, 0, ufbxi_mat_string("material_mode") },
	{ UFBX_MATERIAL_FEATURE_DIFFUSE_ROUGHNESS, 0, 0, ufbxi_mat_string("material_mode") },
	{ UFBX_MATERIAL_FEATURE_TRANSMISSION_ROUGHNESS, UFBXI_SHADER_FEATURE_INVERTED, 0, ufbxi_mat_string("trans_roughness_lock") },
	{ UFBX_MATERIAL_FEATURE_ROUGHNESS_AS_GLOSSINESS, 0, 0, ufbxi_mat_string("roughness_inv") },
	{ UFBX_MATERIAL_FEATURE_TRANSMISSION_ROUGHNESS_AS_GLOSSINESS, 0, 0, ufbxi_mat_string("trans_roughness_inv") },
	{ UFBX_MATERIAL_FEATURE_COAT_ROUGHNESS_AS_GLOSSINESS, 0, 0, ufbxi_mat_string("coat_roughness_inv") },
};

static const ufbxi_shader_mapping ufbxi_gltf_material_pbr_mapping[] = {
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("main|baseColor") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("main|roughness") },
	{ UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("main|metalness") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("main|normal") },
	{ UFBX_MATERIAL_PBR_AMBIENT_OCCLUSION, 0, 0, ufbxi_mat_string("main|ambientOcclusion") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("main|emission") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("main|emissionColor") },
	{ UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("main|Alpha") },
	{ UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("extension|clearcoat") },
	{ UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("extension|clearcoatRoughness") },
	{ UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("extension|clearcoatNormal") },
	{ UFBX_MATERIAL_PBR_SHEEN_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("extension|sheenColor") },
	{ UFBX_MATERIAL_PBR_SHEEN_ROUGHNESS, 0, 0, ufbxi_mat_string("extension|sheenRoughness") },
	{ UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("extension|specular") },
	{ UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("extension|Specular") },
	{ UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("extension|specularcolor") },
	{ UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("extension|specularColor") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("extension|transmission") },
	{ UFBX_MATERIAL_PBR_SPECULAR_IOR, 0, 0, ufbxi_mat_string("extension|indexOfRefraction") },
};

static const ufbxi_shader_mapping ufbxi_3ds_max_pbr_metal_rough_pbr_mapping[] = {
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("base_color") },
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("baseColor") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("roughness") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("Roughness_Map") },
	{ UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("metalness") },
	{ UFBX_MATERIAL_PBR_AMBIENT_OCCLUSION, 0, 0, ufbxi_mat_string("ao") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("norm") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emit_color") },
	{ UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement") },
	{ UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement_amt") },
	{ UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("opacity") },
};

static const ufbxi_shader_mapping ufbxi_3ds_max_pbr_spec_gloss_pbr_mapping[] = {
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("base_color") },
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("baseColor") },
	{ UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Specular") },
	{ UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("specular") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("glossiness") },
	{ UFBX_MATERIAL_PBR_AMBIENT_OCCLUSION, 0, 0, ufbxi_mat_string("ao") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("norm") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emit_color") },
	{ UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement") },
	{ UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement_amt") },
	{ UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("opacity") },
};

static const ufbxi_shader_mapping ufbxi_3ds_max_pbr_features[] = {
	{ UFBX_MATERIAL_FEATURE_ROUGHNESS_AS_GLOSSINESS, UFBXI_SHADER_FEATURE_IF_AROUND_1, 0, ufbxi_mat_string("useGlossiness") },
};

static const ufbxi_shader_mapping ufbxi_gltf_material_features[] = {
	{ UFBX_MATERIAL_FEATURE_DOUBLE_SIDED, 0, 0, ufbxi_mat_string("main|DoubleSided") },
	{ UFBX_MATERIAL_FEATURE_SHEEN, 0, 0, ufbxi_mat_string("extension|enableSheen") },
	{ UFBX_MATERIAL_FEATURE_COAT, 0, 0, ufbxi_mat_string("extension|enableClearCoat") },
	{ UFBX_MATERIAL_FEATURE_TRANSMISSION, 0, 0, ufbxi_mat_string("extension|enableTransmission") },
	{ UFBX_MATERIAL_FEATURE_IOR, 0, 0, ufbxi_mat_string("extension|enableIndexOfRefraction") },
	{ UFBX_MATERIAL_FEATURE_SPECULAR, 0, 0, ufbxi_mat_string("extension|enableSpecular") },
	{ UFBX_MATERIAL_FEATURE_UNLIT, 0, 0, ufbxi_mat_string("extension|unlit") },
};

// NOTE: These are just the names used by the standard PBS "preset".
// In _theory_ we could walk ShaderGraph but that's a bit out of scope for ufbx.
static const ufbxi_shader_mapping ufbxi_shaderfx_graph_pbr_mapping[] = {
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("color") },
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("base_color") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("roughness") },
	{ UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("metallic") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("normal") },
	{ UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("emissive_intensity") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emissive") },
	{ UFBX_MATERIAL_PBR_AMBIENT_OCCLUSION, 0, 0, ufbxi_mat_string("ao") },
};

static const ufbxi_shader_mapping ufbxi_blender_phong_shader_pbr_mapping[] = {
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("DiffuseColor") },
	{ UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, UFBXI_MAT_TRANSFORM_BLENDER_OPACITY, ufbxi_mat_string("TransparencyFactor") },
	{ UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("EmissiveFactor") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("EmissiveColor") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, UFBXI_MAT_TRANSFORM_BLENDER_SHININESS, ufbxi_mat_string("Shininess") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, UFBXI_MAT_TRANSFORM_BLENDER_SHININESS, ufbxi_mat_string("ShininessExponent") },
	{ UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("ReflectionFactor") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("NormalMap") },
};

static const ufbxi_shader_mapping ufbxi_obj_pbr_mapping[] = {
	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Kd") },
	{ UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Ks") },
	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Ke") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, UFBXI_MAT_TRANSFORM_UNKNOWN_SHININESS, ufbxi_mat_string("Ns") },
	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("Pr") },
	{ UFBX_MATERIAL_PBR_SPECULAR_IOR, 0, 0, ufbxi_mat_string("Ni") },
	{ UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("Pm") },
	{ UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("d") },
	{ UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Tf") },
	{ UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("disp") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("bump") },
	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("norm") },
	{ UFBX_MATERIAL_PBR_SHEEN_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Ps") },
	{ UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("Pc") },
	{ UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("Pcr") },
	{ UFBX_MATERIAL_PBR_SPECULAR_ANISOTROPY, 0, 0, ufbxi_mat_string("aniso") },
	{ UFBX_MATERIAL_PBR_SPECULAR_ROTATION, 0, 0, ufbxi_mat_string("anisor") },
};

static const ufbxi_shader_mapping ufbxi_obj_features[] = {
	{ UFBX_MATERIAL_FEATURE_PBR, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Pr") },
	{ UFBX_MATERIAL_FEATURE_PBR, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Pm") },
	{ UFBX_MATERIAL_FEATURE_SHEEN, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Ps") },
	{ UFBX_MATERIAL_FEATURE_COAT, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Pc") },
	{ UFBX_MATERIAL_FEATURE_METALNESS, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Pm") },
	{ UFBX_MATERIAL_FEATURE_IOR, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Ni") },
	{ UFBX_MATERIAL_FEATURE_OPACITY, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("d") },
	{ UFBX_MATERIAL_FEATURE_TRANSMISSION, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Tf") },
	{ UFBX_MATERIAL_FEATURE_EMISSION, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Ke") },
};

enum {
	UFBXI_MAT_PBR = 1 << UFBX_MATERIAL_FEATURE_PBR,
	UFBXI_MAT_METALNESS = 1 << UFBX_MATERIAL_FEATURE_METALNESS,
	UFBXI_MAT_DIFFUSE = 1 << UFBX_MATERIAL_FEATURE_DIFFUSE,
	UFBXI_MAT_SPECULAR = 1 << UFBX_MATERIAL_FEATURE_SPECULAR,
	UFBXI_MAT_EMISSION = 1 << UFBX_MATERIAL_FEATURE_EMISSION,
	UFBXI_MAT_COAT = 1 << UFBX_MATERIAL_FEATURE_COAT,
	UFBXI_MAT_SHEEN = 1 << UFBX_MATERIAL_FEATURE_SHEEN,
	UFBXI_MAT_TRANSMISSION = 1 << UFBX_MATERIAL_FEATURE_TRANSMISSION,
	UFBXI_MAT_OPACITY = 1 << UFBX_MATERIAL_FEATURE_OPACITY,
	UFBXI_MAT_AMBIENT_OCCLUSION = 1 << UFBX_MATERIAL_FEATURE_AMBIENT_OCCLUSION,
	UFBXI_MAT_MATTE = 1 << UFBX_MATERIAL_FEATURE_MATTE,
	UFBXI_MAT_UNLIT = 1 << UFBX_MATERIAL_FEATURE_UNLIT,
	UFBXI_MAT_IOR = 1 << UFBX_MATERIAL_FEATURE_IOR,
	UFBXI_MAT_DIFFUSE_ROUGHNESS = 1 << UFBX_MATERIAL_FEATURE_DIFFUSE_ROUGHNESS,
	UFBXI_MAT_TRANSMISSION_ROUGHNESS = 1 << UFBX_MATERIAL_FEATURE_TRANSMISSION_ROUGHNESS,
	UFBXI_MAT_THIN_WALLED = 1 << UFBX_MATERIAL_FEATURE_THIN_WALLED,
	UFBXI_MAT_CAUSTICS = 1 << UFBX_MATERIAL_FEATURE_CAUSTICS,
	UFBXI_MAT_EXIT_TO_BACKGROUND = 1 << UFBX_MATERIAL_FEATURE_EXIT_TO_BACKGROUND,
	UFBXI_MAT_INTERNAL_REFLECTIONS = 1 << UFBX_MATERIAL_FEATURE_INTERNAL_REFLECTIONS,
	UFBXI_MAT_DOUBLE_SIDED = 1 << UFBX_MATERIAL_FEATURE_DOUBLE_SIDED,
};

static const ufbxi_shader_mapping_list ufbxi_shader_pbr_mappings[] = {
	{ // UFBX_SHADER_UNKNOWN
		ufbxi_fbx_phong_shader_pbr_mapping, ufbxi_arraycount(ufbxi_fbx_phong_shader_pbr_mapping),
		NULL, 0,
		(uint32_t)(UFBXI_MAT_DIFFUSE | UFBXI_MAT_SPECULAR | UFBXI_MAT_EMISSION | UFBXI_MAT_TRANSMISSION),
	},
	{ // UFBX_SHADER_FBX_LAMBERT
		ufbxi_fbx_lambert_shader_pbr_mapping, ufbxi_arraycount(ufbxi_fbx_lambert_shader_pbr_mapping),
		NULL, 0,
		(uint32_t)(UFBXI_MAT_DIFFUSE | UFBXI_MAT_EMISSION | UFBXI_MAT_TRANSMISSION),
	},
	{ // UFBX_SHADER_FBX_PHONG
		ufbxi_fbx_phong_shader_pbr_mapping, ufbxi_arraycount(ufbxi_fbx_phong_shader_pbr_mapping),
		NULL, 0,
		(uint32_t)(UFBXI_MAT_DIFFUSE | UFBXI_MAT_SPECULAR | UFBXI_MAT_EMISSION | UFBXI_MAT_TRANSMISSION),
	},
	{ // UFBX_SHADER_OSL_STANDARD_SURFACE
		ufbxi_osl_standard_shader_pbr_mapping, ufbxi_arraycount(ufbxi_osl_standard_shader_pbr_mapping),
		ufbxi_osl_standard_shader_features, ufbxi_arraycount(ufbxi_osl_standard_shader_features),
		(uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_SPECULAR | UFBXI_MAT_COAT
			| UFBXI_MAT_SHEEN | UFBXI_MAT_TRANSMISSION | UFBXI_MAT_OPACITY | UFBXI_MAT_IOR | UFBXI_MAT_DIFFUSE_ROUGHNESS),
	},
	{ // UFBX_SHADER_ARNOLD_STANDARD_SURFACE
		ufbxi_arnold_shader_pbr_mapping, ufbxi_arraycount(ufbxi_arnold_shader_pbr_mapping),
		ufbxi_arnold_shader_features, ufbxi_arraycount(ufbxi_arnold_shader_features),
		(uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_SPECULAR | UFBXI_MAT_COAT
			| UFBXI_MAT_SHEEN | UFBXI_MAT_TRANSMISSION | UFBXI_MAT_OPACITY | UFBXI_MAT_IOR | UFBXI_MAT_DIFFUSE_ROUGHNESS),
	},
	{ // UFBX_SHADER_3DS_MAX_PHYSICAL_MATERIAL
		ufbxi_3ds_max_physical_material_pbr_mapping, ufbxi_arraycount(ufbxi_3ds_max_physical_material_pbr_mapping),
		ufbxi_3ds_max_physical_material_features, ufbxi_arraycount(ufbxi_3ds_max_physical_material_features),
		(uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_COAT
			| UFBXI_MAT_SHEEN | UFBXI_MAT_TRANSMISSION | UFBXI_MAT_OPACITY | UFBXI_MAT_IOR),
		{ NULL, 0 }, ufbxi_string_literal("_map"),    // texture_prefix/suffix
		{ NULL, 0 }, ufbxi_string_literal("_map_on"), // texture_enabled_prefix/suffix
	},
	{ // UFBX_SHADER_3DS_MAX_PBR_METAL_ROUGH
		ufbxi_3ds_max_pbr_metal_rough_pbr_mapping, ufbxi_arraycount(ufbxi_3ds_max_pbr_metal_rough_pbr_mapping),
		ufbxi_3ds_max_pbr_features, ufbxi_arraycount(ufbxi_3ds_max_pbr_features),
		(uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_OPACITY),
		{ NULL, 0 }, ufbxi_string_literal("_map"), // texture_prefix/suffix
		{ NULL, 0 }, { NULL, 0 }, // texture_enabled_prefix/suffix
	},
	{ // UFBX_SHADER_3DS_MAX_PBR_SPEC_GLOSS
		ufbxi_3ds_max_pbr_spec_gloss_pbr_mapping, ufbxi_arraycount(ufbxi_3ds_max_pbr_spec_gloss_pbr_mapping),
		ufbxi_3ds_max_pbr_features, ufbxi_arraycount(ufbxi_3ds_max_pbr_features),
		(uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_SPECULAR | UFBXI_MAT_DIFFUSE | UFBXI_MAT_OPACITY),
		{ NULL, 0 }, ufbxi_string_literal("_map"), // texture_prefix/suffix
		{ NULL, 0 }, { NULL, 0 }, // texture_enabled_prefix/suffix
	},
	{ // UFBX_SHADER_GLTF_MATERIAL
		ufbxi_gltf_material_pbr_mapping, ufbxi_arraycount(ufbxi_gltf_material_pbr_mapping),
		ufbxi_gltf_material_features, ufbxi_arraycount(ufbxi_gltf_material_features),
		(uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_EMISSION | UFBXI_MAT_OPACITY | UFBXI_MAT_AMBIENT_OCCLUSION),
		{ NULL, 0 }, ufbxi_string_literal("Map"), // texture_prefix/suffix
		{ NULL, 0 }, { NULL, 0 }, // texture_enabled_prefix/suffix
	},
	{ // UFBX_SHADER_SHADERFX_GRAPH
		ufbxi_shaderfx_graph_pbr_mapping, ufbxi_arraycount(ufbxi_shaderfx_graph_pbr_mapping),
		NULL, 0,
		(uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_EMISSION | UFBXI_MAT_AMBIENT_OCCLUSION),
		ufbxi_string_literal("TEX_"), ufbxi_string_literal("_map"), // texture_prefix/suffix
		ufbxi_string_literal("use_"), ufbxi_string_literal("_map"), // texture_enabled_prefix/suffix
	},
	{ // UFBX_SHADER_BLENDER_PHONG
		ufbxi_blender_phong_shader_pbr_mapping, ufbxi_arraycount(ufbxi_blender_phong_shader_pbr_mapping),
		NULL, 0,
		(uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_EMISSION),
	},
	{ // UFBX_SHADER_WAVEFRONT_MTL
		ufbxi_obj_pbr_mapping, ufbxi_arraycount(ufbxi_obj_pbr_mapping),
		ufbxi_obj_features, ufbxi_arraycount(ufbxi_obj_features),
		(uint32_t)(UFBXI_MAT_DIFFUSE | UFBXI_MAT_SPECULAR),
	},
};

ufbx_static_assert(shader_pbr_mapping_list, ufbxi_arraycount(ufbxi_shader_pbr_mappings) == UFBX_SHADER_TYPE_COUNT);

enum {
	UFBXI_MAPPING_FETCH_VALUE = 0x1,
	UFBXI_MAPPING_FETCH_TEXTURE = 0x2,
	UFBXI_MAPPING_FETCH_TEXTURE_ENABLED = 0x4,
	UFBXI_MAPPING_FETCH_FEATURE = 0x8,
};

ufbxi_noinline static void ufbxi_fetch_mapping_maps(ufbx_material *material, ufbx_material_map *maps, ufbx_material_feature_info *features,
	ufbx_shader *shader, const ufbxi_shader_mapping *mappings, size_t count, ufbx_string prefix, ufbx_string prefix2, ufbx_string suffix, uint32_t flags)
{
	char combined_name[512];
	ufbx_shader_prop_binding identity_binding;

	ufbxi_for(const ufbxi_shader_mapping, mapping, mappings, count) {
		ufbx_string prop_name = { mapping->prop, mapping->prop_len };
		if (prefix.length > 0 || prefix2.length > 0 || suffix.length > 0) {
			if (prop_name.length + prefix.length + prefix2.length + suffix.length <= sizeof(combined_name)) {
				char *dst = combined_name;

				if (prefix.length > 0) {
					memcpy(dst, prefix.data, prefix.length);
					dst += prefix.length;
				}
				if (prefix2.length > 0) {
					memcpy(dst, prefix2.data, prefix2.length);
					dst += prefix2.length;
				}
				if (prop_name.length > 0) {
					memcpy(dst, prop_name.data, prop_name.length);
					dst += prop_name.length;
				}
				if (suffix.length > 0) {
					memcpy(dst, suffix.data, suffix.length);
					dst += suffix.length;
				}

				prop_name.data = combined_name;
				prop_name.length = ufbxi_to_size(dst - combined_name);
			}
		}

		ufbx_shader_prop_binding_list bindings = ufbx_find_shader_prop_bindings_len(shader, prop_name.data, prop_name.length);
		if (bindings.count == 0) {
			identity_binding.material_prop = prop_name;
			identity_binding.shader_prop = ufbx_empty_string;
			bindings.data = &identity_binding;
			bindings.count = 1;
		}

		uint32_t mapping_flags = mapping->flags;
		ufbxi_for_list(ufbx_shader_prop_binding, binding, bindings) {
			ufbx_string name = binding->material_prop;

			ufbx_prop *prop = ufbx_find_prop_len(&material->props, name.data, name.length);
			if (flags & UFBXI_MAPPING_FETCH_FEATURE) {
				ufbx_material_feature_info *feature = &features[mapping->index];
				if (prop && prop->type != UFBX_PROP_REFERENCE) {
					feature->enabled = prop->value_int != 0;
					feature->is_explicit = true;
					if (mapping_flags & UFBXI_SHADER_FEATURE_IF_AROUND_1) {
						feature->enabled = (prop->value_real >= 0.5f && prop->value_real <= 1.5f);
					}
					if (mapping_flags & UFBXI_SHADER_FEATURE_INVERTED) {
						feature->enabled = !feature->enabled;
					}
					if (mapping_flags & UFBXI_SHADER_FEATURE_IF_EXISTS) {
						feature->enabled = true;
					}
				}
				if (mapping_flags & UFBXI_SHADER_FEATURE_IF_TEXTURE) {
					ufbx_texture *texture = ufbx_find_prop_texture_len(material, name.data, name.length);
					if (texture) {
						feature->enabled = true;
					}
				}
				continue;
			}

			ufbx_material_map *map = &maps[mapping->index];

			if (flags & UFBXI_MAPPING_FETCH_VALUE) {
				if (prop && prop->type != UFBX_PROP_REFERENCE) {
					map->value_vec4 = prop->value_vec4;
					map->value_int = prop->value_int;
					map->has_value = true;
					if (mapping->transform) {
						ufbxi_mat_transform_fn transform_fn = ufbxi_mat_transform_fns[mapping->transform];
						transform_fn(&map->value_vec4);
					}

					uint32_t prop_flags = (uint32_t)prop->flags;
					if ((mapping->flags & UFBXI_SHADER_MAPPING_DEFAULT_W_1) != 0 && (prop_flags & UFBX_PROP_FLAG_VALUE_VEC4) == 0) {
						map->value_vec4.w = 1.0f;
					}
					if ((mapping->flags & UFBXI_SHADER_MAPPING_WIDEN_TO_RGB) != 0 && (prop_flags & UFBX_PROP_FLAG_VALUE_REAL) != 0) {
						map->value_vec3.y = map->value_vec3.x;
						map->value_vec3.z = map->value_vec3.x;
					}
					if ((prop_flags & UFBX_PROP_FLAG_VALUE_REAL) != 0) {
						map->value_components = 1;
					} else if ((prop_flags & UFBX_PROP_FLAG_VALUE_VEC2) != 0) {
						map->value_components = 2;
					} else if ((prop_flags & UFBX_PROP_FLAG_VALUE_VEC3) != 0) {
						map->value_components = 3;
					} else if ((prop_flags & UFBX_PROP_FLAG_VALUE_VEC4) != 0) {
						map->value_components = 4;
					} else {
						map->value_components = 0;
					}
				}
			}

			if (flags & UFBXI_MAPPING_FETCH_TEXTURE) {
				ufbx_texture *texture = ufbx_find_prop_texture_len(material, name.data, name.length);
				if (texture) {
					map->texture = texture;
					map->texture_enabled = true;
				}
			}

			if (flags & UFBXI_MAPPING_FETCH_TEXTURE_ENABLED) {
				if (prop) {
					map->texture_enabled = prop->value_int != 0;
				}
			}
		}
	}
}

ufbxi_noinline static void ufbxi_update_factor(ufbx_material_map *factor_map, ufbx_material_map *color_map)
{
	if (!factor_map->has_value) {
		if (color_map->has_value && !ufbxi_is_vec4_zero(color_map->value_vec4)) {
			factor_map->value_real = 1.0f;
			factor_map->value_int = 1;
		} else {
			factor_map->value_real = 0.0f;
			factor_map->value_int = 0;
		}
	}
}

// Some material modes have toggleable roughness/glossiness mode, we read it initially
// always as roughness and if a matching feature such as `roughness_as_glossiness` is set
// we transfer the data into the glossiness and invert the roughness.
typedef struct {
	uint8_t feature;
	uint8_t roughness_map;
	uint8_t glossiness_map;
} ufbxi_glossiness_remap;

static const ufbxi_glossiness_remap ufbxi_glossiness_remaps[] = {
	{ UFBX_MATERIAL_FEATURE_ROUGHNESS_AS_GLOSSINESS, UFBX_MATERIAL_PBR_ROUGHNESS, UFBX_MATERIAL_PBR_GLOSSINESS },
	{ UFBX_MATERIAL_FEATURE_COAT_ROUGHNESS_AS_GLOSSINESS, UFBX_MATERIAL_PBR_COAT_ROUGHNESS, UFBX_MATERIAL_PBR_COAT_GLOSSINESS },
	{ UFBX_MATERIAL_FEATURE_TRANSMISSION_ROUGHNESS_AS_GLOSSINESS, UFBX_MATERIAL_PBR_TRANSMISSION_ROUGHNESS, UFBX_MATERIAL_PBR_TRANSMISSION_GLOSSINESS },
};

ufbxi_noinline static void ufbxi_fetch_maps(ufbx_scene *scene, ufbx_material *material)
{
	(void)scene;

	ufbx_shader *shader = material->shader;
	ufbx_assert((uint32_t)material->shader_type < UFBX_SHADER_TYPE_COUNT);

	memset(&material->fbx, 0, sizeof(material->fbx));
	memset(&material->pbr, 0, sizeof(material->pbr));
	memset(&material->features, 0, sizeof(material->features));

	const ufbxi_shader_mapping *base_mapping = ufbxi_base_fbx_mapping;
	size_t num_base_mapping = ufbxi_arraycount(ufbxi_base_fbx_mapping);

	if (scene->metadata.file_format == UFBX_FILE_FORMAT_OBJ || scene->metadata.file_format == UFBX_FILE_FORMAT_MTL) {
		base_mapping = ufbxi_obj_fbx_mapping;
		num_base_mapping = ufbxi_arraycount(ufbxi_obj_fbx_mapping);
	}

	ufbxi_fetch_mapping_maps(material, material->fbx.maps, NULL, NULL,
		base_mapping, num_base_mapping,
		ufbx_empty_string, ufbx_empty_string, ufbx_empty_string,
		UFBXI_MAPPING_FETCH_VALUE | UFBXI_MAPPING_FETCH_TEXTURE);

	ufbxi_shader_mapping_list list = ufbxi_shader_pbr_mappings[material->shader_type];

	for (uint32_t i = 0; i < UFBX_MATERIAL_FEATURE_COUNT; i++) {
		if ((list.default_features & (1u << i)) != 0) {
			material->features.features[i].enabled = true;
		}
	}

	ufbx_string prefix = ufbx_empty_string;
	if (!shader) {
		prefix = material->shader_prop_prefix;
	}

	if (list.texture_prefix.length > 0 || list.texture_suffix.length > 0) {
		ufbxi_fetch_mapping_maps(material, material->pbr.maps, NULL, shader,
			list.data, list.count, prefix, list.texture_prefix, list.texture_suffix,
			UFBXI_MAPPING_FETCH_TEXTURE);
	}

	ufbxi_fetch_mapping_maps(material, material->pbr.maps, NULL, shader,
		list.data, list.count, prefix, ufbx_empty_string, ufbx_empty_string,
		UFBXI_MAPPING_FETCH_VALUE | UFBXI_MAPPING_FETCH_TEXTURE);

	if (list.texture_enabled_prefix.length > 0 || list.texture_enabled_suffix.length > 0) {
		ufbxi_fetch_mapping_maps(material, material->pbr.maps, NULL, shader,
			list.data, list.count, prefix, list.texture_enabled_prefix, list.texture_enabled_suffix,
			UFBXI_MAPPING_FETCH_TEXTURE_ENABLED);
	}

	ufbxi_fetch_mapping_maps(material, NULL, material->features.features, shader,
		list.features, list.feature_count, prefix, ufbx_empty_string, ufbx_empty_string,
		UFBXI_MAPPING_FETCH_FEATURE);

	ufbxi_update_factor(&material->fbx.diffuse_factor, &material->fbx.diffuse_color);
	ufbxi_update_factor(&material->fbx.specular_factor, &material->fbx.specular_color);
	ufbxi_update_factor(&material->fbx.reflection_factor, &material->fbx.reflection_color);
	ufbxi_update_factor(&material->fbx.transparency_factor, &material->fbx.transparency_color);
	ufbxi_update_factor(&material->fbx.emission_factor, &material->fbx.emission_color);
	ufbxi_update_factor(&material->fbx.ambient_factor, &material->fbx.ambient_color);

	ufbxi_update_factor(&material->pbr.base_factor, &material->pbr.base_color);
	ufbxi_update_factor(&material->pbr.specular_factor, &material->pbr.specular_color);
	ufbxi_update_factor(&material->pbr.emission_factor, &material->pbr.emission_color);
	ufbxi_update_factor(&material->pbr.sheen_factor, &material->pbr.sheen_color);
	ufbxi_update_factor(&material->pbr.transmission_factor, &material->pbr.transmission_color);

	// Patch transmission roughness if only extra roughness is defined
	if (!material->pbr.transmission_roughness.has_value && material->pbr.roughness.has_value && material->pbr.transmission_extra_roughness.has_value) {
		material->pbr.transmission_roughness.value_real = material->pbr.roughness.value_real + material->pbr.transmission_extra_roughness.value_real;
	}

	// Map roughness to glossiness and vice versa
	ufbxi_for(const ufbxi_glossiness_remap, remap, ufbxi_glossiness_remaps, ufbxi_arraycount(ufbxi_glossiness_remaps)) {
		ufbx_material_map *roughness = &material->pbr.maps[remap->roughness_map];
		ufbx_material_map *glossiness = &material->pbr.maps[remap->glossiness_map];
		if (material->features.features[remap->feature].enabled) {
			*glossiness = *roughness;
			memset(roughness, 0, sizeof(ufbx_material_map));
			if (glossiness->has_value) {
				roughness->value_real = 1.0f - glossiness->value_real;
			}
		} else {
			if (roughness->has_value) {
				glossiness->value_real = 1.0f - roughness->value_real;
			}
		}
	}
}

typedef enum {
	UFBXI_CONSTRAINT_PROP_NODE,
	UFBXI_CONSTRAINT_PROP_IK_EFFECTOR,
	UFBXI_CONSTRAINT_PROP_IK_END_NODE,
	UFBXI_CONSTRAINT_PROP_AIM_UP,
	UFBXI_CONSTRAINT_PROP_TARGET,
} ufbxi_constraint_prop_type;

typedef struct {
	ufbxi_constraint_prop_type type;
	const char *name;
} ufbxi_constraint_prop;

static const ufbxi_constraint_prop ufbxi_constraint_props[] = {
	{ UFBXI_CONSTRAINT_PROP_NODE, "Constrained Object" },
	{ UFBXI_CONSTRAINT_PROP_NODE, "Constrained object (Child)" },
	{ UFBXI_CONSTRAINT_PROP_NODE, "First Joint" },
	{ UFBXI_CONSTRAINT_PROP_TARGET, "Source" },
	{ UFBXI_CONSTRAINT_PROP_TARGET, "Source (Parent)" },
	{ UFBXI_CONSTRAINT_PROP_TARGET, "Aim At Object" },
	{ UFBXI_CONSTRAINT_PROP_TARGET, "Pole Vector Object" },
	{ UFBXI_CONSTRAINT_PROP_IK_EFFECTOR, "Effector" },
	{ UFBXI_CONSTRAINT_PROP_IK_END_NODE, "End Joint" },
	{ UFBXI_CONSTRAINT_PROP_AIM_UP, "World Up Object" },
};

ufbxi_nodiscard ufbxi_noinline static int ufbxi_add_constraint_prop(ufbxi_context *uc, ufbx_constraint *constraint, ufbx_node *node, const char *prop)
{
	ufbxi_for(const ufbxi_constraint_prop, cprop, ufbxi_constraint_props, ufbxi_arraycount(ufbxi_constraint_props)) {
		if (strcmp(cprop->name, prop) != 0) continue;
		switch (cprop->type) {
		case UFBXI_CONSTRAINT_PROP_NODE: constraint->node = node; break;
		case UFBXI_CONSTRAINT_PROP_IK_EFFECTOR: constraint->ik_effector = node; break;
		case UFBXI_CONSTRAINT_PROP_IK_END_NODE: constraint->ik_end_node = node; break;
		case UFBXI_CONSTRAINT_PROP_AIM_UP: constraint->aim_up_node = node; break;
		case UFBXI_CONSTRAINT_PROP_TARGET: {
			ufbx_constraint_target *target = ufbxi_push_zero(&uc->tmp_stack, ufbx_constraint_target, 1);
			ufbxi_check(target);
			target->node = node;
			target->weight = 1.0f;
			target->transform = ufbx_identity_transform;
		} break;
		default:
			ufbxi_unreachable("Unexpected constraint prop");
		}
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_nurbs_basis(ufbxi_context *uc, ufbx_nurbs_basis *basis)
{
	if (basis->topology == UFBX_NURBS_TOPOLOGY_CLOSED) {
		basis->num_wrap_control_points = 1;
	} else if (basis->topology == UFBX_NURBS_TOPOLOGY_PERIODIC) {
		basis->num_wrap_control_points = basis->order - 1;
	} else {
		basis->num_wrap_control_points = 0;
	}

	if (basis->order > 1) {
		size_t degree = basis->order - 1;
		ufbx_real_list knots = basis->knot_vector;
		if (knots.count >= 2*degree + 1) {
			basis->t_min = knots.data[degree];
			basis->t_max = knots.data[knots.count - degree - 1];

			size_t max_spans = knots.count - 2*degree;
			ufbx_real *spans = ufbxi_push(&uc->result, ufbx_real, max_spans);
			ufbxi_check(spans);

			ufbx_real prev = -UFBX_INFINITY;
			size_t num_spans = 0;
			for (size_t i = 0; i < max_spans; i++) {
				ufbx_real t = knots.data[degree + i];
				if (t != prev) {
					spans[num_spans++] = t;
					prev = t;
				}
			}

			basis->spans.data = spans;
			basis->spans.count = num_spans;
			basis->valid = true;
			for (size_t i = 1; i < knots.count; i++) {
				if (knots.data[i - 1] > knots.data[i]) {
					basis->valid = false;
					break;
				}
			}
		}
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_lod_group(ufbxi_context *uc, ufbx_lod_group *lod)
{
	size_t num_levels = 0;
	for (size_t i = 0; i < lod->instances.count; i++) {
		num_levels = ufbxi_max_sz(num_levels, lod->instances.data[0]->children.count);
	}

	char prop_name[64];
	for (size_t i = 0; ; i++) {
		int len = ufbxi_snprintf(prop_name, sizeof(prop_name), "Thresholds|Level%zu", i);
		ufbx_prop *prop = ufbx_find_prop_len(&lod->props, prop_name, (size_t)len);
		if (!prop) break;
		num_levels = ufbxi_max_sz(num_levels, i + 1);
	}

	ufbx_lod_level *levels = ufbxi_push_zero(&uc->result, ufbx_lod_level, num_levels);
	ufbxi_check(levels);

	lod->relative_distances = ufbx_find_bool(&lod->props, "ThresholdsUsedAsPercentage", false);
	lod->ignore_parent_transform = !ufbx_find_bool(&lod->props, "WorldSpace", true);

	lod->use_distance_limit = ufbx_find_bool(&lod->props, "MinMaxDistance", false);
	lod->distance_limit_min = ufbx_find_real(&lod->props, "MinDistance", (ufbx_real)-100.0);
	lod->distance_limit_max = ufbx_find_real(&lod->props, "MaxDistance", (ufbx_real)100.0);

	lod->lod_levels.data = levels;
	lod->lod_levels.count = num_levels;

	for (size_t i = 0; i < num_levels; i++) {
		ufbx_lod_level *level = &levels[i];

		if (i > 0) {
			int len = ufbxi_snprintf(prop_name, sizeof(prop_name), "Thresholds|Level%zu", i - 1);
			level->distance = ufbx_find_real_len(&lod->props, prop_name, (size_t)len, 0.0f);
		} else if (lod->relative_distances) {
			level->distance = (ufbx_real)100.0;
		}

		{
			int len = ufbxi_snprintf(prop_name, sizeof(prop_name), "DisplayLevels|Level%zu", i);
			int64_t display = ufbx_find_int_len(&lod->props, prop_name, (size_t)len, 0);
			if (display >= 0 && display <= 2) {
				level->display = (ufbx_lod_display)display;
			}
		}
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_generate_normals(ufbxi_context *uc, ufbx_mesh *mesh)
{
	size_t num_indices = mesh->num_indices;

	mesh->generated_normals = true;

	ufbx_topo_edge *topo = ufbxi_push(&uc->tmp_stack, ufbx_topo_edge, num_indices);
	ufbxi_check(topo);

	uint32_t *normal_indices = ufbxi_push(&uc->result, uint32_t, num_indices);
	ufbxi_check(normal_indices);

	ufbx_compute_topology(mesh, topo, num_indices);
	size_t num_normals = ufbx_generate_normal_mapping(mesh, topo, num_indices, normal_indices, num_indices, false);

	if (num_normals == mesh->num_vertices) {
		mesh->vertex_normal.unique_per_vertex = true;
	}

	ufbx_vec3 *normal_data = ufbxi_push(&uc->result, ufbx_vec3, num_normals + 1);
	ufbxi_check(normal_data);

	normal_data[0] = ufbx_zero_vec3;
	normal_data++;

	ufbx_compute_normals(mesh, &mesh->vertex_position, normal_indices, num_indices, normal_data, num_normals);

	mesh->vertex_normal.exists = true;
	mesh->vertex_normal.values.data = normal_data;
	mesh->vertex_normal.values.count = num_normals;
	mesh->vertex_normal.indices.data = normal_indices;
	mesh->vertex_normal.indices.count = num_indices;
	mesh->vertex_normal.value_reals = 3;

	mesh->skinned_normal = mesh->vertex_normal;

	ufbxi_pop(&uc->tmp_stack, ufbx_topo_edge, num_indices, NULL);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_push_prop_prefix(ufbxi_context *uc, ufbx_string *dst, ufbx_string prefix)
{
	size_t stack_size = 0;
	if (prefix.length > 0 && prefix.data[prefix.length - 1] != '|') {
		stack_size = prefix.length + 1;
		char *copy = ufbxi_push(&uc->tmp_stack, char, stack_size);
		ufbxi_check(copy);
		memcpy(copy, prefix.data, prefix.length);
		copy[prefix.length] = '|';

		prefix.data = copy;
		prefix.length += 1;
	}

	ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &prefix, false));
	*dst = prefix;

	if (stack_size > 0) {
		ufbxi_pop(&uc->tmp_stack, char, stack_size, NULL);
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_shader_texture_find_prefix(ufbxi_context *uc, ufbx_texture *texture, ufbx_shader_texture *shader)
{
	ufbx_string suffixes[3];
	size_t num_suffixes = 0;

	suffixes[num_suffixes++] = ufbxi_str_c(" Parameters/Connections");
	if (shader->shader_name.length > 0) {
		suffixes[num_suffixes++] = shader->shader_name;
	}
	suffixes[num_suffixes++] = ufbxi_str_c("3dsMax|parameters");

	ufbx_assert(num_suffixes <= ufbxi_arraycount(suffixes));

	ufbxi_for(ufbx_string, p_suffix, suffixes, num_suffixes) {
		ufbx_string suffix = *p_suffix;

		ufbxi_for_list(ufbx_prop, prop, texture->props.props) {
			if (prop->type != UFBX_PROP_COMPOUND) continue;
			if (ufbxi_ends_with(prop->name, suffix)) {
				ufbxi_check(ufbxi_push_prop_prefix(uc, &shader->prop_prefix, prop->name));
				return 1;
			}
		}
	}

	// Pre-7000 files don't have explicit Compound properties, so let's look for
	// any property that has the suffix before the last `|` ...
	ufbxi_for(ufbx_string, p_suffix, suffixes, num_suffixes) {
		ufbx_string suffix = *p_suffix;

		ufbxi_for_list(ufbx_prop, prop, texture->props.props) {
			ufbx_string name = prop->name;
			while (name.length > 0) {
				if (name.data[name.length - 1] == '|') {
					break;
				}
				name.length--;
			}
			if (name.length <= 1) continue;
			name.length--;

			if (ufbxi_ends_with(name, suffix)) {
				ufbxi_check(ufbxi_push_prop_prefix(uc, &shader->prop_prefix, name));
				return 1;
			}
		}
	}

	return 1;
}

typedef struct {
	uint64_t shader_id;
	const char *shader_name;
	const char *input_name;
} ufbxi_file_shader;

// Known shaders that represent sampled images.
static const ufbxi_file_shader ufbxi_file_shaders[] = {
	{ UINT64_C(0x7e73161fad53b12a), "ai_image", "filename" },
	{ 0, "OSLBitmap", ufbxi_Filename },
	{ 0, "OSLBitmap2", ufbxi_Filename },
	{ 0, "UberBitmap", ufbxi_Filename },
	{ 0, "UberBitmap2", ufbxi_Filename },
};

ufbxi_noinline static void ufbxi_update_shader_texture(ufbx_texture *texture, ufbx_shader_texture *shader)
{
	ufbxi_for_list(ufbx_shader_texture_input, input, shader->inputs) {
		ufbx_prop *prop = input->prop;
		if (prop) {
			input->prop = prop = ufbx_find_prop_len(&texture->props, prop->name.data, prop->name.length);
			input->value_vec4 = prop->value_vec4;
			input->value_int = prop->value_int;
			input->value_str = prop->value_str;
			input->value_blob = prop->value_blob;
			input->texture = (ufbx_texture*)ufbx_get_prop_element(&texture->element, input->prop, UFBX_ELEMENT_TEXTURE);
		}

		prop = input->texture_prop;
		if (prop) {
			input->texture_prop = prop = ufbx_find_prop_len(&texture->props, prop->name.data, prop->name.length);
			ufbx_texture *tex = (ufbx_texture*)ufbx_get_prop_element(&texture->element, prop, UFBX_ELEMENT_TEXTURE);
			if (tex) input->texture = tex;
		}

		input->texture_enabled = input->texture != NULL;
		prop = input->texture_enabled_prop;
		if (prop) {
			input->texture_enabled_prop = prop = ufbx_find_prop_len(&texture->props, prop->name.data, prop->name.length);
			input->texture_enabled = prop->value_int != 0;
		}
	}

	if (shader->type == UFBX_SHADER_TEXTURE_SELECT_OUTPUT) {
		ufbx_shader_texture_input *map = ufbx_find_shader_texture_input(shader, "sourceMap");
		ufbx_shader_texture_input *index = ufbx_find_shader_texture_input(shader, "outputChannelIndex");
		if (index) {
			shader->main_texture_output_index = index->value_int;
		}
		if (map) {
			shader->main_texture = map->texture;
			map->texture_output_index = shader->main_texture_output_index;
		}
	}
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_shader_texture(ufbxi_context *uc, ufbx_texture *texture)
{
	uint32_t classid_a = (uint32_t)(uint64_t)ufbx_find_int(&texture->props, "3dsMax|ClassIDa", 0);
	uint32_t classid_b = (uint32_t)(uint64_t)ufbx_find_int(&texture->props, "3dsMax|ClassIDb", 0);
	uint64_t classid = (uint64_t)classid_a << 32u | classid_b;

	ufbx_string max_texture = ufbx_find_string(&texture->props, "3dsMax|MaxTexture", ufbx_empty_string);

	// Check first if the texture looks like it could be a shader.
	ufbx_shader_texture_type type = (ufbx_shader_texture_type)UFBX_SHADER_TEXTURE_TYPE_COUNT;

	if (!strcmp(max_texture.data, "MULTIOUTPUT_TO_OSLMap") || classid == UINT64_C(0x896ef2fc44bd743f)) {
		type = UFBX_SHADER_TEXTURE_SELECT_OUTPUT;
	} else if (!strcmp(max_texture.data, "OSLMap") || classid == UINT64_C(0x7f9a7b9d6fcdf00d)) {
		type = UFBX_SHADER_TEXTURE_OSL;
	} else if (texture->type == UFBX_TEXTURE_FILE && texture->relative_filename.length == 0 && texture->absolute_filename.length == 0 && !texture->video) {
		type = UFBX_SHADER_TEXTURE_UNKNOWN;
	}

	if ((uint32_t)type == UFBX_SHADER_TEXTURE_TYPE_COUNT) return 1;

	ufbx_shader_texture *shader = ufbxi_push_zero(&uc->result, ufbx_shader_texture, 1);
	ufbxi_check(shader);

	shader->type = type;

	static const char *const name_props[] = {
		"3dsMax|params|OSLShaderName",
	};

	static const char *const source_props[] = {
		"3dsMax|params|OSLCode",
	};

	shader->shader_source.data = ufbxi_empty_char;
	shader->shader_name.data = ufbxi_empty_char;

	ufbxi_nounroll for (size_t i = 0; i < ufbxi_arraycount(name_props); i++) {
		ufbx_prop *prop = ufbx_find_prop(&texture->props, name_props[i]);
		if (prop) {
			shader->shader_name = prop->value_str;
			break;
		}
	}

	ufbxi_nounroll for (size_t i = 0; i < ufbxi_arraycount(source_props); i++) {
		ufbx_prop *prop = ufbx_find_prop(&texture->props, source_props[i]);
		if (prop) {
			shader->shader_source = prop->value_str;
			shader->raw_shader_source = prop->value_blob;
			break;
		}
	}

	ufbxi_check(ufbxi_shader_texture_find_prefix(uc, texture, shader));

	if (shader->shader_name.length == 0) {
		ufbx_string name = shader->prop_prefix;
		if (ufbxi_remove_suffix_c(&name, " Parameters/Connections|")) {
			size_t begin = name.length;
			while (begin > 0 && name.data[begin - 1] != '|') {
				begin--;
			}

			shader->shader_name.data = name.data + begin;
			shader->shader_name.length = name.length - begin;
			ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &shader->shader_name, false));
		}
	}

	if (shader->shader_name.length == 0) {
		if (max_texture.length > 0) {
			shader->shader_name = max_texture;
		}
	}

	if (classid != 0) {
		shader->shader_type_id = classid;
	}

	if (shader->prop_prefix.length == 0) {
		// If we not find any shader properties so we might have guessed wrong.
		// We "leak" (freed with scene) the shader in this case but it's negligible.
		return 1;
	}

	ufbxi_for_list(ufbx_prop, prop, texture->props.props) {

		ufbx_string name = prop->name;
		if (!ufbxi_remove_prefix_str(&name, shader->prop_prefix)) continue;

		// Check if this property is a modifier to an existing input.
		ufbx_string base_name = name;
		if (ufbxi_remove_suffix_c(&base_name, "_map") || ufbxi_remove_suffix_c(&base_name, ".shader")) {
			ufbx_shader_texture_input *base = ufbx_find_shader_texture_input_len(shader, base_name.data, base_name.length);
			if (base) {
				base->texture_prop = prop;
				continue;
			}
		} else if (ufbxi_remove_suffix_c(&base_name, ".connected") || ufbxi_remove_suffix_c(&base_name, "Enabled")) {
			ufbx_shader_texture_input *base = ufbx_find_shader_texture_input_len(shader, base_name.data, base_name.length);
			if (base) {
				base->texture_enabled_prop = prop;
				continue;
			}
		}

		// Use `uc->tmp_arr` to store the texture inputs so we can search them while we insert new ones.
		ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size,
			(shader->inputs.count + 1) * sizeof(ufbx_shader_texture_input)));
		shader->inputs.data = (ufbx_shader_texture_input*)uc->tmp_arr;

		// Add a new property
		ufbx_shader_texture_input *input = &shader->inputs.data[shader->inputs.count++];
		memset(input, 0, sizeof(ufbx_shader_texture_input));

		// NOTE: This is a bit hackish, we are using a suffix of an interned string. It won't compare
		// pointer equal to the same string but that shouldn't matter..
		input->name = name;

		// Connect the property only, values and textures etc are fetched in `ufbxi_update_shader_texture()`.
		input->prop = prop;
	}

	// Retain the shader inputs
	shader->inputs.data = ufbxi_push_copy(&uc->result, ufbx_shader_texture_input, shader->inputs.count, shader->inputs.data);
	ufbxi_check(shader->inputs.data);

	texture->shader = shader;
	texture->type = UFBX_TEXTURE_SHADER;
	uc->scene.metadata.num_shader_textures++;

	if (!uc->opts.disable_quirks) {
		ufbxi_nounroll for (size_t i = 0; i < ufbxi_arraycount(ufbxi_file_shaders); i++) {
			const ufbxi_file_shader *fs = &ufbxi_file_shaders[i];

			if ((fs->shader_id && shader->shader_type_id == fs->shader_id) || !strcmp(shader->shader_name.data, fs->shader_name)) {
				ufbx_shader_texture_input *input = ufbx_find_shader_texture_input(shader, fs->input_name);
				if (input) {
					// TODO: Support for specifying relative filename here if ever needed
					ufbx_prop *prop = input->prop;
					texture->absolute_filename = prop->value_str;
					texture->raw_absolute_filename = prop->value_blob;
					texture->type = UFBX_TEXTURE_FILE;
					break;
				}
			}
		}
	}

	ufbxi_update_shader_texture(texture, shader);

	return 1;
}

ufbxi_noinline static void ufbxi_propagate_main_textures(ufbx_scene *scene)
{
	// We need to do at least 2^(N-1) passes for N shader textures
	size_t mask = scene->metadata.num_shader_textures;
	while (mask) {
		mask >>= 1;

		ufbxi_for_ptr_list(ufbx_texture, p_texture, scene->textures) {
			ufbx_texture *texture = *p_texture;
			ufbx_shader_texture *shader = texture->shader;
			if (!shader) continue;

			ufbx_texture *main_tex = shader->main_texture;
			if (!main_tex || shader->main_texture_output_index != 0) continue;

			ufbx_shader_texture *main_shader = main_tex->shader;
			if (!main_shader || !main_shader->main_texture) continue;

			shader->main_texture = main_shader->main_texture;
			shader->main_texture_output_index = main_shader->main_texture_output_index;
		}
	}

	// Remove cyclic main textures
	ufbxi_for_ptr_list(ufbx_texture, p_texture, scene->textures) {
		ufbx_texture *texture = *p_texture;
		ufbx_shader_texture *shader = texture->shader;
		if (!shader || !shader->main_texture || shader->main_texture_output_index != 0) continue;
		ufbx_texture *main_tex = shader->main_texture;
		if (main_tex && main_tex->shader && main_tex->shader->main_texture) {
			// Should have been propagated to `texture`
			shader->main_texture = NULL;
		}
	}

	ufbxi_for_ptr_list(ufbx_texture, p_texture, scene->textures) {
		ufbx_texture *texture = *p_texture;
		ufbx_shader_texture *shader = texture->shader;
		if (!shader) continue;

		ufbxi_for_list(ufbx_shader_texture_input, input, shader->inputs) {
			if (!input->texture || !input->texture->shader) continue;
			ufbx_shader_texture *input_shader = input->texture->shader;
			if (input_shader->main_texture) {
				input->texture = input_shader->main_texture;
				input->texture_output_index = input_shader->main_texture_output_index;
			}
		}
	}

	ufbxi_for_ptr_list(ufbx_material, p_material, scene->materials) {
		ufbx_material *material = *p_material;

		ufbxi_for_list(ufbx_material_texture, tex, material->textures) {
			ufbx_shader_texture *shader = tex->texture->shader;
			if (shader && shader->main_texture && shader->main_texture_output_index == 0) {
				tex->texture = shader->main_texture;
			}
		}
	}
}

#define ufbxi_patch_empty(m_dst, m_len, m_src) \
	do { if (!(m_dst).m_len) m_dst = m_src; } while (0)

ufbxi_nodiscard ufbxi_noinline static int ufbxi_insert_texture_file(ufbxi_context *uc, ufbx_texture *texture)
{
	texture->file_index = UFBX_NO_INDEX;

	const char *key = NULL;

	// HACK: Even the raw entries have a null terminator so we can offset the
	// pointer by one for relative filenames. This guarantees that an overlapping
	// absolute and relative filenames will get separate textures.
	if (texture->raw_absolute_filename.size > 0) {
		key = (const char*)texture->raw_absolute_filename.data;
	} else if (texture->raw_relative_filename.size > 0) {
		key = (const char*)texture->raw_relative_filename.data + 1;
	}

	if (key == NULL) return 1;
	uint32_t hash = ufbxi_hash_ptr(key);
	ufbxi_texture_file_entry *entry = ufbxi_map_find(&uc->texture_file_map, ufbxi_texture_file_entry, hash, &key);
	if (!entry) {
		entry = ufbxi_map_insert(&uc->texture_file_map, ufbxi_texture_file_entry, hash, &key);
		ufbxi_check(entry);

		ufbx_texture_file *file = ufbxi_push_zero(&uc->tmp, ufbx_texture_file, 1);
		ufbxi_check(file);

		file->index = uc->texture_file_map.size - 1;

		entry->key = key;
		entry->file = file;
	}

	ufbx_texture_file *file = entry->file;
	texture->file_index = file->index;
	texture->has_file = true;
	ufbxi_patch_empty(file->filename, length, texture->filename);
	ufbxi_patch_empty(file->relative_filename, length, texture->relative_filename);
	ufbxi_patch_empty(file->absolute_filename, length, texture->absolute_filename);
	ufbxi_patch_empty(file->raw_filename, size, texture->raw_filename);
	ufbxi_patch_empty(file->raw_relative_filename, size, texture->raw_relative_filename);
	ufbxi_patch_empty(file->raw_absolute_filename, size, texture->raw_absolute_filename);
	ufbxi_patch_empty(file->content, size, texture->content);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_pop_texture_files(ufbxi_context *uc)
{
	uint32_t num_files = uc->texture_file_map.size;
	ufbx_texture_file *files = ufbxi_push(&uc->result, ufbx_texture_file, num_files);
	ufbxi_check(files);

	uc->scene.texture_files.data = files;
	uc->scene.texture_files.count = num_files;

	ufbxi_texture_file_entry *entries = (ufbxi_texture_file_entry*)uc->texture_file_map.items;
	for (size_t i = 0; i < num_files; i++) {
		memcpy(&files[i], entries[i].file, sizeof(ufbx_texture_file));
	}

	return 1;
}

typedef struct {
	ufbx_texture *texture;
	size_t order;
} ufbxi_ordered_texture;

ufbxi_noinline static bool ufbxi_ordered_texture_less_texture(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbxi_ordered_texture *a = (const ufbxi_ordered_texture*)va, *b = (const ufbxi_ordered_texture*)vb;
	return a->texture < b->texture;
}

ufbxi_noinline static bool ufbxi_ordered_texture_less_order(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbxi_ordered_texture *a = (const ufbxi_ordered_texture*)va, *b = (const ufbxi_ordered_texture*)vb;
	return a->order < b->order;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_deduplicate_textures(ufbxi_context *uc, ufbxi_buf *dst_buf, ufbxi_ordered_texture **p_dst, size_t *p_dst_count, size_t count)
{
	ufbxi_ordered_texture *textures = ufbxi_push_pop(dst_buf, &uc->tmp_stack, ufbxi_ordered_texture, count);
	ufbxi_check(textures);

	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbxi_ordered_texture)));

	ufbxi_stable_sort(sizeof(ufbxi_ordered_texture), 16, textures, uc->tmp_arr, count, &ufbxi_ordered_texture_less_texture, NULL);

	// Remove adjacent duplicates
	size_t dst_ix = 0;
	for (size_t src_ix = 0; src_ix < count; src_ix++) {
		if (src_ix > 0 && textures[src_ix - 1].texture == textures[src_ix].texture) {
			continue;
		} else {
			if (src_ix != dst_ix) {
				textures[dst_ix] = textures[src_ix];
			}
			dst_ix++;
		}
	}

	size_t new_count = dst_ix;
	ufbxi_stable_sort(sizeof(ufbxi_ordered_texture), 16, textures, uc->tmp_arr, new_count, &ufbxi_ordered_texture_less_order, NULL);

	*p_dst_count = new_count;
	*p_dst = textures;

	return 1;
}

typedef enum {
	UFBXI_FILE_TEXTURE_FETCH_INITIAL,
	UFBXI_FILE_TEXTURE_FETCH_STARTED,
	UFBXI_FILE_TEXTURE_FETCH_FINISHED,
} ufbxi_file_texture_fetch_state;

// Populate `ufbx_texture.file_textures[]` arrays.
ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_file_textures(ufbxi_context *uc)
{
	// We keep pointers to `ufbx_texture` in `tmp_stack` as a working set, since we don't know
	// how deep the shader graphs might be.

	// Start by pushing all the textures into the stack
	size_t num_stack_textures = uc->scene.textures.count;
	ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_texture*, num_stack_textures, uc->scene.textures.data));

	// Compressed `ufbxi_file_texture_fetch_state`
	uint8_t *states = ufbxi_push_zero(&uc->tmp, uint8_t, uc->scene.textures.count);
	ufbxi_check(states);

	while (num_stack_textures-- > 0) {
		ufbx_texture *texture = NULL;
		ufbxi_pop(&uc->tmp_stack, ufbx_texture*, 1, &texture);

		ufbxi_file_texture_fetch_state state = (ufbxi_file_texture_fetch_state)states[texture->typed_id];
		if (state == UFBXI_FILE_TEXTURE_FETCH_FINISHED) continue;
		ufbx_shader_texture *shader = texture->shader;

		if (state == UFBXI_FILE_TEXTURE_FETCH_STARTED) {
			states[texture->typed_id] = UFBXI_FILE_TEXTURE_FETCH_FINISHED;

			// HACK: Reuse `tmp_parse` for storing intermediate information as we can clear it.
			ufbxi_buf_clear(&uc->tmp_parse);

			// Now all non-cyclical dependents should be processed.
			size_t num_deps = 0;

			if (texture->type == UFBX_TEXTURE_FILE) {
				ufbxi_ordered_texture *dst = ufbxi_push(&uc->tmp_stack, ufbxi_ordered_texture, 1);
				ufbxi_check(dst);
				dst->texture = texture;
				dst->order = num_deps++;
			}

			ufbxi_for_list(ufbx_texture_layer, layer, texture->layers) {
				ufbx_texture *dep_tex = layer->texture;
				if (dep_tex->file_textures.count > 0) {
					ufbxi_ordered_texture *dst = ufbxi_push(&uc->tmp_stack, ufbxi_ordered_texture, 1);
					ufbxi_check(dst);
					dst->texture = dep_tex;
					dst->order = num_deps++;
				}
			}

			if (shader) {
				ufbxi_for_list(ufbx_shader_texture_input, input, shader->inputs) {
					ufbx_texture *dep_tex = input->texture;
					if (dep_tex && dep_tex->file_textures.count > 0) {
						ufbxi_ordered_texture *dst = ufbxi_push(&uc->tmp_stack, ufbxi_ordered_texture, 1);
						ufbxi_check(dst);
						dst->texture = dep_tex;
						dst->order = num_deps++;
					}
				}
			}

			// Deduplicate the direct dependencies first
			ufbxi_ordered_texture *deps;
			ufbxi_check(ufbxi_deduplicate_textures(uc, &uc->tmp_parse, &deps, &num_deps, num_deps));

			if (num_deps == 1) {
				// If we have only a single dependency (that is not the same one) we can just copy the pointer
				texture->file_textures = deps[0].texture->file_textures;
			} else {
				// Now collect all the file textures and deduplicate them
				size_t num_files = 0;
				ufbxi_for(ufbxi_ordered_texture, dep, deps, num_deps) {
					ufbxi_for_ptr_list(ufbx_texture, p_tex, dep->texture->file_textures) {
						ufbxi_ordered_texture *dst = ufbxi_push(&uc->tmp_stack, ufbxi_ordered_texture, 1);
						ufbxi_check(dst);
						dst->texture = *p_tex;
						dst->order = num_files++;
					}
				}

				// Deduplicate the file textures
				ufbxi_ordered_texture *files;
				ufbxi_check(ufbxi_deduplicate_textures(uc, &uc->tmp_parse, &files, &num_files, num_files));

				texture->file_textures.count = num_files;
				texture->file_textures.data = ufbxi_push(&uc->result, ufbx_texture*, num_files);
				ufbxi_check(texture->file_textures.data);

				for (size_t i = 0; i < num_files; i++) {
					texture->file_textures.data[i] = files[i].texture;
				}
			}

		} else {
			if (texture->type == UFBX_TEXTURE_FILE) {
				// Simple case: Just point to self
				texture->file_textures.count = 1;
				texture->file_textures.data = ufbxi_push(&uc->result, ufbx_texture*, 1);
				ufbxi_check(texture->file_textures.data);
				texture->file_textures.data[0] = texture;

				// In simple cases we can quit here, for more complex file textures queue
				// the texture in case there are other file textures as inputs.
				if (!texture->shader) {
					states[texture->typed_id] = UFBXI_FILE_TEXTURE_FETCH_FINISHED;
					continue;
				}
			}

			// Complex: Process all dependencies first
			states[texture->typed_id] = UFBXI_FILE_TEXTURE_FETCH_STARTED;

			// Push self first so we can return after processing dependencies
			ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_texture*, 1, &texture));
			num_stack_textures++;

			ufbxi_for_list(ufbx_texture_layer, layer, texture->layers) {
				ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_texture*, 1, &layer->texture));
				num_stack_textures++;
			}

			if (shader) {
				ufbxi_for_list(ufbx_shader_texture_input, input, shader->inputs) {
					if (input->texture) {
						ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_texture*, 1, &input->texture));
						num_stack_textures++;
					}
				}
			}
		}
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static ufbx_node *ufbxi_get_geometry_transform_node(ufbx_element *element)
{
	if (element->instances.count == 1) {
		ufbx_node *node = element->instances.data[0];
		if (node->has_geometry_transform) return node;
	}
	return NULL;
}

ufbxi_noinline static void ufbxi_mirror_vec3_list(const void *v_list, ufbx_mirror_axis axis, size_t stride)
{
	const ufbx_void_list *list = (const ufbx_void_list*)v_list;
	if (axis == UFBX_MIRROR_AXIS_NONE || !list || list->count == 0) return;
	if (!stride) stride = sizeof(ufbx_vec3);

	void *ptr = (char*)list->data + (size_t)((int)axis - 1) * sizeof(ufbx_real);
	void *end = (char*)ptr + list->count * stride;
	while (ptr != end) {
		ufbx_real *v = (ufbx_real*)ptr;
		*v = -*v;
		ptr = (char*)ptr + stride;
	}
}

ufbxi_noinline static void ufbxi_scale_vec3_list(const void *v_list, ufbx_real scale, size_t stride)
{
	const ufbx_void_list *list = (const ufbx_void_list*)v_list;
	if (!list || list->count == 0) return;
	if (!stride) stride = sizeof(ufbx_vec3);

	void *ptr = list->data, *end = (char*)ptr + list->count * stride;
	while (ptr != end) {
		ufbx_vec3 *v = (ufbx_vec3*)ptr;
		v->x *= scale;
		v->y *= scale;
		v->z *= scale;
		ptr = (char*)ptr + stride;
	}
}

ufbxi_noinline static void ufbxi_transform_vec3_list(const void *v_list, const ufbx_matrix *matrix, size_t stride)
{
	const ufbx_void_list *list = (const ufbx_void_list*)v_list;
	if (!list || list->count == 0) return;
	if (!stride) stride = sizeof(ufbx_vec3);

	void *ptr = list->data, *end = (char*)ptr + list->count * stride;
	while (ptr != end) {
		ufbx_vec3 *v = (ufbx_vec3*)ptr;
		*v = ufbx_transform_position(matrix, *v);
		ptr = (char*)ptr + stride;
	}
}

ufbxi_noinline static void ufbxi_normalize_vec3_list(const ufbx_vec3_list *list)
{
	ufbxi_nounroll ufbxi_for_list(ufbx_vec3, normal, *list) {
		*normal = ufbxi_normalize3(*normal);
	}
}

// Forward declare as we're kind of preprocessing ata here that would usually happen later.
ufbxi_noinline static ufbx_transform ufbxi_get_geometry_transform(const ufbx_props *props, ufbx_node *node);

ufbxi_nodiscard ufbxi_noinline static int ufbxi_flip_attrib_winding(ufbxi_context *uc, ufbx_mesh *mesh, ufbx_uint32_list *indices, bool is_position)
{
	// All zero, no flipping needed
	if (indices->data == uc->zero_indices || indices->count == 0) return 1;

	if (indices->data == mesh->vertex_position.indices.data && !is_position) {
		// Sharing indices with vertex position, already flipped.
		return 1;
	} else if (indices->data == uc->consecutive_indices) {
		// Need to duplicate consecutive indices, but we can cache the per mesh.
		if (uc->tmp_mesh_consecutive_indices) {
			indices->data = uc->tmp_mesh_consecutive_indices;
			return 1;
		}
		indices->data = ufbxi_push_copy(&uc->result, uint32_t, indices->count, indices->data);
		ufbxi_check(indices->data);
		uc->tmp_mesh_consecutive_indices = indices->data;
	}

	uint32_t *data = indices->data;
	ufbxi_for_list(ufbx_face, face, mesh->faces) {
		if (face->num_indices == 0) continue;
		size_t begin = face->index_begin + 1;
		size_t end = face->index_begin + face->num_indices - 1;
		while (begin < end) {
			uint32_t tmp = data[begin];
			data[begin] = data[end];
			data[end] = tmp;
			begin++;
			end--;
		}
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_flip_winding(ufbxi_context *uc, ufbx_mesh *mesh)
{
	uc->tmp_mesh_consecutive_indices = NULL;
	ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &mesh->vertex_position.indices, true));
	ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &mesh->vertex_normal.indices, false));
	ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &mesh->vertex_crease.indices, false));
	if (mesh->uv_sets.count > 0) {
		ufbxi_for_list(ufbx_uv_set, set, mesh->uv_sets) {
			ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &set->vertex_uv.indices, false));
			ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &set->vertex_tangent.indices, false));
			ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &set->vertex_bitangent.indices, false));
		}
		mesh->vertex_uv = mesh->uv_sets.data[0].vertex_uv;
		mesh->vertex_bitangent = mesh->uv_sets.data[0].vertex_bitangent;
		mesh->vertex_tangent = mesh->uv_sets.data[0].vertex_tangent;
	}
	if (mesh->color_sets.count > 0) {
		ufbxi_for_list(ufbx_color_set, set, mesh->color_sets) {
			ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &set->vertex_color.indices, false));
		}
		mesh->vertex_color = mesh->color_sets.data[0].vertex_color;
	}
	ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &mesh->skinned_position.indices, false));
	if (mesh->skinned_normal.indices.data != mesh->vertex_normal.indices.data) {
		ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &mesh->skinned_normal.indices, false));
	}

	ufbxi_update_vertex_first_index(mesh);

	// Mapping from old index values to flipped ones, reserve index -1
	// (aka `UFBX_NO_INDEX`) for itself.
	if (mesh->edges.count > 0) {
		ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, (mesh->num_indices + 1) * sizeof(uint32_t)));
		uint32_t *index_mapping = (uint32_t*)uc->tmp_arr + 1;
		index_mapping[-1] = UFBX_NO_INDEX;
		ufbxi_for_list(ufbx_face, face, mesh->faces) {
			if (face->num_indices == 0) continue;
			uint32_t begin = face->index_begin;
			uint32_t count = face->num_indices - 1;
			index_mapping[begin] = begin;
			for (uint32_t i = 0; i < count; i++) {
				index_mapping[begin + 1 + i] = begin + count - i;
			}
		}

		ufbxi_for_list(ufbx_edge, p_edge, mesh->edges) {
			uint32_t a = index_mapping[(int32_t)p_edge->a];
			uint32_t b = index_mapping[(int32_t)p_edge->b];
			p_edge->a = b;
			p_edge->b = a;
		}
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_modify_geometry(ufbxi_context *uc)
{
	bool do_mirror = false;
	bool do_winding = uc->opts.reverse_winding;
	bool do_scale = false;
	bool do_geometry_transforms = false;
	if (uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY
		|| uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY_NO_FALLBACK) {
		// Prefetch geometry transforms for processing, they will later be overwritten in `ufbxi_update_node()`.
		ufbxi_for_ptr_list(ufbx_node, p_node, uc->scene.nodes) {
			ufbx_node *node = *p_node;
			if (node->is_root) continue;

			node->geometry_transform = ufbxi_get_geometry_transform(&node->props, node);
			if (!ufbxi_is_transform_identity(&node->geometry_transform)) {
				node->geometry_to_node = ufbx_transform_to_matrix(&node->geometry_transform);
				node->has_geometry_transform = true;
			} else {
				node->geometry_to_node = ufbx_identity_matrix;
				node->has_geometry_transform = false;
			}
		}
		do_geometry_transforms = true;
	}
	if (uc->mirror_axis != 0) {
		do_mirror = true;
	}
	if (uc->scene.metadata.geometry_scale != 1.0f) {
		do_scale = true;
	}

	ufbx_real geometry_scale = uc->scene.metadata.geometry_scale;
	ufbx_mirror_axis mirror_axis = uc->mirror_axis;

	ufbxi_for_ptr_list(ufbx_blend_shape, p_shape, uc->scene.blend_shapes) {
		ufbx_blend_shape *shape = *p_shape;

		if (do_scale) {
			ufbxi_scale_vec3_list(&shape->position_offsets, geometry_scale, 0);
		}

		if (do_mirror) {
			ufbxi_mirror_vec3_list(&shape->position_offsets, mirror_axis, 0);
			ufbxi_mirror_vec3_list(&shape->normal_offsets, mirror_axis, 0);
		}
	}

	ufbxi_for_ptr_list(ufbx_mesh, p_mesh, uc->scene.meshes) {
		ufbx_mesh *mesh = *p_mesh;

		if (do_scale) {
			ufbxi_scale_vec3_list(&mesh->vertex_position.values, geometry_scale, 0);
		}

		bool do_flip_winding = do_winding;
		if (do_mirror) {
			ufbxi_mirror_vec3_list(&mesh->vertex_position.values, mirror_axis, 0);
			ufbxi_mirror_vec3_list(&mesh->vertex_normal.values, mirror_axis, 0);
			ufbxi_for_list(ufbx_uv_set, set, mesh->uv_sets) {
				ufbxi_mirror_vec3_list(&set->vertex_tangent.values, mirror_axis, 0);
				ufbxi_mirror_vec3_list(&set->vertex_bitangent.values, mirror_axis, 0);
			}
			if (!uc->opts.handedness_conversion_retain_winding) {
				do_flip_winding = !do_flip_winding;
			}
		}

		// Flip face winding retaining the first vertex
		if (do_flip_winding) {
			mesh->reversed_winding = true;
			ufbxi_check(ufbxi_flip_winding(uc, mesh));
		}

		ufbx_node *geo_node = ufbxi_get_geometry_transform_node(&mesh->element);
		if (do_geometry_transforms && geo_node) {
			ufbx_matrix tangent_matrix = geo_node->geometry_to_node;
			tangent_matrix.m03 = 0.0f;
			tangent_matrix.m13 = 0.0f;
			tangent_matrix.m23 = 0.0f;
			ufbx_matrix normal_matrix = ufbx_matrix_for_normals(&geo_node->geometry_to_node);

			ufbxi_transform_vec3_list(&mesh->vertex_position.values, &geo_node->geometry_to_node, 0);
			ufbxi_transform_vec3_list(&mesh->vertex_normal.values, &normal_matrix, 0);
			ufbxi_normalize_vec3_list(&mesh->vertex_normal.values);

			ufbxi_for_list(ufbx_uv_set, set, mesh->uv_sets) {
				ufbxi_transform_vec3_list(&set->vertex_tangent.values, &tangent_matrix, 0);
				ufbxi_transform_vec3_list(&set->vertex_bitangent.values, &tangent_matrix, 0);
				ufbxi_normalize_vec3_list(&set->vertex_tangent.values);
				ufbxi_normalize_vec3_list(&set->vertex_bitangent.values);
			}
		}
	}

	ufbxi_for_ptr_list(ufbx_line_curve, p_curve, uc->scene.line_curves) {
		ufbx_line_curve *curve = *p_curve;

		if (do_scale) {
			ufbxi_scale_vec3_list(&curve->control_points, geometry_scale, 0);
		}

		if (do_mirror) {
			ufbxi_mirror_vec3_list(&curve->control_points, mirror_axis, 0);
		}

		ufbx_node *geo_node = ufbxi_get_geometry_transform_node(&curve->element);
		if (do_geometry_transforms && geo_node) {
			ufbxi_transform_vec3_list(&curve->control_points, &geo_node->geometry_to_node, 0);
		}
	}

	ufbxi_for_ptr_list(ufbx_nurbs_curve, p_curve, uc->scene.nurbs_curves) {
		ufbx_nurbs_curve *curve = *p_curve;

		if (do_scale) {
			ufbxi_scale_vec3_list(&curve->control_points, geometry_scale, sizeof(ufbx_vec4));
		}

		if (do_mirror) {
			ufbxi_mirror_vec3_list(&curve->control_points, mirror_axis, sizeof(ufbx_vec4));
		}

		ufbx_node *geo_node = ufbxi_get_geometry_transform_node(&curve->element);
		if (do_geometry_transforms && geo_node) {
			ufbxi_transform_vec3_list(&curve->control_points, &geo_node->geometry_to_node, sizeof(ufbx_vec4));
		}
	}

	ufbxi_for_ptr_list(ufbx_nurbs_surface, p_surface, uc->scene.nurbs_surfaces) {
		ufbx_nurbs_surface *surface = *p_surface;

		if (do_scale) {
			ufbxi_scale_vec3_list(&surface->control_points, geometry_scale, sizeof(ufbx_vec4));
		}

		if (do_mirror) {
			ufbxi_mirror_vec3_list(&surface->control_points, mirror_axis, sizeof(ufbx_vec4));
		}

		ufbx_node *geo_node = ufbxi_get_geometry_transform_node(&surface->element);
		if (do_geometry_transforms && geo_node) {
			ufbxi_transform_vec3_list(&surface->control_points, &geo_node->geometry_to_node, sizeof(ufbx_vec4));
		}
	}

	if (uc->opts.geometry_transform_handling != UFBX_GEOMETRY_TRANSFORM_HANDLING_PRESERVE) {
		// Reset all geometry transforms if we're not preserving them
		ufbx_props *defaults = NULL;
		ufbxi_for_ptr_list(ufbx_node, p_node, uc->scene.nodes) {
			ufbx_node *node = *p_node;
			if (!defaults) defaults = node->props.defaults;

			if (node->has_geometry_transform) {
				ufbxi_set_own_prop_vec3_uniform(&node->props, ufbxi_GeometricTranslation, 0.0f);
				ufbxi_set_own_prop_vec3_uniform(&node->props, ufbxi_GeometricRotation, 0.0f);
				ufbxi_set_own_prop_vec3_uniform(&node->props, ufbxi_GeometricScaling, 1.0f);
			}
		}

		if (defaults) {
			ufbxi_set_own_prop_vec3_uniform(defaults, ufbxi_GeometricTranslation, 0.0f);
			ufbxi_set_own_prop_vec3_uniform(defaults, ufbxi_GeometricRotation, 0.0f);
			ufbxi_set_own_prop_vec3_uniform(defaults, ufbxi_GeometricScaling, 1.0f);
		}
	}

	return 1;
}

ufbxi_noinline static void ufbxi_postprocess_scene(ufbxi_context *uc)
{
	if (uc->opts.normalize_normals || uc->opts.normalize_tangents) {
		ufbxi_for_ptr_list(ufbx_mesh, p_mesh, uc->scene.meshes) {
			ufbx_mesh *mesh = *p_mesh;
			if (uc->opts.normalize_normals) {
				ufbxi_normalize_vec3_list(&mesh->vertex_normal.values);
			}
			if (uc->opts.normalize_tangents) {
				ufbxi_for_list(ufbx_uv_set, set, mesh->uv_sets) {
					ufbxi_normalize_vec3_list(&mesh->vertex_tangent.values);
					ufbxi_normalize_vec3_list(&mesh->vertex_bitangent.values);
				}
			}
		}
	}

	if (uc->exporter == UFBX_EXPORTER_BLENDER_BINARY) {
		uc->scene.metadata.ortho_size_unit = 1.0f / uc->scene.metadata.geometry_scale;
	} else {
		uc->scene.metadata.ortho_size_unit = 30.0f;
	}
}

ufbxi_noinline static size_t ufbxi_next_path_segment(const char *data, size_t begin, size_t length)
{
	for (size_t i = begin; i < length; i++) {
		if (data[i] == '/' || data[i] == '\\') {
			return i;
		}
	}
	return length;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_absolute_to_relative_path(ufbxi_context *uc, ufbxi_strblob *p_dst, const ufbxi_strblob *p_rel, const ufbxi_strblob *p_src, bool raw)
{
	const char *rel = ufbxi_strblob_data(p_rel, raw);
	const char *src = ufbxi_strblob_data(p_src, raw);
	size_t rel_length = ufbxi_strblob_length(p_rel, raw);
	size_t src_length = ufbxi_strblob_length(p_src, raw);

	if (rel_length == 0 || src_length == 0) return 1;

	// Absolute paths must start with the same character (either drive or '/')
	if (rel[0] != src[0]) return 1;

	// Find the last directory of the path we want to be relative to
	while (rel_length > 0 && (rel[rel_length - 1] != '/' && rel[rel_length - 1] != '\\')) {
		rel_length--;
	}

	if (rel_length == 0) return 1;
	char separator = rel[rel_length - 1];

	size_t max_length = rel_length * 2 + src_length;

	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, max_length));
	char *tmp = uc->tmp_arr;
	size_t tmp_length = 0;

	size_t rel_begin = 0;
	size_t src_begin = 0;
	while (rel_begin < rel_length && src_begin < src_length) {
		size_t rel_end = ufbxi_next_path_segment(rel, rel_begin, rel_length);
		size_t src_end = ufbxi_next_path_segment(src, src_begin, src_length);
		if (rel_end != src_end || memcmp(rel + rel_begin, src + src_begin, src_end - src_begin) != 0) break;

		rel_begin = rel_end + 1;
		src_begin = src_end + 1;
	}

	while (rel_begin < rel_length) {
		size_t rel_end = ufbxi_next_path_segment(rel, rel_begin, rel_length);
		tmp[tmp_length++] = '.';
		tmp[tmp_length++] = '.';
		tmp[tmp_length++] = separator;
		rel_begin = rel_end + 1;
	}

	while (src_begin < src_length) {
		size_t src_end = ufbxi_next_path_segment(src, src_begin, src_length);
		size_t len = src_end - src_begin;

		memcpy(tmp + tmp_length, src + src_begin, len);
		tmp_length += len;

		if (src_end < src_length) {
			tmp[tmp_length++] = separator;
		}

		src_begin = src_end + 1;
	}

	ufbx_assert(tmp_length <= max_length);

	const char *dst = ufbxi_push_string(&uc->string_pool, tmp, tmp_length, NULL, true);
	ufbxi_check(dst);

	ufbxi_strblob_set(p_dst, dst, tmp_length, raw);

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_filenames(ufbxi_context *uc, ufbxi_strblob *filename, ufbxi_strblob *absolute_filename, ufbxi_strblob *relative_filename, bool raw)
{
	if (ufbxi_strblob_length(relative_filename, raw) == 0) {
		const ufbxi_strblob *original_file_path = raw
			? (const ufbxi_strblob*)&uc->scene.metadata.raw_original_file_path
			: (const ufbxi_strblob*)&uc->scene.metadata.original_file_path;

		ufbxi_check(ufbxi_absolute_to_relative_path(uc, relative_filename, original_file_path, absolute_filename, raw));
	}

	ufbxi_check(ufbxi_resolve_relative_filename(uc, filename, relative_filename, raw));

	return 1;
}

ufbxi_noinline static bool ufbxi_file_content_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbxi_file_content *a = (const ufbxi_file_content*)va, *b = (const ufbxi_file_content*)vb;
	return ufbxi_str_less(a->absolute_filename, b->absolute_filename);
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_file_contents(ufbxi_context *uc, ufbxi_file_content *content, size_t count)
{
	ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbxi_file_content)));
	ufbxi_stable_sort(sizeof(ufbxi_file_content), 32, content, uc->tmp_arr, count, &ufbxi_file_content_less, NULL);
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_push_file_content(ufbxi_context *uc, ufbx_string *p_filename, ufbx_blob *p_data)
{
	if (p_data->size == 0 || p_filename->length == 0) return 1;
	ufbxi_file_content *content = ufbxi_push(&uc->tmp_stack, ufbxi_file_content, 1);
	ufbxi_check(content);

	content->absolute_filename = *p_filename;
	content->content = *p_data;
	return 1;
}

ufbxi_noinline static void ufbxi_fetch_file_content(ufbxi_context *uc, ufbx_string *p_filename, ufbx_blob *p_data)
{
	if (p_data->size > 0) return;
	ufbx_string filename = *p_filename;
	size_t index = SIZE_MAX;
	ufbxi_macro_lower_bound_eq(ufbxi_file_content, 8, &index, uc->file_content, 0, uc->num_file_content,
		( ufbxi_str_less(a->absolute_filename, filename) ),
		( a->absolute_filename.data == filename.data ));
	if (index != SIZE_MAX) {
		*p_data = uc->file_content[index].content;
	}
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_file_content(ufbxi_context *uc)
{
	size_t initial_stack = uc->tmp_stack.num_items;

	ufbxi_for_ptr_list(ufbx_video, p_video, uc->scene.videos) {
		ufbx_video *video = *p_video;
		ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&video->filename, (ufbxi_strblob*)&video->absolute_filename, (ufbxi_strblob*)&video->relative_filename, false));
		ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&video->raw_filename, (ufbxi_strblob*)&video->raw_absolute_filename, (ufbxi_strblob*)&video->raw_relative_filename, true));
		ufbxi_check(ufbxi_push_file_content(uc, &video->absolute_filename, &video->content));
	}

	ufbxi_for_ptr_list(ufbx_audio_clip, p_clip, uc->scene.audio_clips) {
		ufbx_audio_clip *clip = *p_clip;
		clip->absolute_filename = ufbx_find_string(&clip->props, "Path", ufbx_empty_string);
		clip->relative_filename = ufbx_find_string(&clip->props, "RelPath", ufbx_empty_string);
		clip->raw_absolute_filename = ufbx_find_blob(&clip->props, "Path", ufbx_empty_blob);
		clip->raw_relative_filename = ufbx_find_blob(&clip->props, "RelPath", ufbx_empty_blob);
		ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&clip->filename, (ufbxi_strblob*)&clip->absolute_filename, (ufbxi_strblob*)&clip->relative_filename, false));
		ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&clip->raw_filename, (ufbxi_strblob*)&clip->raw_absolute_filename, (ufbxi_strblob*)&clip->raw_relative_filename, true));
		ufbxi_check(ufbxi_push_file_content(uc, &clip->absolute_filename, &clip->content));
	}

	uc->num_file_content = uc->tmp_stack.num_items - initial_stack;
	uc->file_content = ufbxi_push_pop(&uc->tmp, &uc->tmp_stack, ufbxi_file_content, uc->num_file_content);
	ufbxi_check(uc->file_content);
	ufbxi_check(ufbxi_sort_file_contents(uc, uc->file_content, uc->num_file_content));

	ufbxi_for_ptr_list(ufbx_video, p_video, uc->scene.videos) {
		ufbx_video *video = *p_video;
		ufbxi_fetch_file_content(uc, &video->absolute_filename, &video->content);
	}

	ufbxi_for_ptr_list(ufbx_audio_clip, p_clip, uc->scene.audio_clips) {
		ufbx_audio_clip *clip = *p_clip;
		ufbxi_fetch_file_content(uc, &clip->absolute_filename, &clip->content);
	}

	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_validate_indices(ufbxi_context *uc, ufbx_uint32_list *indices, size_t max_index)
{
	if (max_index == 0 && uc->opts.index_error_handling == UFBX_INDEX_ERROR_HANDLING_CLAMP) {
		indices->data = NULL;
		indices->count = 0;
		return 1;
	}

	ufbxi_nounroll ufbxi_for_list(uint32_t, p_ix, *indices) {
		uint32_t ix = *p_ix;
		if (ix >= max_index) {
			ufbxi_check(ufbxi_fix_index(uc, p_ix, ix, max_index));
		}
	}

	return 1;
}

static bool ufbxi_material_part_usage_less(void *user, const void *va, const void *vb)
{
	ufbx_mesh_part *parts = (ufbx_mesh_part*)user;
	uint32_t a = *(const uint32_t*)va, b = *(const uint32_t*)vb;
	ufbx_mesh_part *pa = &parts[a];
	ufbx_mesh_part *pb = &parts[b];
	if (pa->face_indices.count == 0 || pb->face_indices.count == 0) {
		if (pa->face_indices.count == pb->face_indices.count) return a < b;
		return pa->face_indices.count > pb->face_indices.count;
	}
	return pa->face_indices.data[0] < pb->face_indices.data[0];
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_mesh_material(ufbxi_buf *buf, ufbx_error *error, ufbx_mesh *mesh)
{
	size_t num_materials = mesh->materials.count;
	size_t num_parts = mesh->material_parts.count;
	size_t num_faces = mesh->faces.count;

	ufbx_mesh_part *parts = mesh->material_parts.data;
	ufbx_assert(!parts || (mesh->material_parts.count == num_materials) || (mesh->material_parts.count == 1 && num_materials == 0));

	uint32_t *face_material = mesh->face_material.data;

	// Count the number of faces and triangles per material
	ufbxi_nounroll for (size_t i = 0; i < num_faces; i++) {
		ufbx_face face = mesh->faces.data[i];
		uint32_t mat_ix = 0;

		if (face_material) {
			mat_ix = face_material[i];
			if (mat_ix >= num_materials) {
				face_material[i] = 0;
				mat_ix = 0;
			}
		}

		if (parts) {
			ufbxi_mesh_part_add_face(&parts[mat_ix], face.num_indices);
		}
	}

	if (parts) {
		// Allocate per-material buffers (clear `num_faces` to 0 to re-use it as
		// an index when fetching the face indices).
		uint32_t part_index = 0;
		ufbxi_for(ufbx_mesh_part, part, parts, num_parts) {
			part->index = part_index++;
			part->face_indices.count = part->num_faces;
			part->face_indices.data = ufbxi_push(buf, uint32_t, part->num_faces);
			ufbxi_check_err(error, part->face_indices.data);
			part->num_faces = 0;
		}

		// Fetch the per-material face indices
		ufbxi_nounroll for (size_t i = 0; i < num_faces; i++) {
			uint32_t mat_ix = face_material ? face_material[i] : 0;
			if (mat_ix < num_parts) {
				ufbx_mesh_part *part = &parts[mat_ix];
				part->face_indices.data[part->num_faces++] = (uint32_t)i;
			}
		}

		mesh->material_part_usage_order.count = num_parts;
		mesh->material_part_usage_order.data = ufbxi_push(buf, uint32_t, num_parts);
		ufbxi_check_err(error, mesh->material_part_usage_order.data);
		for (size_t i = 0; i < num_parts; i++) {
			mesh->material_part_usage_order.data[i] = (uint32_t)i;
		}
		ufbxi_unstable_sort(mesh->material_part_usage_order.data, num_parts, sizeof(uint32_t), &ufbxi_material_part_usage_less, parts);
	}

	return 1;
}

typedef struct {
	ufbxi_refcount refcount;
	ufbx_anim anim;
	uint32_t magic;
} ufbxi_anim_imp;

ufbxi_nodiscard ufbxi_noinline static int ufbxi_push_anim(ufbxi_context *uc, ufbx_anim **p_anim, ufbx_anim_layer **layers, size_t num_layers)
{
	ufbx_anim *anim = ufbxi_push_zero(&uc->result, ufbx_anim, 1);
	ufbxi_check(anim);

	anim->layers.data = layers;
	anim->layers.count = num_layers;

	*p_anim = anim;
	return 1;
}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_scene(ufbxi_context *uc)
{
	size_t num_elements = uc->num_elements;

	uc->scene.elements.count = num_elements;
	uc->scene.elements.data = ufbxi_push(&uc->result, ufbx_element*, num_elements);
	ufbxi_check(uc->scene.elements.data);

	uc->scene.metadata.element_buffer_size = uc->tmp_element_byte_offset;
	char *element_data = (char*)ufbxi_push_pop(&uc->result, &uc->tmp_elements, uint64_t, uc->tmp_element_byte_offset/8);
	ufbxi_check(element_data);

	size_t *element_offsets = ufbxi_push_pop(&uc->tmp, &uc->tmp_element_offsets, size_t, uc->tmp_element_offsets.num_items);
	ufbxi_buf_free(&uc->tmp_element_offsets);
	ufbxi_check(element_offsets);
	for (size_t i = 0; i < num_elements; i++) {
		ufbx_element *element = (ufbx_element*)(element_data + element_offsets[i]);

		if (element->type == UFBX_ELEMENT_NODE) {
			ufbx_node *node = (ufbx_node*)element;
			if (node->scale_helper) {
				ufbxi_node_extra *extra = (ufbxi_node_extra*)ufbxi_get_element_extra(uc, node->element_id);
				ufbx_assert(extra);
				node->scale_helper = (ufbx_node*)(element_data + element_offsets[extra->scale_helper_id]);
			}
		}

		uc->scene.elements.data[i] = element;
	}

	uc->scene.elements.count = num_elements;
	ufbxi_buf_free(&uc->tmp_element_offsets);
	ufbxi_buf_free(&uc->tmp_elements);

	uc->tmp_element_flag = ufbxi_push_zero(&uc->tmp, uint8_t, num_elements);
	ufbxi_check(uc->tmp_element_flag);

	uc->scene.metadata.original_file_path = ufbx_find_string(&uc->scene.metadata.scene_props, "DocumentUrl", ufbx_empty_string);
	uc->scene.metadata.raw_original_file_path = ufbx_find_blob(&uc->scene.metadata.scene_props, "DocumentUrl", ufbx_empty_blob);

	// Resolve and add the connections to elements
	ufbxi_check(ufbxi_resolve_connections(uc));
	ufbxi_check(ufbxi_add_connections_to_elements(uc));
	ufbxi_check(ufbxi_linearize_nodes(uc));

	for (size_t type = 0; type < UFBX_ELEMENT_TYPE_COUNT; type++) {
		size_t num_typed = uc->tmp_typed_element_offsets[type].num_items;
		size_t *typed_offsets = ufbxi_push_pop(&uc->tmp, &uc->tmp_typed_element_offsets[type], size_t, num_typed);
		ufbxi_buf_free(&uc->tmp_typed_element_offsets[type]);
		ufbxi_check(typed_offsets);

		ufbx_element_list *typed_elems = &uc->scene.elements_by_type[type];
		typed_elems->count = num_typed;
		typed_elems->data = ufbxi_push(&uc->result, ufbx_element*, num_typed);
		ufbxi_check(typed_elems->data);

		for (size_t i = 0; i < num_typed; i++) {
			typed_elems->data[i] = (ufbx_element*)(element_data + typed_offsets[i]);
		}

		ufbxi_buf_free(&uc->tmp_typed_element_offsets[type]);
	}

	// Create named elements
	uc->scene.elements_by_name.count = num_elements;
	uc->scene.elements_by_name.data = ufbxi_push(&uc->result, ufbx_name_element, num_elements);
	ufbxi_check(uc->scene.elements_by_name.data);

	for (size_t i = 0; i < num_elements; i++) {

		ufbx_element *elem = uc->scene.elements.data[i];
		ufbx_name_element *name_elem = &uc->scene.elements_by_name.data[i];

		name_elem->name = elem->name;
		name_elem->type = elem->type;
		name_elem->_internal_key = ufbxi_get_name_key(elem->name.data, elem->name.length);
		name_elem->element = elem;
	}

	ufbxi_check(ufbxi_sort_name_elements(uc, uc->scene.elements_by_name.data, num_elements));

	// Setup node children arrays and attribute pointers/lists
	ufbxi_for_ptr_list(ufbx_node, p_node, uc->scene.nodes) {
		ufbx_node *node = *p_node, *parent = node->parent;
		if (parent) {
			parent->children.count++;
			if (parent->children.data == NULL) {
				parent->children.data = p_node;
			}

			if (node->is_geometry_transform_helper) {
				parent->geometry_transform_helper = node;
			}

			// Force top-level nodes to have `UFBX_INHERIT_MODE_NORMAL` to make unit scaling work.
			if (parent->is_root && uc->opts.space_conversion == UFBX_SPACE_CONVERSION_TRANSFORM_ROOT && uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_PRESERVE) {
				node->original_inherit_mode = UFBX_INHERIT_MODE_NORMAL;
				node->inherit_mode = UFBX_INHERIT_MODE_NORMAL;
			}

			// RrSs nodes inherit scale from their parent, Rrs ignore the scale of
			// their _immediate_ parent, potentially multiple if chained.
			if (node->original_inherit_mode == UFBX_INHERIT_MODE_COMPONENTWISE_SCALE) {
				node->inherit_scale_node = parent;
			} else if (node->original_inherit_mode == UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE) {
				node->inherit_scale_node = parent->inherit_scale_node;
			}
		}

		ufbx_connection_list conns = ufbxi_find_dst_connections(&node->element, NULL);

		ufbxi_for_list(ufbx_connection, conn, conns) {
			ufbx_element *elem = conn->src;
			ufbx_element_type type = elem->type;
			if (!(type >= UFBX_ELEMENT_TYPE_FIRST_ATTRIB && type <= UFBX_ELEMENT_TYPE_LAST_ATTRIB)) continue;

			size_t index = node->all_attribs.count++;
			if (index == 0) {
				node->attrib = elem;
				node->attrib_type = type;
			} else {
				if (index == 1) {
					ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_element*, 1, &node->attrib));
				}
				ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_element*, 1, &elem));
			}

			switch (elem->type) {
			case UFBX_ELEMENT_MESH: node->mesh = (ufbx_mesh*)elem; break;
			case UFBX_ELEMENT_LIGHT: node->light = (ufbx_light*)elem; break;
			case UFBX_ELEMENT_CAMERA: node->camera = (ufbx_camera*)elem; break;
			case UFBX_ELEMENT_BONE: node->bone = (ufbx_bone*)elem; break;
			default: /* No shorthand */ break;
			}
		}

		if (node->all_attribs.count > 1) {
			node->all_attribs.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_element*, node->all_attribs.count);
			ufbxi_check(node->all_attribs.data);
		} else if (node->all_attribs.count == 1) {
			node->all_attribs.data = &node->attrib;
		}

		ufbxi_check(ufbxi_fetch_dst_elements(uc, &node->materials, &node->element, false, false, NULL, UFBX_ELEMENT_MATERIAL));
	}

	// Resolve bind pose bones that don't use the normal connection system
	ufbxi_for_ptr_list(ufbx_pose, p_pose, uc->scene.poses) {
		ufbx_pose *pose = *p_pose;

		// HACK: Transport `ufbxi_tmp_bone_pose` array through the `ufbx_bone_pose` pointer
		size_t num_bones = pose->bone_poses.count;
		ufbxi_tmp_bone_pose *tmp_poses = (ufbxi_tmp_bone_pose*)pose->bone_poses.data;
		pose->bone_poses.data = ufbxi_push(&uc->result, ufbx_bone_pose, num_bones);
		ufbxi_check(pose->bone_poses.data);

		// Filter only found bones
		pose->bone_poses.count = 0;
		for (size_t i = 0; i < num_bones; i++) {
			ufbx_element *elem = ufbxi_find_element_by_fbx_id(uc, tmp_poses[i].bone_fbx_id);
			if (!elem || elem->type != UFBX_ELEMENT_NODE) continue;

			ufbx_node *node = (ufbx_node*)elem;
			ufbx_bone_pose *bone = &pose->bone_poses.data[pose->bone_poses.count++];
			bone->bone_node = node;
			bone->bone_to_world = tmp_poses[i].bone_to_world;

			if (pose->is_bind_pose) {
				if (node->bind_pose == NULL) {
					node->bind_pose = pose;
				}

				ufbx_connection_list node_conns = ufbxi_find_src_connections(elem, NULL);
				ufbxi_for_list(ufbx_connection, conn, node_conns) {
					if (conn->dst->type != UFBX_ELEMENT_SKIN_CLUSTER) continue;
					ufbx_skin_cluster *cluster = (ufbx_skin_cluster*)conn->dst;
					if (ufbxi_matrix_all_zero(&cluster->bind_to_world)) {
						cluster->bind_to_world = bone->bone_to_world;
					}
				}
			}
		}
		ufbxi_check(ufbxi_sort_bone_poses(uc, pose));
	}

	// Fetch pointers that may break elements

	// Setup node attribute instances
	for (int type = UFBX_ELEMENT_TYPE_FIRST_ATTRIB; type <= UFBX_ELEMENT_TYPE_LAST_ATTRIB; type++) {
		ufbxi_for_ptr_list(ufbx_element, p_elem, uc->scene.elements_by_type[type]) {
			ufbx_element *elem = *p_elem;
			ufbxi_check(ufbxi_fetch_src_elements(uc, &elem->instances, elem, false, true, NULL, UFBX_ELEMENT_NODE));
		}
	}

	bool search_node = uc->version < 7000;

	ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, uc->scene.skin_clusters) {
		ufbx_skin_cluster *cluster = *p_cluster;
		cluster->bone_node = (ufbx_node*)ufbxi_fetch_dst_element(&cluster->element, false, NULL, UFBX_ELEMENT_NODE);
	}

	ufbxi_for_ptr_list(ufbx_skin_deformer, p_skin, uc->scene.skin_deformers) {
		ufbx_skin_deformer *skin = *p_skin;
		ufbxi_check(ufbxi_fetch_dst_elements(uc, &skin->clusters, &skin->element, false, true, NULL, UFBX_ELEMENT_SKIN_CLUSTER));

		// Remove clusters without a valid `bone`
		if (!uc->opts.connect_broken_elements) {
			size_t num_broken = 0;
			for (size_t i = 0; i < skin->clusters.count; i++) {
				if (!skin->clusters.data[i]->bone_node) {
					num_broken++;
				} else if (num_broken > 0) {
					skin->clusters.data[i - num_broken] = skin->clusters.data[i];
				}
			}
			skin->clusters.count -= num_broken;
		}

		size_t total_weights = 0;
		ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, skin->clusters) {
			ufbx_skin_cluster *cluster = *p_cluster;
			ufbxi_check(SIZE_MAX - total_weights > cluster->num_weights);
			total_weights += cluster->num_weights;
		}

		size_t num_vertices = 0;

		// Iterate through meshes so we can pad the vertices to the largest one
		{
			ufbx_connection_list conns = ufbxi_find_src_connections(&skin->element, NULL);
			ufbxi_for_list(ufbx_connection, conn, conns) {
				ufbx_mesh *mesh = NULL;
				if (conn->dst_prop.length > 0) continue;
				if (conn->dst->type == UFBX_ELEMENT_MESH) {
					mesh = (ufbx_mesh*)conn->dst;
				} else if (conn->dst->type == UFBX_ELEMENT_NODE) {
					ufbx_node *node = (ufbx_node*)conn->dst;
					if (node->geometry_transform_helper) node = node->geometry_transform_helper;
					mesh = node->mesh;
				}
				if (!mesh) continue;
				num_vertices = ufbxi_max_sz(num_vertices, mesh->num_vertices);
			}
		}

		if (!uc->opts.skip_skin_vertices) {
			skin->vertices.count = num_vertices;
			skin->vertices.data = ufbxi_push_zero(&uc->result, ufbx_skin_vertex, num_vertices);
			ufbxi_check(skin->vertices.data);

			skin->weights.count = total_weights;
			skin->weights.data = ufbxi_push_zero(&uc->result, ufbx_skin_weight, total_weights);
			ufbxi_check(skin->weights.data);

			bool retain_all = !uc->opts.clean_skin_weights;

			// Count the number of weights per vertex
			ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, skin->clusters) {
				ufbx_skin_cluster *cluster = *p_cluster;
				for (size_t i = 0; i < cluster->num_weights; i++) {
					uint32_t vertex = cluster->vertices.data[i];
					if (vertex < num_vertices && (retain_all || cluster->weights.data[i] > 0.0f)) {
						skin->vertices.data[vertex].num_weights++;
					}
				}
			}

			ufbx_real default_dq = skin->skinning_method == UFBX_SKINNING_METHOD_DUAL_QUATERNION ? 1.0f : 0.0f;

			// Prefix sum to assign the vertex weight offsets and set up default DQ values
			uint32_t offset = 0;
			uint32_t max_weights = 0;
			for (size_t i = 0; i < num_vertices; i++) {
				skin->vertices.data[i].weight_begin = offset;
				skin->vertices.data[i].dq_weight = default_dq;
				uint32_t num_weights = skin->vertices.data[i].num_weights;
				offset += num_weights;
				skin->vertices.data[i].num_weights = 0;

				if (num_weights > max_weights) max_weights = num_weights;
			}
			ufbx_assert(offset <= total_weights);
			skin->max_weights_per_vertex = max_weights;

			// Copy the DQ weights to vertices
			for (size_t i = 0; i < skin->num_dq_weights; i++) {
				uint32_t vertex = skin->dq_vertices.data[i];
				if (vertex < num_vertices) {
					skin->vertices.data[vertex].dq_weight = skin->dq_weights.data[i];
				}
			}

			// Copy the weights to vertices
			uint32_t cluster_index = 0;
			ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, skin->clusters) {
				ufbx_skin_cluster *cluster = *p_cluster;
				for (size_t i = 0; i < cluster->num_weights; i++) {
					uint32_t vertex = cluster->vertices.data[i];
					if (vertex < num_vertices && (retain_all || cluster->weights.data[i] > 0.0f)) {
						uint32_t local_index = skin->vertices.data[vertex].num_weights++;
						uint32_t index = skin->vertices.data[vertex].weight_begin + local_index;
						skin->weights.data[index].cluster_index = cluster_index;
						skin->weights.data[index].weight = cluster->weights.data[i];
					}
				}
				cluster_index++;
			}

			// Sort the vertex weights by descending weight value
			ufbxi_check(ufbxi_sort_skin_weights(uc, skin));
		}
	}

	ufbxi_for_ptr_list(ufbx_blend_deformer, p_blend, uc->scene.blend_deformers) {
		ufbx_blend_deformer *blend = *p_blend;
		ufbxi_check(ufbxi_fetch_dst_elements(uc, &blend->channels, &blend->element, false, true, NULL, UFBX_ELEMENT_BLEND_CHANNEL));
	}

	ufbxi_for_ptr_list(ufbx_cache_deformer, p_deformer, uc->scene.cache_deformers) {
		ufbx_cache_deformer *deformer = *p_deformer;
		deformer->channel = ufbx_find_string(&deformer->props, "ChannelName", ufbx_empty_string);
		deformer->file = (ufbx_cache_file*)ufbxi_fetch_dst_element(&deformer->element, false, NULL, UFBX_ELEMENT_CACHE_FILE);
	}

	ufbxi_for_ptr_list(ufbx_cache_file, p_cache, uc->scene.cache_files) {
		ufbx_cache_file *cache = *p_cache;

		cache->absolute_filename = ufbx_find_string(&cache->props, "CacheAbsoluteFileName", ufbx_empty_string);
		cache->relative_filename = ufbx_find_string(&cache->props, "CacheFileName", ufbx_empty_string);

		cache->raw_absolute_filename = ufbx_find_blob(&cache->props, "CacheAbsoluteFileName", ufbx_empty_blob);
		cache->raw_relative_filename = ufbx_find_blob(&cache->props, "CacheFileName", ufbx_empty_blob);

		int64_t type = ufbx_find_int(&cache->props, "CacheFileType", 0);
		if (type >= 0 && type <= UFBX_CACHE_FILE_FORMAT_MC) {
			cache->format = (ufbx_cache_file_format)type;
		}

		ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&cache->filename, (ufbxi_strblob*)&cache->absolute_filename, (ufbxi_strblob*)&cache->relative_filename, false));
		ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&cache->raw_filename, (ufbxi_strblob*)&cache->raw_absolute_filename, (ufbxi_strblob*)&cache->raw_relative_filename, true));
	}

	ufbx_assert(uc->tmp_full_weights.num_items == uc->scene.blend_channels.count);
	ufbx_real_list *full_weights = ufbxi_push_pop(&uc->tmp, &uc->tmp_full_weights, ufbx_real_list, uc->tmp_full_weights.num_items);
	ufbxi_buf_free(&uc->tmp_full_weights);
	ufbxi_check(full_weights);

	ufbxi_for_ptr_list(ufbx_blend_channel, p_channel, uc->scene.blend_channels) {
		ufbx_blend_channel *channel = *p_channel;

		ufbxi_check(ufbxi_fetch_blend_keyframes(uc, &channel->keyframes, &channel->element));

		for (size_t i = 0; i < channel->keyframes.count; i++) {
			ufbx_blend_keyframe *key = &channel->keyframes.data[i];
			if (i < full_weights->count) {
				key->target_weight = full_weights->data[i] / (ufbx_real)100.0;
			} else {
				key->target_weight = 1.0f;
			}
		}

		ufbxi_check(ufbxi_sort_blend_keyframes(uc, channel->keyframes.data, channel->keyframes.count));
		full_weights++;

		if (channel->keyframes.count > 0) {
			channel->target_shape = channel->keyframes.data[channel->keyframes.count - 1].shape;
		}
	}
	ufbxi_buf_free(&uc->tmp_full_weights);

	{
		// Generate and patch procedural index buffers
		uint32_t *zero_indices = ufbxi_push(&uc->result, uint32_t, uc->max_zero_indices);
		uint32_t *consecutive_indices = ufbxi_push(&uc->result, uint32_t, uc->max_consecutive_indices);
		ufbxi_check(zero_indices && consecutive_indices);

		memset(zero_indices, 0, sizeof(uint32_t) * uc->max_zero_indices);
		for (size_t i = 0; i < uc->max_consecutive_indices; i++) {
			consecutive_indices[i] = (uint32_t)i;
		}

		uc->zero_indices = zero_indices;
		uc->consecutive_indices = consecutive_indices;

		ufbxi_for_ptr_list(ufbx_mesh, p_mesh, uc->scene.meshes) {
			ufbx_mesh *mesh = *p_mesh;

			ufbxi_patch_index_pointer(uc, &mesh->vertex_position.indices.data);
			ufbxi_patch_index_pointer(uc, &mesh->vertex_normal.indices.data);
			ufbxi_patch_index_pointer(uc, &mesh->vertex_color.indices.data);
			ufbxi_patch_index_pointer(uc, &mesh->vertex_crease.indices.data);
			ufbxi_patch_index_pointer(uc, &mesh->face_material.data);
			ufbxi_patch_index_pointer(uc, &mesh->face_group.data);

			ufbxi_patch_index_pointer(uc, &mesh->skinned_position.indices.data);
			ufbxi_patch_index_pointer(uc, &mesh->skinned_normal.indices.data);

			ufbxi_for_list(ufbx_uv_set, set, mesh->uv_sets) {
				ufbxi_patch_index_pointer(uc, &set->vertex_uv.indices.data);
				ufbxi_patch_index_pointer(uc, &set->vertex_bitangent.indices.data);
				ufbxi_patch_index_pointer(uc, &set->vertex_tangent.indices.data);
			}

			ufbxi_for_list(ufbx_color_set, set, mesh->color_sets) {
				ufbxi_patch_index_pointer(uc, &set->vertex_color.indices.data);
			}

			// Generate normals if necessary
			if (!mesh->vertex_normal.exists && uc->opts.generate_missing_normals) {
				ufbxi_check(ufbxi_generate_normals(uc, mesh));
			}

			// Assign first UV and color sets as the "canonical" ones
			if (mesh->uv_sets.count > 0) {
				mesh->vertex_uv = mesh->uv_sets.data[0].vertex_uv;
				mesh->vertex_bitangent = mesh->uv_sets.data[0].vertex_bitangent;
				mesh->vertex_tangent = mesh->uv_sets.data[0].vertex_tangent;
			}
			if (mesh->color_sets.count > 0) {
				mesh->vertex_color = mesh->color_sets.data[0].vertex_color;
			}

			if (mesh->face_group_parts.count == 1) {
				ufbxi_patch_index_pointer(uc, &mesh->face_group_parts.data[0].face_indices.data);
			}

			ufbxi_check(ufbxi_fetch_mesh_materials(uc, &mesh->materials, &mesh->element, true));

			// Patch materials to instances if necessary
			if (mesh->materials.count > 0) {
				ufbxi_for_ptr_list(ufbx_node, p_node, mesh->instances) {
					ufbx_node *node = *p_node;
					if (node->materials.count < mesh->materials.count && mesh->materials.data[0] != NULL) {
						ufbx_material **materials = ufbxi_push(&uc->result, ufbx_material*, mesh->materials.count);
						ufbxi_check(materials);
						ufbxi_nounroll for (size_t i = 0; i < node->materials.count; i++) {
							materials[i] = node->materials.data[i];
						}
						ufbxi_nounroll for (size_t i = node->materials.count; i < mesh->materials.count; i++) {
							materials[i] = mesh->materials.data[i];
						}
						node->materials.data = materials;
						node->materials.count = mesh->materials.count;
					}
				}
			}

			if (uc->retain_mesh_parts) {
				size_t num_parts = ufbxi_max_sz(mesh->materials.count, 1);
				mesh->material_parts.data = ufbxi_push_zero(&uc->result, ufbx_mesh_part, num_parts);
				ufbxi_check(mesh->material_parts.data);
				mesh->material_parts.count = num_parts;
			}

			if (mesh->materials.count <= 1) {
				// Use the shared consecutive index buffer for mesh faces if there's only one material
				// See HACK(consecutive-faces) in `ufbxi_read_mesh()`.
				if (mesh->material_parts.count > 0) {
					ufbx_mesh_part *part = &mesh->material_parts.data[0];
					part->num_faces = mesh->num_faces;
					part->num_triangles = mesh->num_triangles;
					part->num_empty_faces = mesh->num_empty_faces;
					part->num_point_faces = mesh->num_point_faces;
					part->num_line_faces = mesh->num_line_faces;
					part->face_indices.data = uc->consecutive_indices;
					part->face_indices.count = mesh->num_faces;
					mesh->material_part_usage_order.data = uc->zero_indices;
					mesh->material_part_usage_order.count = 1;
				}

				if (mesh->materials.count == 1) {
					mesh->face_material.data = uc->zero_indices;
					mesh->face_material.count = mesh->num_faces;
				} else {
					mesh->face_material.data = NULL;
					mesh->face_material.count = 0;
				}
			} else if (mesh->materials.count > 0) {
				ufbxi_check(ufbxi_finalize_mesh_material(&uc->result, &uc->error, mesh));
			}

			// Fetch deformers
			ufbxi_check(ufbxi_fetch_dst_elements(uc, &mesh->skin_deformers, &mesh->element, search_node, true, NULL, UFBX_ELEMENT_SKIN_DEFORMER));
			ufbxi_check(ufbxi_fetch_dst_elements(uc, &mesh->blend_deformers, &mesh->element, search_node, true, NULL, UFBX_ELEMENT_BLEND_DEFORMER));
			ufbxi_check(ufbxi_fetch_dst_elements(uc, &mesh->cache_deformers, &mesh->element, search_node, true, NULL, UFBX_ELEMENT_CACHE_DEFORMER));
			ufbxi_check(ufbxi_fetch_deformers(uc, &mesh->all_deformers, &mesh->element, search_node));

			// Vertex position must always exist if not explicitly allowed to be missing
			if (!mesh->vertex_position.exists && !uc->opts.allow_missing_vertex_position) {
				ufbxi_check(mesh->num_indices == 0);
				mesh->vertex_position.exists = true;
				mesh->vertex_position.unique_per_vertex = true;
				mesh->skinned_position.exists = true;
				mesh->skinned_position.unique_per_vertex = true;
			}

			// Update metadata
			if (mesh->max_face_triangles > uc->scene.metadata.max_face_triangles) {
				uc->scene.metadata.max_face_triangles = mesh->max_face_triangles;
			}
		}
	}

	ufbxi_for_ptr_list(ufbx_stereo_camera, p_stereo, uc->scene.stereo_cameras) {
		ufbx_stereo_camera *stereo = *p_stereo;
		stereo->left = (ufbx_camera*)ufbxi_fetch_dst_element(&stereo->element, search_node, ufbxi_LeftCamera, UFBX_ELEMENT_CAMERA);
		stereo->right = (ufbx_camera*)ufbxi_fetch_dst_element(&stereo->element, search_node, ufbxi_RightCamera, UFBX_ELEMENT_CAMERA);
	}

	ufbxi_for_ptr_list(ufbx_nurbs_curve, p_curve, uc->scene.nurbs_curves) {
		ufbx_nurbs_curve *curve = *p_curve;
		ufbxi_check(ufbxi_finalize_nurbs_basis(uc, &curve->basis));
	}

	ufbxi_for_ptr_list(ufbx_nurbs_surface, p_surface, uc->scene.nurbs_surfaces) {
		ufbx_nurbs_surface *surface = *p_surface;
		ufbxi_check(ufbxi_finalize_nurbs_basis(uc, &surface->basis_u));
		ufbxi_check(ufbxi_finalize_nurbs_basis(uc, &surface->basis_v));

		surface->material = (ufbx_material*)ufbxi_fetch_dst_element(&surface->element, true, NULL, UFBX_ELEMENT_MATERIAL);
	}

	ufbxi_for_ptr_list(ufbx_anim_stack, p_stack, uc->scene.anim_stacks) {
		ufbx_anim_stack *stack = *p_stack;
		ufbxi_check(ufbxi_fetch_dst_elements(uc, &stack->layers, &stack->element, false, true, NULL, UFBX_ELEMENT_ANIM_LAYER));

		ufbxi_check(ufbxi_push_anim(uc, &stack->anim, stack->layers.data, stack->layers.count));
	}

	ufbxi_for_ptr_list(ufbx_anim_layer, p_layer, uc->scene.anim_layers) {
		ufbx_anim_layer *layer = *p_layer;
		ufbxi_check(ufbxi_fetch_dst_elements(uc, &layer->anim_values, &layer->element, false, true, NULL, UFBX_ELEMENT_ANIM_VALUE));

		ufbxi_check(ufbxi_push_anim(uc, &layer->anim, p_layer, 1));

		uint32_t min_id = UINT32_MAX, max_id = 0;

		// Combine the animated properties with elements (potentially duplicates!)
		size_t num_anim_props = 0;
		ufbxi_for_ptr_list(ufbx_anim_value, p_value, layer->anim_values) {
			ufbx_anim_value *value = *p_value;
			ufbxi_for_list(ufbx_connection, ac, value->element.connections_src) {
				if (ac->src_prop.length == 0 && ac->dst_prop.length > 0) {
					ufbx_anim_prop *aprop = ufbxi_push(&uc->tmp_stack, ufbx_anim_prop, 1);
					uint32_t id = ac->dst->element_id;
					min_id = ufbxi_min32(min_id, id);
					max_id = ufbxi_max32(max_id, id);
					uint32_t id_mask = ufbxi_arraycount(layer->_element_id_bitmask) - 1;
					layer->_element_id_bitmask[(id >> 5) & id_mask] |= 1u << (id & 31);
					ufbxi_check(aprop);
					aprop->anim_value = value;
					aprop->element = ac->dst;
					aprop->_internal_key = ufbxi_get_name_key(ac->dst_prop.data, ac->dst_prop.length);
					aprop->prop_name = ac->dst_prop;
					num_anim_props++;
				}
			}
		}

		if (min_id != UINT32_MAX) {
			layer->_min_element_id = min_id;
			layer->_max_element_id = max_id;
		}

		switch (ufbxi_find_int(&layer->props, ufbxi_BlendMode, 0)) {
		case 0: // Additive
			layer->blended = true;
			layer->additive = true;
			break;
		case 1: // Override
			layer->blended = false;
			layer->additive = false;
			break;
		case 2: // Override Passthrough
			layer->blended = true;
			layer->additive = false;
			break;
		default: // Unknown
			layer->blended = false;
			layer->additive = false;
			break;
		}

		ufbx_prop *weight_prop = ufbxi_find_prop(&layer->props, ufbxi_Weight);
		if (weight_prop) {
			layer->weight = weight_prop->value_real / (ufbx_real)100.0;
			if (layer->weight < 0.0f) layer->weight = 0.0f;
			if (layer->weight > 0.99999f) layer->weight = 1.0f;
			layer->weight_is_animated = (weight_prop->flags & UFBX_PROP_FLAG_ANIMATED) != 0;
		} else {
			layer->weight = 1.0f;
			layer->weight_is_animated = false;
		}
		layer->compose_rotation = ufbxi_find_int(&layer->props, ufbxi_RotationAccumulationMode, 0) == 0;
		layer->compose_scale = ufbxi_find_int(&layer->props, ufbxi_ScaleAccumulationMode, 0) == 0;

		// Add a dummy NULL element animated prop at the end so we can iterate
		// animated props without worrying about boundary conditions..
		{
			ufbx_anim_prop *aprop = ufbxi_push_zero(&uc->tmp_stack, ufbx_anim_prop, 1);
			ufbxi_check(aprop);
		}

		layer->anim_props.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_anim_prop, num_anim_props + 1);
		ufbxi_check(layer->anim_props.data);
		layer->anim_props.count = num_anim_props;
		ufbxi_check(ufbxi_sort_anim_props(uc, layer->anim_props.data, layer->anim_props.count));
	}

	ufbxi_for_ptr_list(ufbx_anim_value, p_value, uc->scene.anim_values) {
		ufbx_anim_value *value = *p_value;

		// TODO: Search for things like d|Visibility with a constructed name
		value->default_value.x = ufbxi_find_real(&value->props, ufbxi_X, value->default_value.x);
		value->default_value.x = ufbxi_find_real(&value->props, ufbxi_d_X, value->default_value.x);
		value->default_value.y = ufbxi_find_real(&value->props, ufbxi_Y, value->default_value.y);
		value->default_value.y = ufbxi_find_real(&value->props, ufbxi_d_Y, value->default_value.y);
		value->default_value.z = ufbxi_find_real(&value->props, ufbxi_Z, value->default_value.z);
		value->default_value.z = ufbxi_find_real(&value->props, ufbxi_d_Z, value->default_value.z);

		ufbxi_for_list(ufbx_connection, conn, value->element.connections_dst) {
			if (conn->src->type == UFBX_ELEMENT_ANIM_CURVE && conn->src_prop.length == 0) {
				ufbx_anim_curve *curve = (ufbx_anim_curve*)conn->src;

				uint32_t index = 0;
				const char *name = conn->dst_prop.data;
				if (name == ufbxi_Y || name == ufbxi_d_Y) index = 1;
				if (name == ufbxi_Z || name == ufbxi_d_Z) index = 2;

				ufbx_prop *prop = ufbx_find_prop_len(&value->props, conn->dst_prop.data, conn->dst_prop.length);
				if (prop) {
					value->default_value.v[index] = prop->value_real;
				}
				value->curves[index] = curve;
			}
		}
	}

	ufbxi_for_ptr_list(ufbx_shader, p_shader, uc->scene.shaders) {
		ufbx_shader *shader = *p_shader;
		ufbxi_check(ufbxi_fetch_dst_elements(uc, &shader->bindings, &shader->element, false, false, NULL, UFBX_ELEMENT_SHADER_BINDING));

		ufbx_prop *api = ufbx_find_prop(&shader->props, "RenderAPI");
		if (api) {
			if (!strcmp(api->value_str.data, "ARNOLD_SHADER_ID")) {
				shader->type = UFBX_SHADER_ARNOLD_STANDARD_SURFACE;
			} else if (!strcmp(api->value_str.data, "OSL")) {
				shader->type = UFBX_SHADER_OSL_STANDARD_SURFACE;
			} else if (!strcmp(api->value_str.data, "SFX_PBS_SHADER")) {
				shader->type = UFBX_SHADER_SHADERFX_GRAPH;
			}
		}
	}

	ufbxi_for_ptr_list(ufbx_material, p_material, uc->scene.materials) {
		ufbx_material *material = *p_material;
		material->shader = (ufbx_shader*)ufbxi_fetch_src_element(&material->element, false, NULL, UFBX_ELEMENT_SHADER);

		if (!strcmp(material->shading_model_name.data, "lambert") || !strcmp(material->shading_model_name.data, "Lambert")) {
			material->shader_type = UFBX_SHADER_FBX_LAMBERT;
		} else if (!strcmp(material->shading_model_name.data, "phong") || !strcmp(material->shading_model_name.data, "Phong")) {
			material->shader_type = UFBX_SHADER_FBX_PHONG;
		}

		if (material->shader) {
			material->shader_type = material->shader->type;
		} else {
			if (uc->opts.use_blender_pbr_material && uc->exporter == UFBX_EXPORTER_BLENDER_BINARY && uc->exporter_version >= ufbx_pack_version(4,12,0)) {
				material->shader_type = UFBX_SHADER_BLENDER_PHONG;
			}

			// TODO: Is this too strict?
			if (material->shader_type == UFBX_SHADER_UNKNOWN) {
				uint32_t classid_a = (uint32_t)(uint64_t)ufbx_find_int(&material->props, "3dsMax|ClassIDa", 0);
				uint32_t classid_b = (uint32_t)(uint64_t)ufbx_find_int(&material->props, "3dsMax|ClassIDb", 0);
				if (classid_a == 0x3d6b1cecu && classid_b == 0xdeadc001u) {
					material->shader_type = UFBX_SHADER_3DS_MAX_PHYSICAL_MATERIAL;
					material->shader_prop_prefix.data = "3dsMax|Parameters|";
					material->shader_prop_prefix.length = strlen("3dsMax|Parameters|");
				} else if (classid_a == 0x38420192u && classid_b == 0x45fe4e1bu) {
					material->shader_type = UFBX_SHADER_GLTF_MATERIAL;
					material->shader_prop_prefix.data = "3dsMax|";
					material->shader_prop_prefix.length = strlen("3dsMax|");
				} else if (classid_a == 0xd00f1e00u && classid_b == 0xbe77e500u) {
					material->shader_type = UFBX_SHADER_3DS_MAX_PBR_METAL_ROUGH;
					material->shader_prop_prefix.data = "3dsMax|main|";
					material->shader_prop_prefix.length = strlen("3dsMax|main|");
				} else if (classid_a == 0xd00f1e00u && classid_b == 0x01dbad33u) {
					material->shader_type = UFBX_SHADER_3DS_MAX_PBR_SPEC_GLOSS;
					material->shader_prop_prefix.data = "3dsMax|main|";
					material->shader_prop_prefix.length = strlen("3dsMax|main|");
				}
			}
		}

		ufbxi_check(ufbxi_fetch_textures(uc, &material->textures, &material->element, false));
	}

	// Ugh.. Patch the textures from meshes for legacy LayerElement-style textures
	{
		ufbxi_for_ptr_list(ufbx_mesh, p_mesh, uc->scene.meshes) {
			ufbx_mesh *mesh = *p_mesh;
			size_t num_materials = mesh->materials.count;

			ufbxi_mesh_extra *extra = (ufbxi_mesh_extra*)ufbxi_get_element_extra(uc, mesh->element.element_id);
			if (!extra) continue;
			if (num_materials == 0) continue;

			// TODO: This leaks currently to result, probably doesn't matter..
			ufbx_texture_list textures;
			ufbxi_check(ufbxi_fetch_dst_elements(uc, &textures, &mesh->element, true, false, NULL, UFBX_ELEMENT_TEXTURE));

			size_t num_material_textures = 0;
			ufbxi_for(ufbxi_tmp_mesh_texture, tex, extra->texture_arr, extra->texture_count) {
				if (tex->all_same) {
					int32_t texture_id = tex->num_faces > 0 ? (int32_t)tex->face_texture[0] : 0;
					if (texture_id >= 0 && (size_t)texture_id < textures.count) {
						ufbxi_tmp_material_texture *mat_texs = ufbxi_push(&uc->tmp_stack, ufbxi_tmp_material_texture, num_materials);
						ufbxi_check(mat_texs);
						num_material_textures += num_materials;
						for (size_t i = 0; i < num_materials; i++) {
							mat_texs[i].material_id = (int32_t)i;
							mat_texs[i].texture_id = texture_id;
							mat_texs[i].prop_name = tex->prop_name;
						}
					}
				} else if (mesh->face_material.count) {
					size_t num_faces = ufbxi_min_sz(tex->num_faces, mesh->num_faces);
					int32_t prev_material = -1;
					int32_t prev_texture = -1;
					for (size_t i = 0; i < num_faces; i++) {
						int32_t texture_id = (int32_t)tex->face_texture[i];
						int32_t material_id = (int32_t)mesh->face_material.data[i];
						if (texture_id < 0 || (size_t)texture_id >= textures.count) continue;
						if (material_id < 0 || (size_t)material_id >= num_materials) continue;
						if (material_id == prev_material && texture_id == prev_texture) continue;
						prev_material = material_id;
						prev_texture = texture_id;

						ufbxi_tmp_material_texture *mat_tex = ufbxi_push(&uc->tmp_stack, ufbxi_tmp_material_texture, 1);
						ufbxi_check(mat_tex);
						mat_tex->material_id = material_id;
						mat_tex->texture_id = texture_id;
						mat_tex->prop_name = tex->prop_name;
						num_material_textures++;
					}
				}
			}

			// Push a sentinel material texture to the end so we don't need to
			// duplicate the material texture flushing code twice.
			{
				ufbxi_tmp_material_texture *mat_tex = ufbxi_push(&uc->tmp_stack, ufbxi_tmp_material_texture, 1);
				ufbxi_check(mat_tex);
				mat_tex->material_id = -1;
				mat_tex->texture_id = -1;
				mat_tex->prop_name = ufbx_empty_string;
			}

			ufbxi_tmp_material_texture *mat_texs = ufbxi_push_pop(&uc->tmp, &uc->tmp_stack, ufbxi_tmp_material_texture, num_material_textures + 1);
			ufbxi_check(mat_texs);
			ufbxi_check(ufbxi_sort_tmp_material_textures(uc, mat_texs, num_material_textures));

			int32_t prev_material = -2;
			int32_t prev_texture = -2;
			const char *prev_prop = NULL;
			size_t num_textures_in_material = 0;
			for (size_t i = 0; i < num_material_textures + 1; i++) {
				ufbxi_tmp_material_texture mat_tex = mat_texs[i];
				if (mat_tex.material_id != prev_material) {
					if (prev_material >= 0 && num_textures_in_material > 0) {
						ufbx_material *mat = mesh->materials.data[prev_material];
						if (mat && mat->textures.count == 0) {
							ufbx_material_texture *texs = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_material_texture, num_textures_in_material);
							ufbxi_check(texs);
							mat->textures.data = texs;
							mat->textures.count = num_textures_in_material;
						} else {
							ufbxi_pop(&uc->tmp_stack, ufbx_material_texture, num_textures_in_material, NULL);
						}
					}

					if (mat_tex.material_id < 0) break;
					prev_material = mat_tex.material_id;
					prev_texture = -1;
					prev_prop = NULL;
					num_textures_in_material = 0;
				}
				if (mat_tex.texture_id == prev_texture && mat_tex.prop_name.data == prev_prop) continue;
				prev_texture = mat_tex.texture_id;
				prev_prop = mat_tex.prop_name.data;

				ufbx_material_texture *tex = ufbxi_push(&uc->tmp_stack, ufbx_material_texture, 1);
				ufbxi_check(tex);
				ufbx_assert(prev_texture >= 0 && (size_t)prev_texture < textures.count);
				tex->texture = textures.data[prev_texture];
				tex->shader_prop = tex->material_prop = mat_tex.prop_name;
				num_textures_in_material++;
			}
		}
	}

	ufbxi_check(ufbxi_resolve_file_content(uc));

	ufbxi_for_ptr_list(ufbx_texture, p_texture, uc->scene.textures) {
		ufbx_texture *texture = *p_texture;
		ufbxi_texture_extra *extra = (ufbxi_texture_extra*)ufbxi_get_element_extra(uc, texture->element.element_id);

		ufbx_prop *uv_set = ufbxi_find_prop(&texture->props, ufbxi_UVSet);
		if (uv_set) {
			texture->uv_set = uv_set->value_str;
		} else {
			texture->uv_set = ufbx_empty_string;
		}

		texture->video = (ufbx_video*)ufbxi_fetch_dst_element(&texture->element, false, NULL, UFBX_ELEMENT_VIDEO);
		if (texture->video) {
			texture->content = texture->video->content;
		}

		ufbxi_check(ufbxi_finalize_shader_texture(uc, texture));

		ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&texture->filename, (ufbxi_strblob*)&texture->absolute_filename, (ufbxi_strblob*)&texture->relative_filename, false));
		ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&texture->raw_filename, (ufbxi_strblob*)&texture->raw_absolute_filename, (ufbxi_strblob*)&texture->raw_relative_filename, true));

		// Fetch layered texture layers and patch alphas/blend modes
		if (texture->type == UFBX_TEXTURE_LAYERED) {
			ufbxi_check(ufbxi_fetch_texture_layers(uc, &texture->layers, &texture->element));
			if (extra) {
				for (size_t i = 0, num = ufbxi_min_sz(extra->num_alphas, texture->layers.count); i < num; i++) {
					texture->layers.data[i].alpha = extra->alphas[i];
				}
				for (size_t i = 0, num = ufbxi_min_sz(extra->num_blend_modes, texture->layers.count); i < num; i++) {
					int32_t mode = extra->blend_modes[i];
					if (mode >= 0 && mode < UFBX_BLEND_OVERLAY) {
						texture->layers.data[i].blend_mode = (ufbx_blend_mode)mode;
					}
				}
			}
		}

		ufbxi_check(ufbxi_insert_texture_file(uc, texture));
	}

	ufbxi_propagate_main_textures(&uc->scene);
	ufbxi_check(ufbxi_pop_texture_files(uc));

	// Second pass to fetch material maps
	ufbxi_for_ptr_list(ufbx_material, p_material, uc->scene.materials) {
		ufbx_material *material = *p_material;

		ufbxi_check(ufbxi_sort_material_textures(uc, material->textures.data, material->textures.count));
		ufbxi_fetch_maps(&uc->scene, material);

		// Fetch `ufbx_material_texture.shader_prop` names
		if (material->shader) {
			ufbxi_for_ptr_list(ufbx_shader_binding, p_binding, material->shader->bindings) {
				ufbx_shader_binding *binding = *p_binding;

				ufbxi_for_list(ufbx_shader_prop_binding, prop, binding->prop_bindings) {
					ufbx_string name = prop->material_prop;

					size_t index = SIZE_MAX;
					ufbxi_macro_lower_bound_eq(ufbx_material_texture, 4, &index, material->textures.data, 0, material->textures.count,
						( ufbxi_str_less(a->material_prop, name) ), ( a->material_prop.data == name.data ));
					for (; index < material->textures.count && material->textures.data[index].shader_prop.data == name.data; index++) {
						material->textures.data[index].shader_prop = prop->shader_prop;
					}
				}
			}
		}
	}

	ufbxi_for_ptr_list(ufbx_display_layer, p_layer, uc->scene.display_layers) {
		ufbx_display_layer *layer = *p_layer;
		ufbxi_check(ufbxi_fetch_dst_elements(uc, &layer->nodes, &layer->element, false, true, NULL, UFBX_ELEMENT_NODE));
	}

	ufbxi_for_ptr_list(ufbx_selection_set, p_set, uc->scene.selection_sets) {
		ufbx_selection_set *set = *p_set;
		ufbxi_check(ufbxi_fetch_dst_elements(uc, &set->nodes, &set->element, false, true, NULL, UFBX_ELEMENT_SELECTION_NODE));
	}

	ufbxi_for_ptr_list(ufbx_selection_node, p_node, uc->scene.selection_nodes) {
		ufbx_selection_node *node = *p_node;
		node->target_node = (ufbx_node*)ufbxi_fetch_dst_element(&node->element, false, NULL, UFBX_ELEMENT_NODE);
		node->target_mesh = (ufbx_mesh*)ufbxi_fetch_dst_element(&node->element, false, NULL, UFBX_ELEMENT_MESH);
		if (!node->target_mesh && node->target_node) {
			node->target_mesh = node->target_node->mesh;
		} else if (!node->target_node && node->target_mesh && node->target_mesh->instances.count > 0) {
			node->target_node = node->target_mesh->instances.data[0];
		}

		ufbx_mesh *mesh = node->target_mesh;
		if (mesh) {
			ufbxi_check(ufbxi_validate_indices(uc, &node->vertices, mesh->num_vertices));
			ufbxi_check(ufbxi_validate_indices(uc, &node->edges, mesh->num_edges));
			ufbxi_check(ufbxi_validate_indices(uc, &node->faces, mesh->num_faces));
		}
	}

	ufbxi_for_ptr_list(ufbx_constraint, p_constraint, uc->scene.constraints) {
		ufbx_constraint *constraint = *p_constraint;

		size_t tmp_base = uc->tmp_stack.num_items;

		// Find property connections in _both_ src and dst connections as they are inconsistent
		// in pre-7000 files. For example "Constrained Object" is a "PO" connection in 6100.
		ufbxi_for_list(ufbx_connection, conn, constraint->element.connections_src) {
			if (conn->src_prop.length == 0 || conn->dst->type != UFBX_ELEMENT_NODE) continue;
			ufbxi_check(ufbxi_add_constraint_prop(uc, constraint, (ufbx_node*)conn->dst, conn->src_prop.data));
		}
		ufbxi_for_list(ufbx_connection, conn, constraint->element.connections_dst) {
			if (conn->dst_prop.length == 0 || conn->src->type != UFBX_ELEMENT_NODE) continue;
			ufbxi_check(ufbxi_add_constraint_prop(uc, constraint, (ufbx_node*)conn->src, conn->dst_prop.data));
		}

		size_t num_targets = uc->tmp_stack.num_items - tmp_base;
		constraint->targets.count = num_targets;
		constraint->targets.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_constraint_target, num_targets);
		ufbxi_check(constraint->targets.data);
	}

	ufbxi_for_ptr_list(ufbx_audio_layer, p_layer, uc->scene.audio_layers) {
		ufbx_audio_layer *layer = *p_layer;
		ufbxi_check(ufbxi_fetch_dst_elements(uc, &layer->clips, &layer->element, false, true, NULL, UFBX_ELEMENT_AUDIO_CLIP));
	}

	ufbxi_for_ptr_list(ufbx_lod_group, p_lod, uc->scene.lod_groups) {
		ufbxi_check(ufbxi_finalize_lod_group(uc, *p_lod));
	}

	ufbxi_check(ufbxi_fetch_file_textures(uc));

	// NOTE: This will be patched over in `ufbxi_update_scene()` if there are `anim_layers`
	if (uc->scene.anim_layers.count == 0) {
		ufbxi_check(ufbxi_push_anim(uc, &uc->scene.anim, NULL, 0));
	}

	uc->scene.metadata.ktime_second = uc->ktime_sec;

	// Maya seems to use scale of 100/3, Blender binary uses exactly 33, ASCII has always value of 1.0
	if (uc->version < 6000) {
		uc->scene.metadata.bone_prop_size_unit = 1.0f;
	} else if (uc->exporter == UFBX_EXPORTER_BLENDER_BINARY) {
		uc->scene.metadata.bone_prop_size_unit = 33.0f;
	} else if (uc->exporter == UFBX_EXPORTER_BLENDER_ASCII) {
		uc->scene.metadata.bone_prop_size_unit = 1.0f;
	} else {
		uc->scene.metadata.bone_prop_size_unit = (ufbx_real)(100.0/3.0);
	}
	if (uc->exporter == UFBX_EXPORTER_BLENDER_ASCII) {
		uc->scene.metadata.bone_prop_limb_length_relative = false;
	} else {
		uc->scene.metadata.bone_prop_limb_length_relative = true;
	}

	return 1;
}

// -- Interpret the read scene

static ufbxi_forceinline void ufbxi_add_translate(ufbx_transform *t, ufbx_vec3 v)
{
	t->translation.x += v.x;
	t->translation.y += v.y;
	t->translation.z += v.z;
}

static ufbxi_forceinline void ufbxi_sub_translate(ufbx_transform *t, ufbx_vec3 v)
{
	t->translation.x -= v.x;
	t->translation.y -= v.y;
	t->translation.z -= v.z;
}

static ufbxi_forceinline void ufbxi_mul_scale(ufbx_transform *t, ufbx_vec3 v)
{
	t->translation.x *= v.x;
	t->translation.y *= v.y;
	t->translation.z *= v.z;
	t->scale.x *= v.x;
	t->scale.y *= v.y;
	t->scale.z *= v.z;
}

static ufbxi_forceinline void ufbxi_mul_scale_real(ufbx_transform *t, ufbx_real v)
{
	t->translation.x *= v;
	t->translation.y *= v;
	t->translation.z *= v;
	t->scale.x *= v;
	t->scale.y *= v;
	t->scale.z *= v;
}

static ufbxi_noinline ufbx_quat ufbxi_mul_quat(ufbx_quat a, ufbx_quat b)
{
	ufbx_quat r;
	r.x = a.w*b.x + a.x*b.w + a.y*b.z - a.z*b.y;
	r.y = a.w*b.y - a.x*b.z + a.y*b.w + a.z*b.x;
	r.z = a.w*b.z + a.x*b.y - a.y*b.x + a.z*b.w;
	r.w = a.w*b.w - a.x*b.x - a.y*b.y - a.z*b.z;
	return r;
}

static ufbxi_forceinline void ufbxi_add_weighted_vec3(ufbx_vec3 *r, ufbx_vec3 b, ufbx_real w)
{
	r->x += b.x * w;
	r->y += b.y * w;
	r->z += b.z * w;
}

static ufbxi_forceinline void ufbxi_add_weighted_quat(ufbx_quat *r, ufbx_quat b, ufbx_real w)
{
	r->x += b.x * w;
	r->y += b.y * w;
	r->z += b.z * w;
	r->w += b.w * w;
}

static ufbxi_noinline void ufbxi_add_weighted_mat(ufbx_matrix *r, const ufbx_matrix *b, ufbx_real w)
{
	ufbxi_add_weighted_vec3(&r->cols[0], b->cols[0], w);
	ufbxi_add_weighted_vec3(&r->cols[1], b->cols[1], w);
	ufbxi_add_weighted_vec3(&r->cols[2], b->cols[2], w);
	ufbxi_add_weighted_vec3(&r->cols[3], b->cols[3], w);
}

static void ufbxi_mul_rotate(ufbx_transform *t, ufbx_vec3 v, ufbx_rotation_order order)
{
	if (ufbxi_is_vec3_zero(v)) return;

	ufbx_quat q = ufbx_euler_to_quat(v, order);
	if (t->rotation.w != 1.0) {
		t->rotation = ufbxi_mul_quat(q, t->rotation);
	} else {
		t->rotation = q;
	}

	if (!ufbxi_is_vec3_zero(t->translation)) {
		t->translation = ufbx_quat_rotate_vec3(q, t->translation);
	}
}

static void ufbxi_mul_rotate_quat(ufbx_transform *t, ufbx_quat q)
{
	if (ufbxi_is_quat_identity(q)) return;

	if (t->rotation.w != 1.0) {
		t->rotation = ufbxi_mul_quat(q, t->rotation);
	} else {
		t->rotation = q;
	}

	if (!ufbxi_is_vec3_zero(t->translation)) {
		t->translation = ufbx_quat_rotate_vec3(q, t->translation);
	}
}

static void ufbxi_mul_inv_rotate(ufbx_transform *t, ufbx_vec3 v, ufbx_rotation_order order)
{
	if (ufbxi_is_vec3_zero(v)) return;

	ufbx_quat q = ufbx_euler_to_quat(v, order);
	q.x = -q.x; q.y = -q.y; q.z = -q.z;
	if (t->rotation.w != 1.0) {
		t->rotation = ufbxi_mul_quat(q, t->rotation);
	} else {
		t->rotation = q;
	}

	if (!ufbxi_is_vec3_zero(t->translation)) {
		t->translation = ufbx_quat_rotate_vec3(q, t->translation);
	}
}

// -- Updating state from properties

ufbxi_forceinline static void ufbxi_mirror_translation(ufbx_vec3 *p_vec, ufbx_mirror_axis axis)
{
	ufbxi_dev_assert(axis);
	p_vec->v[axis - 1] = -p_vec->v[axis - 1];
}

ufbxi_forceinline static void ufbxi_mirror_rotation(ufbx_quat *p_quat, ufbx_mirror_axis axis)
{
	ufbxi_dev_assert(axis);
	p_quat->v[axis % 3] = -p_quat->v[axis % 3];
	p_quat->v[(axis + 1) % 3] = -p_quat->v[(axis + 1) % 3];
}

ufbxi_noinline static ufbx_transform ufbxi_get_geometry_transform(const ufbx_props *props, ufbx_node *node)
{
	ufbx_vec3 translation = ufbxi_find_vec3(props, ufbxi_GeometricTranslation, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 rotation = ufbxi_find_vec3(props, ufbxi_GeometricRotation, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 scaling = ufbxi_find_vec3(props, ufbxi_GeometricScaling, 1.0f, 1.0f, 1.0f);

	ufbx_transform t = { { 0,0,0 }, { 0,0,0,1 }, { 1,1,1 }};

	// WorldTransform = ParentWorldTransform * T * R * S * (OT * OR * OS)

	ufbxi_mul_scale(&t, scaling);
	ufbxi_mul_rotate(&t, rotation, UFBX_ROTATION_ORDER_XYZ);
	ufbxi_add_translate(&t, translation);

	if (node->has_adjust_transform) {
		t.translation.x *= node->adjust_translation_scale;
		t.translation.y *= node->adjust_translation_scale;
		t.translation.z *= node->adjust_translation_scale;
	}

	if (node->adjust_mirror_axis) {
		ufbxi_mirror_translation(&t.translation, node->adjust_mirror_axis);
		ufbxi_mirror_rotation(&t.rotation, node->adjust_mirror_axis);
	}

	return t;
}

ufbxi_noinline static ufbx_transform ufbxi_get_transform(const ufbx_props *props, ufbx_rotation_order order, const ufbx_node *node, const ufbx_vec3 *translation_scale)
{
	ufbx_vec3 scale_pivot = ufbxi_find_vec3(props, ufbxi_ScalingPivot, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 rot_pivot = ufbxi_find_vec3(props, ufbxi_RotationPivot, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 scale_offset = ufbxi_find_vec3(props, ufbxi_ScalingOffset, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 rot_offset = ufbxi_find_vec3(props, ufbxi_RotationOffset, 0.0f, 0.0f, 0.0f);

	ufbx_vec3 translation = ufbxi_find_vec3(props, ufbxi_Lcl_Translation, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 rotation = ufbxi_find_vec3(props, ufbxi_Lcl_Rotation, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 scaling = ufbxi_find_vec3(props, ufbxi_Lcl_Scaling, 1.0f, 1.0f, 1.0f);

	ufbx_vec3 pre_rotation = ufbxi_find_vec3(props, ufbxi_PreRotation, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 post_rotation = ufbxi_find_vec3(props, ufbxi_PostRotation, 0.0f, 0.0f, 0.0f);

	ufbx_transform t = { { 0,0,0 }, { 0,0,0,1 }, { 1,1,1 }};

	// WorldTransform = ParentWorldTransform * T * Roff * Rp * Rpre * R * Rpost * Rp-1 * Soff * Sp * S * Sp-1
	// NOTE: Rpost is inverted (!) after converting from PostRotation Euler angles

	if (translation_scale) {
		translation.x *= translation_scale->x;
		translation.y *= translation_scale->y;
		translation.z *= translation_scale->z;
	}

	if (node->has_adjust_transform) {
		ufbxi_mul_rotate_quat(&t, node->adjust_post_rotation);
		ufbxi_mul_scale_real(&t, node->adjust_post_scale);
	}

	ufbxi_sub_translate(&t, scale_pivot);
	ufbxi_mul_scale(&t, scaling);
	ufbxi_add_translate(&t, scale_pivot);

	ufbxi_add_translate(&t, scale_offset);

	ufbxi_sub_translate(&t, rot_pivot);
	ufbxi_mul_inv_rotate(&t, post_rotation, UFBX_ROTATION_ORDER_XYZ);
	ufbxi_mul_rotate(&t, rotation, order);
	ufbxi_mul_rotate(&t, pre_rotation, UFBX_ROTATION_ORDER_XYZ);
	ufbxi_add_translate(&t, rot_pivot);

	ufbxi_add_translate(&t, rot_offset);

	ufbxi_add_translate(&t, translation);

	if (node->has_adjust_transform) {
		ufbxi_add_translate(&t, node->adjust_pre_translation);
		ufbxi_mul_rotate_quat(&t, node->adjust_pre_rotation);
		ufbxi_mul_scale_real(&t, node->adjust_pre_scale);
		t.translation.x *= node->adjust_translation_scale;
		t.translation.y *= node->adjust_translation_scale;
		t.translation.z *= node->adjust_translation_scale;
	}

	if (node->adjust_mirror_axis) {
		ufbxi_mirror_translation(&t.translation, node->adjust_mirror_axis);
		ufbxi_mirror_rotation(&t.rotation, node->adjust_mirror_axis);
	}

	return t;
}

ufbxi_noinline static ufbx_quat ufbxi_get_rotation(const ufbx_props *props, ufbx_rotation_order order, const ufbx_node *node)
{
	ufbx_vec3 rotation = ufbxi_find_vec3(props, ufbxi_Lcl_Rotation, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 pre_rotation = ufbxi_find_vec3(props, ufbxi_PreRotation, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 post_rotation = ufbxi_find_vec3(props, ufbxi_PostRotation, 0.0f, 0.0f, 0.0f);

	ufbx_transform t = { { 0,0,0 }, { 0,0,0,1 }, { 1,1,1 }};

	if (node->has_adjust_transform) {
		ufbxi_mul_rotate_quat(&t, node->adjust_post_rotation);
	}

	ufbxi_mul_inv_rotate(&t, post_rotation, UFBX_ROTATION_ORDER_XYZ);
	ufbxi_mul_rotate(&t, rotation, order);
	ufbxi_mul_rotate(&t, pre_rotation, UFBX_ROTATION_ORDER_XYZ);

	if (node->has_adjust_transform) {
		ufbxi_mul_rotate_quat(&t, node->adjust_pre_rotation);
	}

	if (node->adjust_mirror_axis) {
		ufbxi_mirror_rotation(&t.rotation, node->adjust_mirror_axis);
	}

	return t.rotation;
}

ufbxi_noinline static ufbx_vec3 ufbxi_get_scale(const ufbx_props *props, const ufbx_node *node)
{
	ufbx_vec3 scaling = ufbxi_find_vec3(props, ufbxi_Lcl_Scaling, 1.0f, 1.0f, 1.0f);

	ufbx_transform t = { { 0,0,0 }, { 0,0,0,1 }, { 1,1,1 }};

	if (node->has_adjust_transform) {
		ufbxi_mul_scale_real(&t, node->adjust_post_scale);
	}

	ufbxi_mul_scale(&t, scaling);

	if (node->has_adjust_transform) {
		ufbxi_mul_scale_real(&t, node->adjust_pre_scale);
	}

	return t.scale;
}

ufbxi_noinline static ufbx_transform ufbxi_get_texture_transform(const ufbx_props *props)
{
	ufbx_vec3 scale_pivot = ufbxi_find_vec3(props, ufbxi_TextureScalingPivot, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 rot_pivot = ufbxi_find_vec3(props, ufbxi_TextureRotationPivot, 0.0f, 0.0f, 0.0f);

	ufbx_vec3 translation = ufbxi_find_vec3(props, ufbxi_Translation, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 rotation = ufbxi_find_vec3(props, ufbxi_Rotation, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 scaling = ufbxi_find_vec3(props, ufbxi_Scaling, 1.0f, 1.0f, 1.0f);

	ufbx_transform t = { { 0,0,0 }, { 0,0,0,1 }, { 1,1,1 }};

	ufbxi_sub_translate(&t, scale_pivot);
	ufbxi_mul_scale(&t, scaling);
	ufbxi_add_translate(&t, scale_pivot);

	ufbxi_sub_translate(&t, rot_pivot);
	ufbxi_mul_rotate(&t, rotation, UFBX_ROTATION_ORDER_XYZ);
	ufbxi_add_translate(&t, rot_pivot);

	ufbxi_add_translate(&t, translation);

	if (ufbxi_find_int(props, ufbxi_UVSwap, 0) != 0) {
		const ufbx_vec3 swap_scale = { -1.0f, 0.0f, 0.0f };
		const ufbx_vec3 swap_rotate = { 0.0f, 0.0f, -90.0f };
		ufbxi_mul_scale(&t, swap_scale);
		ufbxi_mul_rotate(&t, swap_rotate, UFBX_ROTATION_ORDER_XYZ);
	}

	return t;
}

ufbxi_noinline static ufbx_transform ufbxi_get_constraint_transform(const ufbx_props *props)
{
	ufbx_vec3 translation = ufbxi_find_vec3(props, ufbxi_Translation, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 rotation = ufbxi_find_vec3(props, ufbxi_Rotation, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 rotation_offset = ufbxi_find_vec3(props, ufbxi_RotationOffset, 0.0f, 0.0f, 0.0f);
	ufbx_vec3 scaling = ufbxi_find_vec3(props, ufbxi_Scaling, 1.0f, 1.0f, 1.0f);

	ufbx_transform t = { { 0,0,0 }, { 0,0,0,1 }, { 1,1,1 }};

	ufbxi_mul_scale(&t, scaling);
	ufbxi_mul_rotate(&t, rotation, UFBX_ROTATION_ORDER_XYZ);
	ufbxi_mul_rotate(&t, rotation_offset, UFBX_ROTATION_ORDER_XYZ);
	ufbxi_add_translate(&t, translation);

	return t;
}

ufbxi_noinline static void ufbxi_update_node(ufbx_node *node, const ufbx_transform_override *overrides, size_t num_overrides)
{
	node->rotation_order = (ufbx_rotation_order)ufbxi_find_enum(&node->props, ufbxi_RotationOrder, UFBX_ROTATION_ORDER_XYZ, UFBX_ROTATION_ORDER_SPHERIC);
	node->euler_rotation = ufbxi_find_vec3(&node->props, ufbxi_Lcl_Rotation, 0.0f, 0.0f, 0.0f);

	if (!node->is_root) {
		const ufbx_vec3 *transform_scale = NULL;
		if (node->parent && node->parent->scale_helper) {
			transform_scale = &node->parent->scale_helper->local_transform.scale;
		}
		node->local_transform = ufbxi_get_transform(&node->props, node->rotation_order, node, transform_scale);
		if (node->is_scale_helper && node->parent && node->parent->inherit_scale_node) {
			ufbx_node *scale_parent = node->parent->inherit_scale_node;
			if (scale_parent->scale_helper) {
				ufbx_vec3 inherit_scale = scale_parent->scale_helper->local_transform.scale;
				node->local_transform.scale.x *= inherit_scale.x;
				node->local_transform.scale.y *= inherit_scale.y;
				node->local_transform.scale.z *= inherit_scale.z;
			}
		}

		if (num_overrides > 0) {
			uint32_t typed_id = node->typed_id;
			size_t override_ix = SIZE_MAX;
			ufbxi_macro_lower_bound_eq(ufbx_transform_override, 16, &override_ix, overrides, 0, num_overrides,
				( a->node_id < typed_id ), ( a->node_id == typed_id ));
			if (override_ix != SIZE_MAX) {
				node->local_transform = overrides[override_ix].transform;
			}
		}
		node->node_to_parent = ufbx_transform_to_matrix(&node->local_transform);
		node->geometry_transform = ufbxi_get_geometry_transform(&node->props, node);
	} else {
		node->geometry_transform = ufbx_identity_transform;
	}

	ufbx_matrix unscaled_node_to_parent = ufbxi_unscaled_transform_to_matrix(&node->local_transform);

	node->inherit_scale = node->local_transform.scale;

	ufbx_node *parent = node->parent;
	if (parent) {
		if (node->inherit_mode == UFBX_INHERIT_MODE_NORMAL) {
			node->node_to_world = ufbx_matrix_mul(&parent->node_to_world, &node->node_to_parent);
			node->unscaled_node_to_world = ufbx_matrix_mul(&parent->node_to_world, &unscaled_node_to_parent);
		} else {
			ufbx_transform transform = node->local_transform;

			ufbx_vec3 parent_scale = ufbxi_one_vec3;
			if (node->inherit_scale_node) {
				parent_scale = node->inherit_scale_node->inherit_scale;
			}

			transform.scale.x *= parent_scale.x;
			transform.scale.y *= parent_scale.y;
			transform.scale.z *= parent_scale.z;
			transform.translation.x *= parent->inherit_scale.x;
			transform.translation.y *= parent->inherit_scale.y;
			transform.translation.z *= parent->inherit_scale.z;

			ufbx_matrix node_to_unscaled_parent = ufbx_transform_to_matrix(&transform);
			ufbx_matrix unscaled_node_to_unscaled_parent = ufbxi_unscaled_transform_to_matrix(&transform);

			node->inherit_scale = transform.scale;
			node->node_to_world = ufbx_matrix_mul(&parent->unscaled_node_to_world, &node_to_unscaled_parent);
			node->unscaled_node_to_world = ufbx_matrix_mul(&parent->unscaled_node_to_world, &unscaled_node_to_unscaled_parent);
		}
	} else {
		node->node_to_world = node->node_to_parent;
		node->unscaled_node_to_world = unscaled_node_to_parent;
	}

	if (!ufbxi_is_transform_identity(&node->geometry_transform)) {
		node->geometry_to_node = ufbx_transform_to_matrix(&node->geometry_transform);
		node->geometry_to_world = ufbx_matrix_mul(&node->node_to_world, &node->geometry_to_node);
		node->has_geometry_transform = true;
	} else {
		node->geometry_to_node = ufbx_identity_matrix;
		node->geometry_to_world = node->node_to_world;
		node->has_geometry_transform = false;
	}

	node->visible = ufbxi_find_int(&node->props, ufbxi_Visibility, 1) != 0;
}

ufbxi_noinline static void ufbxi_update_light(ufbx_light *light)
{
	// NOTE: FBX seems to store intensities 100x of what's specified in at least
	// Maya and Blender, should there be a quirks mode to not do this for specific
	// exporters. Does the FBX SDK do this transparently as well?
	light->intensity = ufbxi_find_real(&light->props, ufbxi_Intensity, (ufbx_real)100.0) / (ufbx_real)100.0;

	light->color = ufbxi_find_vec3(&light->props, ufbxi_Color, 1.0f, 1.0f, 1.0f);
	light->type = (ufbx_light_type)ufbxi_find_enum(&light->props, ufbxi_LightType, 0, UFBX_LIGHT_VOLUME);
	light->decay = (ufbx_light_decay)ufbxi_find_enum(&light->props, ufbxi_DecayType, UFBX_LIGHT_DECAY_NONE, UFBX_LIGHT_DECAY_CUBIC);
	light->area_shape = (ufbx_light_area_shape)ufbxi_find_enum(&light->props, ufbxi_AreaLightShape, 0, UFBX_LIGHT_AREA_SHAPE_SPHERE);
	light->inner_angle = ufbxi_find_real(&light->props, ufbxi_HotSpot, 0.0f);
	light->inner_angle = ufbxi_find_real(&light->props, ufbxi_InnerAngle, light->inner_angle);
	light->outer_angle = ufbxi_find_real(&light->props, ufbxi_Cone_angle, 0.0f);
	light->outer_angle = ufbxi_find_real(&light->props, ufbxi_ConeAngle, light->outer_angle);
	light->outer_angle = ufbxi_find_real(&light->props, ufbxi_OuterAngle, light->outer_angle);
	light->cast_light = ufbxi_find_int(&light->props, ufbxi_CastLight, 1) != 0;
	light->cast_shadows = ufbxi_find_int(&light->props, ufbxi_CastShadows, 0) != 0;
}

typedef struct {
	// 1/1000 decimal fixed point for size
	uint16_t film_size_x, film_size_y;
} ufbxi_aperture_format;

static const ufbxi_aperture_format ufbxi_aperture_formats[] = {
	{ 1000, 1000, }, // UFBX_APERTURE_FORMAT_CUSTOM
	{  404,  295, }, // UFBX_APERTURE_FORMAT_16MM_THEATRICAL
	{  493,  292, }, // UFBX_APERTURE_FORMAT_SUPER_16MM
	{  864,  630, }, // UFBX_APERTURE_FORMAT_35MM_ACADEMY
	{  816,  612, }, // UFBX_APERTURE_FORMAT_35MM_TV_PROJECTION
	{  980,  735, }, // UFBX_APERTURE_FORMAT_35MM_FULL_APERTURE
	{  825,  446, }, // UFBX_APERTURE_FORMAT_35MM_185_PROJECTION
	{  864,  732, }, // UFBX_APERTURE_FORMAT_35MM_ANAMORPHIC
	{ 2066,  906, }, // UFBX_APERTURE_FORMAT_70MM_PROJECTION
	{ 1485,  991, }, // UFBX_APERTURE_FORMAT_VISTAVISION
	{ 2080, 1480, }, // UFBX_APERTURE_FORMAT_DYNAVISION
	{ 2772, 2072, }, // UFBX_APERTURE_FORMAT_IMAX
};

ufbxi_noinline static void ufbxi_update_camera(ufbx_scene *scene, ufbx_camera *camera)
{
	camera->projection_mode = (ufbx_projection_mode)ufbxi_find_enum(&camera->props, ufbxi_CameraProjectionType, 0, UFBX_PROJECTION_MODE_ORTHOGRAPHIC);
	camera->aspect_mode = (ufbx_aspect_mode)ufbxi_find_enum(&camera->props, ufbxi_AspectRatioMode, 0, UFBX_ASPECT_MODE_FIXED_HEIGHT);
	camera->aperture_mode = (ufbx_aperture_mode)ufbxi_find_enum(&camera->props, ufbxi_ApertureMode, UFBX_APERTURE_MODE_VERTICAL, UFBX_APERTURE_MODE_FOCAL_LENGTH);
	camera->aperture_format = (ufbx_aperture_format)ufbxi_find_enum(&camera->props, ufbxi_ApertureFormat, UFBX_APERTURE_FORMAT_CUSTOM, UFBX_APERTURE_FORMAT_IMAX);
	camera->gate_fit = (ufbx_gate_fit)ufbxi_find_enum(&camera->props, ufbxi_GateFit, 0, UFBX_GATE_FIT_STRETCH);

	camera->near_plane = ufbxi_find_real(&camera->props, ufbxi_NearPlane, 0.0f);
	camera->far_plane = ufbxi_find_real(&camera->props, ufbxi_FarPlane, 0.0f);

	// Search both W/H and Width/Height but prefer the latter
	ufbx_real aspect_x = ufbxi_find_real(&camera->props, ufbxi_AspectW, 0.0f);
	ufbx_real aspect_y = ufbxi_find_real(&camera->props, ufbxi_AspectH, 0.0f);
	aspect_x = ufbxi_find_real(&camera->props, ufbxi_AspectWidth, aspect_x);
	aspect_y = ufbxi_find_real(&camera->props, ufbxi_AspectHeight, aspect_y);

	ufbx_real fov = ufbxi_find_real(&camera->props, ufbxi_FieldOfView, 0.0f);
	ufbx_real fov_x = ufbxi_find_real(&camera->props, ufbxi_FieldOfViewX, 0.0f);
	ufbx_real fov_y = ufbxi_find_real(&camera->props, ufbxi_FieldOfViewY, 0.0f);

	ufbx_real focal_length = ufbxi_find_real(&camera->props, ufbxi_FocalLength, 0.0f);
	ufbx_real ortho_extent = scene->metadata.ortho_size_unit * ufbxi_find_real(&camera->props, ufbxi_OrthoZoom, 1.0f);

	ufbxi_aperture_format format = ufbxi_aperture_formats[camera->aperture_format];
	ufbx_vec2 film_size = { (ufbx_real)format.film_size_x * (ufbx_real)0.001, (ufbx_real)format.film_size_y * (ufbx_real)0.001 };
	ufbx_real squeeze_ratio = camera->aperture_format == UFBX_APERTURE_FORMAT_35MM_ANAMORPHIC ? 2.0f : 1.0f;

	film_size.x = ufbxi_find_real(&camera->props, ufbxi_FilmWidth, film_size.x);
	film_size.y = ufbxi_find_real(&camera->props, ufbxi_FilmHeight, film_size.y);
	squeeze_ratio = ufbxi_find_real(&camera->props, ufbxi_FilmSqueezeRatio, squeeze_ratio);

	if (aspect_x <= 0.0f && aspect_y <= 0.0f) {
		aspect_x = film_size.x > 0.0f ? film_size.x : 1.0f;
		aspect_y = film_size.y > 0.0f ? film_size.y : 1.0f;
	} else if (aspect_x <= 0.0f) {
		if (film_size.x > 0.0f && film_size.y > 0.0f) {
			aspect_x = aspect_y / film_size.y * film_size.x;
		} else {
			aspect_x = aspect_y;
		}
	} else if (aspect_y <= 0.0f) {
		if (film_size.x > 0.0f && film_size.y > 0.0f) {
			aspect_y = aspect_x / film_size.x * film_size.y;
		} else {
			aspect_y = aspect_x;
		}
	}

	film_size.y *= squeeze_ratio;

	// TODO: Should this be done always?
	ortho_extent *= scene->metadata.geometry_scale;
	camera->near_plane *= scene->metadata.geometry_scale;
	camera->far_plane *= scene->metadata.geometry_scale;

	camera->focal_length_mm = focal_length;
	camera->film_size_inch = film_size;
	camera->squeeze_ratio = squeeze_ratio;
	camera->orthographic_extent = ortho_extent;

	switch (camera->aspect_mode) {
	case UFBX_ASPECT_MODE_WINDOW_SIZE:
	case UFBX_ASPECT_MODE_FIXED_RATIO:
		camera->resolution_is_pixels = false;
		camera->resolution.x = aspect_x;
		camera->resolution.y = aspect_y;
		break;
	case UFBX_ASPECT_MODE_FIXED_RESOLUTION:
		camera->resolution_is_pixels = true;
		camera->resolution.x = aspect_x;
		camera->resolution.y = aspect_y;
		break;
	case UFBX_ASPECT_MODE_FIXED_WIDTH:
		camera->resolution_is_pixels = true;
		camera->resolution.x = aspect_x;
		camera->resolution.y = aspect_x * aspect_y;
		break;
	case UFBX_ASPECT_MODE_FIXED_HEIGHT:
		camera->resolution_is_pixels = true;
		camera->resolution.x = aspect_y * aspect_x;
		camera->resolution.y = aspect_y;
		break;
	default:
		ufbxi_unreachable("Unexpected aspect mode");
	}

	ufbx_real aspect_ratio = camera->resolution.x / camera->resolution.y;
	ufbx_real film_ratio = film_size.x / film_size.y;

	camera->aspect_ratio = aspect_ratio;

	ufbx_gate_fit effective_fit = camera->gate_fit;
	if (effective_fit == UFBX_GATE_FIT_FILL) {
		effective_fit = aspect_ratio > film_ratio ? UFBX_GATE_FIT_HORIZONTAL : UFBX_GATE_FIT_VERTICAL;
	} else if (effective_fit == UFBX_GATE_FIT_OVERSCAN) {
		effective_fit = aspect_ratio < film_ratio ? UFBX_GATE_FIT_HORIZONTAL : UFBX_GATE_FIT_VERTICAL;
	}

	switch (effective_fit) {
	case UFBX_GATE_FIT_NONE:
		camera->aperture_size_inch = camera->film_size_inch;
		camera->orthographic_size.x = ortho_extent;
		camera->orthographic_size.y = ortho_extent;
		break;
	case UFBX_GATE_FIT_VERTICAL:
		camera->aperture_size_inch.x = camera->film_size_inch.y * aspect_ratio;
		camera->aperture_size_inch.y = camera->film_size_inch.y;
		camera->orthographic_size.x = ortho_extent * aspect_ratio;
		camera->orthographic_size.y = ortho_extent;
		break;
	case UFBX_GATE_FIT_HORIZONTAL:
		camera->aperture_size_inch.x = camera->film_size_inch.x;
		camera->aperture_size_inch.y = camera->film_size_inch.x / aspect_ratio;
		camera->orthographic_size.x = ortho_extent;
		camera->orthographic_size.y = ortho_extent / aspect_ratio;
		break;
	case UFBX_GATE_FIT_FILL:
	case UFBX_GATE_FIT_OVERSCAN:
		camera->aperture_size_inch = camera->film_size_inch;
		camera->orthographic_size.x = ortho_extent;
		camera->orthographic_size.y = ortho_extent;
		ufbxi_unreachable("Unreachable, set to vertical/horizontal above");
		break;
	case UFBX_GATE_FIT_STRETCH:
		camera->aperture_size_inch = camera->film_size_inch;
		camera->orthographic_size.x = ortho_extent;
		camera->orthographic_size.y = ortho_extent;
		// TODO: Not sure what to do here...
		break;
	default:
		ufbxi_unreachable("Unexpected gate fit");
	}

	switch (camera->aperture_mode) {
	case UFBX_APERTURE_MODE_HORIZONTAL_AND_VERTICAL:
		camera->field_of_view_deg.x = fov_x;
		camera->field_of_view_deg.y = fov_y;
		camera->field_of_view_tan.x = (ufbx_real)ufbx_tan((double)(fov_x * (UFBXI_DEG_TO_RAD * 0.5f)));
		camera->field_of_view_tan.y = (ufbx_real)ufbx_tan((double)(fov_y * (UFBXI_DEG_TO_RAD * 0.5f)));
		break;
	case UFBX_APERTURE_MODE_HORIZONTAL:
		camera->field_of_view_deg.x = fov;
		camera->field_of_view_tan.x = (ufbx_real)ufbx_tan((double)(fov * (UFBXI_DEG_TO_RAD * 0.5f)));
		camera->field_of_view_tan.y = camera->field_of_view_tan.x / aspect_ratio;
		camera->field_of_view_deg.y = (ufbx_real)ufbx_atan((double)camera->field_of_view_tan.y) * UFBXI_RAD_TO_DEG * 2.0f;
		break;
	case UFBX_APERTURE_MODE_VERTICAL:
		camera->field_of_view_deg.y = fov;
		camera->field_of_view_tan.y = (ufbx_real)ufbx_tan((double)(fov * (UFBXI_DEG_TO_RAD * 0.5f)));
		camera->field_of_view_tan.x = camera->field_of_view_tan.y * aspect_ratio;
		camera->field_of_view_deg.x = (ufbx_real)ufbx_atan((double)camera->field_of_view_tan.x) * UFBXI_RAD_TO_DEG * 2.0f;
		break;
	case UFBX_APERTURE_MODE_FOCAL_LENGTH:
		camera->field_of_view_tan.x = camera->aperture_size_inch.x / (camera->focal_length_mm * UFBXI_MM_TO_INCH) * 0.5f;
		camera->field_of_view_tan.y = camera->aperture_size_inch.y / (camera->focal_length_mm * UFBXI_MM_TO_INCH) * 0.5f;
		camera->field_of_view_deg.x = (ufbx_real)ufbx_atan((double)camera->field_of_view_tan.x) * UFBXI_RAD_TO_DEG * 2.0f;
		camera->field_of_view_deg.y = (ufbx_real)ufbx_atan((double)camera->field_of_view_tan.y) * UFBXI_RAD_TO_DEG * 2.0f;
		break;
	default:
		ufbxi_unreachable("Unexpected aperture mode");
	}

	if (camera->projection_mode == UFBX_PROJECTION_MODE_PERSPECTIVE) {
		camera->projection_plane = camera->field_of_view_tan;
	} else {
		camera->projection_plane = camera->orthographic_size;
	}
}

ufbxi_noinline static void ufbxi_update_bone(ufbx_scene *scene, ufbx_bone *bone)
{
	ufbx_real unit = scene->metadata.bone_prop_size_unit;

	bone->radius = ufbxi_find_real(&bone->props, ufbxi_Size, unit) / unit;
	if (scene->metadata.bone_prop_limb_length_relative) {
		bone->relative_length = ufbxi_find_real(&bone->props, ufbxi_LimbLength, 1.0f);
	} else {
		bone->relative_length = 1.0f;
	}
}

ufbxi_noinline static void ufbxi_update_line_curve(ufbx_line_curve *line)
{
	line->color = ufbxi_find_vec3(&line->props, ufbxi_Color, 1.0f, 1.0f, 1.0f);
}

ufbxi_noinline static void ufbxi_update_pose(ufbx_pose *pose)
{
	ufbxi_for_list(ufbx_bone_pose, bone, pose->bone_poses) {
		ufbx_node *node = bone->bone_node;

		const ufbx_matrix *parent_to_world = &ufbx_identity_matrix;
		ufbx_bone_pose *bone_pose = ufbx_get_bone_pose(pose, node->parent);
		if (bone_pose) {
			parent_to_world = &bone_pose->bone_to_world;
		} else if (node->parent) {
			parent_to_world = &node->parent->node_to_world;
		}

		ufbx_matrix world_to_parent = ufbx_matrix_invert(parent_to_world);
		bone->bone_to_parent = ufbx_matrix_mul(&world_to_parent, &bone->bone_to_world);
	}
}

ufbxi_noinline static void ufbxi_update_skin_cluster(ufbx_skin_cluster *cluster)
{
	if (cluster->bone_node) {
		cluster->geometry_to_world = ufbx_matrix_mul(&cluster->bone_node->node_to_world, &cluster->geometry_to_bone);
	} else {
		cluster->geometry_to_world = ufbx_matrix_mul(&cluster->bind_to_world, &cluster->geometry_to_bone);
	}
	cluster->geometry_to_world_transform = ufbx_matrix_to_transform(&cluster->geometry_to_world);
}

ufbxi_noinline static void ufbxi_update_blend_channel(ufbx_blend_channel *channel)
{
	ufbx_real weight = ufbxi_find_real(&channel->props, ufbxi_DeformPercent, 0.0f) * (ufbx_real)0.01;
	channel->weight = weight;

	ptrdiff_t num_keys = (ptrdiff_t)channel->keyframes.count;
	if (num_keys > 0) {
		ufbx_blend_keyframe *keys = channel->keyframes.data;

		// Reset the effective weights to zero and find the split around zero
		ptrdiff_t last_negative = -1;
		for (ptrdiff_t i = 0; i < num_keys; i++) {
			keys[i].effective_weight = (ufbx_real)0.0;
			if (keys[i].target_weight < 0.0) last_negative = i;
		}

		// Find either the next or last keyframe away from zero
		ufbx_blend_keyframe zero_key = { NULL };
		ufbx_blend_keyframe *prev = &zero_key, *next = &zero_key;
		if (weight > 0.0) {
			if (last_negative >= 0) prev = &keys[last_negative];
			for (ptrdiff_t i = last_negative + 1; i < num_keys; i++) {
				prev = next;
				next = &keys[i];
				if (next->target_weight > weight) break;
			}
		} else {
			if (last_negative + 1 < num_keys) prev = &keys[last_negative + 1];
			for (ptrdiff_t i = last_negative; i >= 0; i--) {
				prev = next;
				next = &keys[i];
				if (next->target_weight < weight) break;
			}
		}

		// Linearly interpolate between the endpoints with the weight
		ufbx_real delta = next->target_weight - prev->target_weight;
		if (delta != 0.0) {
			ufbx_real t = (weight - prev->target_weight) / delta;
			prev->effective_weight = 1.0f - t;
			next->effective_weight = t;
		}
	}
}

ufbxi_noinline static void ufbxi_update_material(ufbx_scene *scene, ufbx_material *material)
{
	if (material->props.num_animated > 0) {
		ufbxi_fetch_maps(scene, material);
	}
}

ufbxi_noinline static void ufbxi_update_texture(ufbx_texture *texture)
{
	texture->uv_transform = ufbxi_get_texture_transform(&texture->props);
	if (!ufbxi_is_transform_identity(&texture->uv_transform)) {
		texture->has_uv_transform = true;
		texture->texture_to_uv = ufbx_transform_to_matrix(&texture->uv_transform);
		texture->uv_to_texture = ufbx_matrix_invert(&texture->texture_to_uv);
	} else {
		texture->has_uv_transform = false;
		texture->texture_to_uv = ufbx_identity_matrix;
		texture->uv_to_texture = ufbx_identity_matrix;
	}
	texture->wrap_u = (ufbx_wrap_mode)ufbxi_find_enum(&texture->props, ufbxi_WrapModeU, 0, UFBX_WRAP_CLAMP);
	texture->wrap_v = (ufbx_wrap_mode)ufbxi_find_enum(&texture->props, ufbxi_WrapModeV, 0, UFBX_WRAP_CLAMP);

	if (texture->shader) {
		ufbxi_update_shader_texture(texture, texture->shader);
	}
}

ufbxi_noinline static void ufbxi_update_anim_stack(ufbx_scene *scene, ufbx_anim_stack *stack)
{
	ufbx_prop *begin, *end;
	begin = ufbxi_find_prop(&stack->props, ufbxi_LocalStart);
	end = ufbxi_find_prop(&stack->props, ufbxi_LocalStop);
	if (!begin || !end) {
		begin = ufbxi_find_prop(&stack->props, ufbxi_ReferenceStart);
		end = ufbxi_find_prop(&stack->props, ufbxi_ReferenceStop);
	}

	if (begin && end) {
		stack->time_begin = (double)begin->value_int / (double)scene->metadata.ktime_second;
		stack->time_end = (double)end->value_int / (double)scene->metadata.ktime_second;
	}

	stack->anim->time_begin = stack->time_begin;
	stack->anim->time_end = stack->time_end;
}

ufbxi_noinline static void ufbxi_update_display_layer(ufbx_display_layer *layer)
{
	layer->visible = ufbxi_find_int(&layer->props, ufbxi_Show, 1) != 0;
	layer->frozen = ufbxi_find_int(&layer->props, ufbxi_Freeze, 1) != 0;
	layer->ui_color = ufbxi_find_vec3(&layer->props, ufbxi_Color, 0.8f, 0.8f, 0.8f);
}

ufbxi_noinline static void ufbxi_find_bool3(bool *dst, ufbx_props *props, const char *name, bool default_value)
{
	size_t name_len = strlen(name);
	char local[64];
	ufbx_assert(name_len < sizeof(local) - 2);
	memcpy(local, name, name_len);

	size_t local_len = name_len + 1;
	local[local_len] = '\0';

	int64_t def = default_value ? 1 : 0;
	local[name_len] = 'X';
	dst[0] = ufbx_find_int_len(props, local, local_len, def) != 0;
	local[name_len] = 'Y';
	dst[1] = ufbx_find_int_len(props, local, local_len, def) != 0;
	local[name_len] = 'Z';
	dst[2] = ufbx_find_int_len(props, local, local_len, def) != 0;
}

ufbxi_noinline static void ufbxi_update_constraint(ufbx_constraint *constraint)
{
	ufbx_props *props = &constraint->props;
	ufbx_constraint_type constraint_type = constraint->type;

	constraint->transform_offset = ufbxi_get_constraint_transform(props);

	constraint->weight = ufbxi_find_real(props, ufbxi_Weight, (ufbx_real)100.0) / (ufbx_real)100.0;

	ufbxi_for_list(ufbx_constraint_target, target, constraint->targets) {
		ufbx_node *node = target->node;

		ufbx_real weight_scale = (ufbx_real)100.0;
		if (constraint_type == UFBX_CONSTRAINT_SINGLE_CHAIN_IK) {
			// IK weights seem to be not scaled 100x?
			weight_scale = (ufbx_real)1.0;
		}

		ufbx_prop *prop; // ufbxi_uninit
		ufbx_string parts[2]; // ufbxi_uninit
		parts[0] = node->name;
		parts[1] = ufbxi_str_c(".Weight");
		prop = ufbx_find_prop_concat(props, parts, 2);
		target->weight = (prop ? prop->value_real : weight_scale) / weight_scale;

		if (constraint_type == UFBX_CONSTRAINT_PARENT) {
			parts[1] = ufbxi_str_c(".Offset T");
			prop = ufbx_find_prop_concat(props, parts, 2);
			ufbx_vec3 t = prop ? prop->value_vec3 : ufbx_zero_vec3;
			parts[1] = ufbxi_str_c(".Offset R");
			prop = ufbx_find_prop_concat(props, parts, 2);
			ufbx_vec3 r = prop ? prop->value_vec3 : ufbx_zero_vec3;
			parts[1] = ufbxi_str_c(".Offset S");
			prop = ufbx_find_prop_concat(props, parts, 2);
			ufbx_vec3 s = prop ? prop->value_vec3 : ufbxi_one_vec3;

			target->transform.translation = t;
			target->transform.rotation = ufbx_euler_to_quat(r, UFBX_ROTATION_ORDER_XYZ);
			target->transform.scale = s;
		}
	}

	constraint->active = ufbx_find_int(props, "Active", 1) != 0;
	if (constraint_type == UFBX_CONSTRAINT_AIM) {
		ufbxi_find_bool3(constraint->constrain_rotation, props, "Affect", 1);

		const ufbx_vec3 default_aim = { 1.0f, 0.0f, 0.0f };
		const ufbx_vec3 default_up = { 0.0f, 1.0f, 0.0f };

		int64_t up_type = ufbx_find_int(props, "WorldUpType", 0);
		if (up_type >= 0 && up_type < UFBX_CONSTRAINT_AIM_UP_NONE) {
			constraint->aim_up_type = (ufbx_constraint_aim_up_type)up_type;
		}
		constraint->aim_vector = ufbx_find_vec3(props, "AimVector", default_aim);
		constraint->aim_up_vector = ufbx_find_vec3(props, "UpVector", default_up);

	} else if (constraint_type == UFBX_CONSTRAINT_PARENT) {
		ufbxi_find_bool3(constraint->constrain_translation, props, "AffectTranslation", 1);
		ufbxi_find_bool3(constraint->constrain_rotation, props, "AffectRotation", 1);
		ufbxi_find_bool3(constraint->constrain_scale, props, "AffectScale", 0);
	} else if (constraint_type == UFBX_CONSTRAINT_POSITION) {
		ufbxi_find_bool3(constraint->constrain_translation, props, "Affect", 1);
	} else if (constraint_type == UFBX_CONSTRAINT_ROTATION) {
		ufbxi_find_bool3(constraint->constrain_rotation, props, "Affect", 1);
	} else if (constraint_type == UFBX_CONSTRAINT_SCALE) {
		ufbxi_find_bool3(constraint->constrain_scale, props, "Affect", 1);
	} else if (constraint_type == UFBX_CONSTRAINT_SINGLE_CHAIN_IK) {
		constraint->constrain_rotation[0] = true;
		constraint->constrain_rotation[1] = true;
		constraint->constrain_rotation[2] = true;
		constraint->ik_pole_vector = ufbx_find_vec3(props, "PoleVectorType", ufbx_zero_vec3);
	}
}

ufbxi_noinline static void ufbxi_update_anim(ufbx_scene *scene)
{
	if (scene->anim_stacks.count > 0) {
		scene->anim = scene->anim_stacks.data[0]->anim;
	}
}

static ufbxi_forceinline void ufbxi_mirror_matrix_dst(ufbx_matrix *m, ufbx_mirror_axis axis)
{
	if (axis == 0) return;
	int32_t ax = (int32_t)axis - 1;
	m->cols[0].v[ax] = -m->cols[0].v[ax];
	m->cols[1].v[ax] = -m->cols[1].v[ax];
	m->cols[2].v[ax] = -m->cols[2].v[ax];
	m->cols[3].v[ax] = -m->cols[3].v[ax];
}

static ufbxi_forceinline void ufbxi_mirror_matrix_src(ufbx_matrix *m, ufbx_mirror_axis axis)
{
	if (axis == 0) return;
	int32_t ax = (int32_t)axis - 1;
	m->cols[ax].x = -m->cols[ax].x;
	m->cols[ax].y = -m->cols[ax].y;
	m->cols[ax].z = -m->cols[ax].z;
}

static ufbxi_noinline void ufbxi_mirror_matrix(ufbx_matrix *m, ufbx_mirror_axis axis)
{
	if (axis == 0) return;
	ufbxi_mirror_matrix_src(m, axis);
	ufbxi_mirror_matrix_dst(m, axis);
}

ufbxi_noinline static void ufbxi_update_initial_clusters(ufbx_scene *scene)
{
	ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, scene->skin_clusters) {
		ufbx_skin_cluster *cluster = *p_cluster;
		cluster->geometry_to_bone = cluster->mesh_node_to_bone;
	}

	ufbx_mirror_axis mirror_axis = scene->metadata.mirror_axis;
	ufbx_real geometry_scale = scene->metadata.geometry_scale;

	// Space conversion for bind matrices
	{
		ufbx_matrix world_to_units;
		ufbx_real translation_scale = 1.0f;

		if (scene->metadata.space_conversion == UFBX_SPACE_CONVERSION_TRANSFORM_ROOT && scene->metadata.mirror_axis == UFBX_MIRROR_AXIS_NONE) {
			world_to_units = scene->root_node->node_to_parent;
		} else {
			ufbx_transform root_transform;
			root_transform.translation = ufbx_zero_vec3;
			root_transform.rotation = scene->metadata.root_rotation;
			root_transform.scale.x = scene->metadata.root_scale;
			root_transform.scale.y = scene->metadata.root_scale;
			root_transform.scale.z = scene->metadata.root_scale;
			world_to_units = ufbx_transform_to_matrix(&root_transform);
			translation_scale = scene->metadata.geometry_scale;
		}

		ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, scene->skin_clusters) {
			ufbx_skin_cluster *cluster = *p_cluster;
			cluster->bind_to_world = ufbx_matrix_mul(&world_to_units, &cluster->bind_to_world);
			cluster->bind_to_world.cols[3].x *= translation_scale;
			cluster->bind_to_world.cols[3].y *= translation_scale;
			cluster->bind_to_world.cols[3].z *= translation_scale;
			ufbxi_mirror_matrix(&cluster->bind_to_world, mirror_axis);
		}

		ufbxi_for_ptr_list(ufbx_pose, p_pose, scene->poses) {
			ufbxi_for_list(ufbx_bone_pose, pose, (*p_pose)->bone_poses) {
				pose->bone_to_world = ufbx_matrix_mul(&world_to_units, &pose->bone_to_world);
				pose->bone_to_world.cols[3].x *= translation_scale;
				pose->bone_to_world.cols[3].y *= translation_scale;
				pose->bone_to_world.cols[3].z *= translation_scale;
				ufbxi_mirror_matrix(&pose->bone_to_world, mirror_axis);
			}
		}
	}

	// Patch initial `mesh_node_to_bone`
	ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, scene->skin_clusters) {
		ufbx_skin_cluster *cluster = *p_cluster;

		ufbx_skin_deformer *skin = (ufbx_skin_deformer*)ufbxi_fetch_src_element(&cluster->element, false, NULL, UFBX_ELEMENT_SKIN_DEFORMER);
		if (!skin) continue;

		ufbx_node *node = (ufbx_node*)ufbxi_fetch_src_element(&skin->element, false, NULL, UFBX_ELEMENT_NODE);
		if (!node) {
			ufbx_mesh *mesh = (ufbx_mesh*)ufbxi_fetch_src_element(&skin->element, false, NULL, UFBX_ELEMENT_MESH);
			if (mesh && mesh->instances.count > 0) {
				node = mesh->instances.data[0];
			}
		}
		if (!node) continue;

		// Normalize to the non-helper node
		if (node->is_geometry_transform_helper) {
			node = node->parent;
		}

		if (ufbxi_matrix_all_zero(&cluster->mesh_node_to_bone)) {
			// If `mesh_node_to_bone` is not explicitly specified compute it from bind pose.
			ufbx_matrix world_to_bind = ufbx_matrix_invert(&cluster->bind_to_world);
			cluster->mesh_node_to_bone = ufbx_matrix_mul(&world_to_bind, &node->node_to_world);
		} else {
			// If `mesh_node_to_bone` is explicit, we may need to modify it for space conversion.
			ufbxi_mirror_matrix(&cluster->mesh_node_to_bone, mirror_axis);
			if (geometry_scale != 1.0f) {
				cluster->mesh_node_to_bone.cols[3].x *= geometry_scale;
				cluster->mesh_node_to_bone.cols[3].y *= geometry_scale;
				cluster->mesh_node_to_bone.cols[3].z *= geometry_scale;
			}
		}

		// HACK: Account for geometry transforms by looking at the transform of the
		// helper node if one is present. I don't think this is exactly how the skinning
		// matrices are formed.
		// TODO: Add a test with moving the skinned mesh root around.
		if (node->geometry_transform_helper) {
			ufbx_node *geo_node = node->geometry_transform_helper;
			cluster->geometry_to_bone = ufbx_matrix_mul(&cluster->mesh_node_to_bone, &geo_node->node_to_parent);
		} else if (node->has_geometry_transform) {
			cluster->geometry_to_bone = ufbx_matrix_mul(&cluster->mesh_node_to_bone, &node->geometry_to_node);
		} else {
			cluster->geometry_to_bone = cluster->mesh_node_to_bone;
		}
	}
}

ufbxi_noinline static ufbx_coordinate_axis ufbxi_find_axis(const ufbx_props *props, const char *axis_name, const char *sign_name)
{
	int64_t axis = ufbxi_find_int(props, axis_name, 3);
	int64_t sign = ufbxi_find_int(props, sign_name, 2);

	switch (axis) {
	case 0: return sign > 0 ? UFBX_COORDINATE_AXIS_POSITIVE_X : UFBX_COORDINATE_AXIS_NEGATIVE_X;
	case 1: return sign > 0 ? UFBX_COORDINATE_AXIS_POSITIVE_Y : UFBX_COORDINATE_AXIS_NEGATIVE_Y;
	case 2: return sign > 0 ? UFBX_COORDINATE_AXIS_POSITIVE_Z : UFBX_COORDINATE_AXIS_NEGATIVE_Z;
	default: return UFBX_COORDINATE_AXIS_UNKNOWN;
	}
}

static const ufbx_real ufbxi_time_mode_fps[] = {
	30.0f,   // UFBX_TIME_MODE_DEFAULT
	120.0f,  // UFBX_TIME_MODE_120_FPS
	100.0f,  // UFBX_TIME_MODE_100_FPS
	60.0f,   // UFBX_TIME_MODE_60_FPS
	50.0f,   // UFBX_TIME_MODE_50_FPS
	48.0f,   // UFBX_TIME_MODE_48_FPS
	30.0f,   // UFBX_TIME_MODE_30_FPS
	30.0f,   // UFBX_TIME_MODE_30_FPS_DROP
	29.97f,  // UFBX_TIME_MODE_NTSC_DROP_FRAME
	29.97f,  // UFBX_TIME_MODE_NTSC_FULL_FRAME
	25.0f,   // UFBX_TIME_MODE_PAL
	24.0f,   // UFBX_TIME_MODE_24_FPS
	1000.0f, // UFBX_TIME_MODE_1000_FPS
	23.976f, // UFBX_TIME_MODE_FILM_FULL_FRAME
	24.0f,   // UFBX_TIME_MODE_CUSTOM
	96.0f,   // UFBX_TIME_MODE_96_FPS
	72.0f,   // UFBX_TIME_MODE_72_FPS
	59.94f,  // UFBX_TIME_MODE_59_94_FPS
};

// Returns whether a non-identity matrix was needed
static ufbxi_noinline bool ufbxi_axis_matrix(ufbx_matrix *mat, ufbx_coordinate_axes src, ufbx_coordinate_axes dst)
{
	uint32_t src_x = (uint32_t)src.right;
	uint32_t dst_x = (uint32_t)dst.right;
	uint32_t src_y = (uint32_t)src.up;
	uint32_t dst_y = (uint32_t)dst.up;
	uint32_t src_z = (uint32_t)src.front;
	uint32_t dst_z = (uint32_t)dst.front;

	if (src_x == dst_x && src_y == dst_y && src_z == dst_z) return false;

	// Remap axes (axis enum divided by 2) potentially flipping if the signs (enum parity) doesn't match
	memset(mat, 0, sizeof(ufbx_matrix));
	mat->cols[src_x >> 1].v[dst_x >> 1] = ((src_x ^ dst_x) & 1) == 0 ? 1.0f : -1.0f;
	mat->cols[src_y >> 1].v[dst_y >> 1] = ((src_y ^ dst_y) & 1) == 0 ? 1.0f : -1.0f;
	mat->cols[src_z >> 1].v[dst_z >> 1] = ((src_z ^ dst_z) & 1) == 0 ? 1.0f : -1.0f;

	return true;
}

ufbxi_noinline static void ufbxi_update_adjust_transforms(ufbxi_context *uc, ufbx_scene *scene)
{
	ufbx_transform root_transform = ufbx_identity_transform;
	if (!ufbxi_matrix_all_zero(&uc->axis_matrix)) {
		root_transform = ufbx_matrix_to_transform(&uc->axis_matrix);
	}
	root_transform.scale.x *= uc->unit_scale;
	root_transform.scale.y *= uc->unit_scale;
	root_transform.scale.z *= uc->unit_scale;

	ufbx_space_conversion conversion = uc->opts.space_conversion;

	ufbx_quat light_post_rotation = ufbx_identity_quat;
	ufbx_quat camera_post_rotation = ufbx_identity_quat;
	ufbx_vec3 light_direction = { 0.0f, -1.0f, 0.0f };
	bool has_light_transform = false;
	bool has_camera_transform = false;

	if (ufbx_coordinate_axes_valid(uc->opts.target_light_axes)) {
		ufbx_matrix mat; // ufbxi_uninit
		ufbx_coordinate_axes light_axes = {
			UFBX_COORDINATE_AXIS_POSITIVE_X,
			UFBX_COORDINATE_AXIS_NEGATIVE_Z,
			UFBX_COORDINATE_AXIS_POSITIVE_Y,
		};
		if (ufbxi_axis_matrix(&mat, uc->opts.target_light_axes, light_axes)) {
			light_post_rotation = ufbx_matrix_to_transform(&mat).rotation;

			ufbx_matrix inv = ufbx_matrix_invert(&mat);
			light_direction = ufbx_transform_direction(&inv, light_direction);
			has_light_transform = true;
		}
	}

	if (ufbx_coordinate_axes_valid(uc->opts.target_camera_axes)) {
		ufbx_matrix mat; // ufbxi_uninit
		ufbx_coordinate_axes camera_axes = {
			UFBX_COORDINATE_AXIS_POSITIVE_Z,
			UFBX_COORDINATE_AXIS_POSITIVE_Y,
			UFBX_COORDINATE_AXIS_NEGATIVE_X,
		};
		if (ufbxi_axis_matrix(&mat, uc->opts.target_camera_axes, camera_axes)) {
			camera_post_rotation = ufbx_matrix_to_transform(&mat).rotation;
			has_camera_transform = true;
		}
	}

	ufbxi_for_ptr_list(ufbx_light, p_light, scene->lights) {
		ufbx_light *light = *p_light;
		light->local_direction.x = 0.0f;
		light->local_direction.y = -1.0f;
		light->local_direction.z = 0.0f;
	}

	ufbx_real root_scale = ufbxi_min3(root_transform.scale);
	scene->metadata.space_conversion = conversion;
	if (conversion == UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY) {
		scene->metadata.geometry_scale = root_scale;
		scene->metadata.root_scale = 1.0f;
	} else {
		scene->metadata.geometry_scale = 1.0f;
		scene->metadata.root_scale = root_scale;
	}
	scene->metadata.root_rotation = root_transform.rotation;

	ufbxi_for_ptr_list(ufbx_node, p_node, scene->nodes) {
		ufbx_node *node = *p_node;

		node->adjust_post_rotation = ufbx_identity_quat;
		node->adjust_pre_rotation = ufbx_identity_quat;
		node->adjust_pre_scale = 1.0f;
		node->adjust_post_scale = 1.0f;
		node->adjust_translation_scale = 1.0f;

		if (conversion == UFBX_SPACE_CONVERSION_ADJUST_TRANSFORMS) {
			if (node->node_depth <= 1 && !node->is_root) {
				node->adjust_pre_rotation = root_transform.rotation;
				node->adjust_pre_scale = root_scale;
				node->has_adjust_transform = true;
				node->has_root_adjust_transform = true;
			}
		} else if (conversion == UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY) {
			if (!node->is_root) {
				if (node->node_depth <= 1) {
					node->adjust_pre_rotation = root_transform.rotation;
				}
				node->adjust_translation_scale = root_scale;
				node->has_adjust_transform = true;
			}
		}

		if (node->parent) {
			// We are not inheriting local scale, so propagate root scale manually and
			// apply scale compensation if necessary.
			ufbx_node *parent = node->parent;
			if (parent->has_root_adjust_transform && node->inherit_mode == UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE) {
				node->adjust_post_scale *= root_scale;
				node->has_adjust_transform = true;
				node->has_root_adjust_transform = true;
			}
			if (parent->is_scale_compensate_parent && node->original_inherit_mode == UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE) {
				ufbx_vec3 scale = ufbxi_find_vec3(&parent->props, ufbxi_Lcl_Scaling, 1.0f, 1.0f, 1.0f);
				ufbx_real size = scale.x;
				if (ufbx_fabs(scale.y - 1.0f) < ufbx_fabs(size - 1.0f)) size = scale.y;
				if (ufbx_fabs(scale.z - 1.0f) < ufbx_fabs(size - 1.0f)) size = scale.z;
				node->adjust_post_scale *= 1.0f / size;
				node->has_adjust_transform = true;
			}
		}

		if (node->all_attribs.count == 1) {
			if (has_light_transform && node->light) {
				node->adjust_post_rotation = light_post_rotation;
				node->light->local_direction = light_direction;
				node->has_adjust_transform = true;
			}
			if (has_camera_transform && node->camera) {
				node->adjust_post_rotation = camera_post_rotation;
				node->camera->projection_axes = uc->opts.target_camera_axes;
				node->has_adjust_transform = true;
			}
		}
	}
}

ufbxi_noinline static void ufbxi_update_scene(ufbx_scene *scene, bool initial, const ufbx_transform_override *transform_overrides, size_t num_transform_overrides)
{
	ufbxi_for_ptr_list(ufbx_node, p_node, scene->nodes) {
		ufbxi_update_node(*p_node, transform_overrides, num_transform_overrides);
	}

	ufbxi_for_ptr_list(ufbx_light, p_light, scene->lights) {
		ufbxi_update_light(*p_light);
	}

	ufbxi_for_ptr_list(ufbx_camera, p_camera, scene->cameras) {
		ufbxi_update_camera(scene, *p_camera);
	}

	ufbxi_for_ptr_list(ufbx_bone, p_bone, scene->bones) {
		ufbxi_update_bone(scene, *p_bone);
	}

	ufbxi_for_ptr_list(ufbx_line_curve, p_line, scene->line_curves) {
		ufbxi_update_line_curve(*p_line);
	}

	if (initial) {
		ufbxi_update_initial_clusters(scene);

		ufbxi_for_ptr_list(ufbx_pose, p_pose, scene->poses) {
			ufbxi_update_pose(*p_pose);
		}
	}

	ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, scene->skin_clusters) {
		ufbxi_update_skin_cluster(*p_cluster);
	}

	ufbxi_for_ptr_list(ufbx_blend_channel, p_channel, scene->blend_channels) {
		ufbxi_update_blend_channel(*p_channel);
	}

	ufbxi_for_ptr_list(ufbx_texture, p_texture, scene->textures) {
		ufbxi_update_texture(*p_texture);
	}

	ufbxi_propagate_main_textures(scene);

	ufbxi_for_ptr_list(ufbx_material, p_material, scene->materials) {
		ufbxi_update_material(scene, *p_material);
	}

	ufbxi_for_ptr_list(ufbx_anim_stack, p_stack, scene->anim_stacks) {
		ufbxi_update_anim_stack(scene, *p_stack);
	}

	ufbxi_for_ptr_list(ufbx_display_layer, p_layer, scene->display_layers) {
		ufbxi_update_display_layer(*p_layer);
	}

	ufbxi_for_ptr_list(ufbx_constraint, p_constraint, scene->constraints) {
		ufbxi_update_constraint(*p_constraint);
	}

	ufbxi_update_anim(scene);
}

static ufbxi_noinline void ufbxi_update_scene_metadata(ufbx_metadata *metadata)
{
	ufbx_props *props = &metadata->scene_props;
	metadata->original_application.vendor = ufbx_find_string(props, "Original|ApplicationVendor", ufbx_empty_string);
	metadata->original_application.name = ufbx_find_string(props, "Original|ApplicationName", ufbx_empty_string);
	metadata->original_application.version = ufbx_find_string(props, "Original|ApplicationVersion", ufbx_empty_string);
	metadata->latest_application.vendor = ufbx_find_string(props, "LastSaved|ApplicationVendor", ufbx_empty_string);
	metadata->latest_application.name = ufbx_find_string(props, "LastSaved|ApplicationName", ufbx_empty_string);
	metadata->latest_application.version = ufbx_find_string(props, "LastSaved|ApplicationVersion", ufbx_empty_string);
}

static const ufbx_real ufbxi_pow10_targets[] = {
	0.0f,
	(ufbx_real)1e-8, (ufbx_real)1e-7, (ufbx_real)1e-6, (ufbx_real)1e-5,
	(ufbx_real)1e-4, (ufbx_real)1e-3, (ufbx_real)1e-2, (ufbx_real)1e-1,
	(ufbx_real)1e+0, (ufbx_real)1e+1, (ufbx_real)1e+2, (ufbx_real)1e+3,
	(ufbx_real)1e+4, (ufbx_real)1e+5, (ufbx_real)1e+6, (ufbx_real)1e+7,
	(ufbx_real)1e+8, (ufbx_real)1e+9,
};

static ufbxi_noinline ufbx_real ufbxi_round_if_near(const ufbx_real *targets, size_t num_targets, ufbx_real value)
{
	for (size_t i = 0; i < num_targets; i++) {
		double target = targets[i];
		double error = target * 9.5367431640625e-7;
		if (error < 0.0) error = -error;
		if (error < 7.52316384526264005e-37) error = 7.52316384526264005e-37;
		if (value >= target - error && value <= target + error) {
			return (ufbx_real)target;
		}
	}
	return value;
}

static ufbxi_noinline void ufbxi_update_scene_settings(ufbx_scene_settings *settings)
{
	ufbx_real unit_scale_factor = ufbxi_find_real(&settings->props, ufbxi_UnitScaleFactor, 1.0f);
	ufbx_real original_unit_scale_factor = ufbxi_find_real(&settings->props, ufbxi_OriginalUnitScaleFactor, unit_scale_factor);

	settings->axes.up = ufbxi_find_axis(&settings->props, ufbxi_UpAxis, ufbxi_UpAxisSign);
	settings->axes.front = ufbxi_find_axis(&settings->props, ufbxi_FrontAxis, ufbxi_FrontAxisSign);
	settings->axes.right = ufbxi_find_axis(&settings->props, ufbxi_CoordAxis, ufbxi_CoordAxisSign);
	settings->unit_meters = ufbxi_round_if_near(ufbxi_pow10_targets, ufbxi_arraycount(ufbxi_pow10_targets), unit_scale_factor * (ufbx_real)0.01);
	settings->original_unit_meters = ufbxi_round_if_near(ufbxi_pow10_targets, ufbxi_arraycount(ufbxi_pow10_targets), original_unit_scale_factor * (ufbx_real)0.01);
	settings->frames_per_second = ufbxi_find_real(&settings->props, ufbxi_CustomFrameRate, 24.0f);
	settings->ambient_color = ufbxi_find_vec3(&settings->props, ufbxi_AmbientColor, 0.0f, 0.0f, 0.0f);
	settings->original_axis_up = ufbxi_find_axis(&settings->props, ufbxi_OriginalUpAxis, ufbxi_OriginalUpAxisSign);

	ufbx_prop *default_camera = ufbxi_find_prop(&settings->props, ufbxi_DefaultCamera);
	if (default_camera) {
		settings->default_camera = default_camera->value_str;
	} else {
		settings->default_camera = ufbx_empty_string;
	}

	settings->time_mode = (ufbx_time_mode)ufbxi_find_enum(&settings->props, ufbxi_TimeMode, UFBX_TIME_MODE_24_FPS, UFBX_TIME_MODE_59_94_FPS);
	settings->time_protocol = (ufbx_time_protocol)ufbxi_find_enum(&settings->props, ufbxi_TimeProtocol, UFBX_TIME_PROTOCOL_DEFAULT, UFBX_TIME_PROTOCOL_DEFAULT);
	settings->snap_mode = (ufbx_snap_mode)ufbxi_find_enum(&settings->props, ufbxi_SnapOnFrameMode, UFBX_SNAP_MODE_NONE, UFBX_SNAP_MODE_SNAP_AND_PLAY);

	if (settings->time_mode != UFBX_TIME_MODE_CUSTOM) {
		settings->frames_per_second = ufbxi_time_mode_fps[settings->time_mode];
	}
}

static ufbxi_noinline void ufbxi_update_scene_settings_obj(ufbxi_context *uc)
{
	ufbx_scene_settings *settings = &uc->scene.settings;
	settings->original_unit_meters = settings->unit_meters = uc->opts.obj_unit_meters;
	if (ufbx_coordinate_axes_valid(uc->opts.obj_axes)) {
		settings->axes = uc->opts.obj_axes;
	} else {
		settings->axes.right = UFBX_COORDINATE_AXIS_UNKNOWN;
		settings->axes.up = UFBX_COORDINATE_AXIS_UNKNOWN;
		settings->axes.front = UFBX_COORDINATE_AXIS_UNKNOWN;
	}
}

// -- Geometry caches

#if UFBXI_FEATURE_GEOMETRY_CACHE

typedef struct {
	ufbxi_refcount refcount;
	ufbx_geometry_cache cache;
	uint32_t magic;
	bool owned_by_scene;

	ufbxi_buf string_buf;
} ufbxi_geometry_cache_imp;

ufbx_static_assert(geometry_cache_imp_offset, offsetof(ufbxi_geometry_cache_imp, cache) == sizeof(ufbxi_refcount));

typedef struct {
	ufbx_string name;
	ufbx_string interpretation;
	uint32_t sample_rate;
	uint32_t start_time;
	uint32_t end_time;
	uint32_t current_time;
	uint32_t consecutive_fails;
	bool try_load;
} ufbxi_cache_tmp_channel;

typedef enum {
	UFBXI_CACHE_XML_TYPE_NONE,
	UFBXI_CACHE_XML_TYPE_FILE_PER_FRAME,
	UFBXI_CACHE_XML_TYPE_SINGLE_FILE,
} ufbxi_cache_xml_type;

typedef enum {
	UFBXI_CACHE_XML_FORMAT_NONE,
	UFBXI_CACHE_XML_FORMAT_MCC,
	UFBXI_CACHE_XML_FORMAT_MCX,
} ufbxi_cache_xml_format;

typedef struct {
	ufbx_error error;
	ufbx_string filename;
	bool owned_by_scene;
	bool ignore_if_not_found;

	ufbx_geometry_cache_opts opts;

	ufbxi_allocator *ator_tmp;
	ufbxi_allocator ator_result;

	ufbxi_buf result;
	ufbxi_buf tmp;
	ufbxi_buf tmp_stack;

	ufbxi_cache_tmp_channel *channels;
	size_t num_channels;

	// Temporary array
	char *tmp_arr;
	size_t tmp_arr_size;

	ufbxi_string_pool string_pool;

	ufbx_open_file_cb open_file_cb;

	double frames_per_second;

	ufbx_string stream_filename;
	ufbx_stream stream;

	bool mc_for8;

	ufbx_string xml_filename;
	uint32_t xml_ticks_per_frame;
	ufbxi_cache_xml_type xml_type;
	ufbxi_cache_xml_format xml_format;

	ufbx_string channel_name;

	char *name_buf;
	size_t name_cap;

	uint64_t file_offset;
	const char *pos, *pos_end;

	ufbx_geometry_cache cache;
	ufbxi_geometry_cache_imp *imp;

	char buffer[128];
} ufbxi_cache_context;

ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_read(ufbxi_cache_context *cc, void *dst, size_t size, bool allow_eof)
{
	size_t buffered = ufbxi_min_sz(ufbxi_to_size(cc->pos_end - cc->pos), size);
	memcpy(dst, cc->pos, buffered);
	cc->pos += buffered;
	size -= buffered;
	cc->file_offset += buffered;
	if (size == 0) return 1;
	dst = (char*)dst + buffered;

	if (size >= sizeof(cc->buffer)) {
		size_t num_read = cc->stream.read_fn(cc->stream.user, dst, size);
		ufbxi_check_err_msg(&cc->error, num_read <= size, "IO error");
		if (!allow_eof) {
			ufbxi_check_err_msg(&cc->error, num_read == size, "Truncated file");
		}
		cc->file_offset += num_read;
		size -= num_read;
		dst = (char*)dst + num_read;
	} else {
		size_t num_read = cc->stream.read_fn(cc->stream.user, cc->buffer, sizeof(cc->buffer));
		ufbxi_check_err_msg(&cc->error, num_read <= sizeof(cc->buffer), "IO error");
		if (!allow_eof) {
			ufbxi_check_err_msg(&cc->error, num_read >= size, "Truncated file");
		}
		cc->pos = cc->buffer;
		cc->pos_end = cc->buffer + sizeof(cc->buffer);

		memcpy(dst, cc->pos, size);
		cc->pos += size;
		cc->file_offset += size;

		size_t num_written = ufbxi_min_sz(size, num_read);
		size -= num_written;
		dst = (char*)dst + num_written;
	}

	if (size > 0) {
		memset(dst, 0, size);
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_skip(ufbxi_cache_context *cc, uint64_t size)
{
	cc->file_offset += size;

	uint64_t buffered = ufbxi_min64((uint64_t)(cc->pos_end - cc->pos), size);
	cc->pos += buffered;
	size -= buffered;

	if (cc->stream.skip_fn) {
		while (size >= UFBXI_MAX_SKIP_SIZE) {
			size -= UFBXI_MAX_SKIP_SIZE;
			ufbxi_check_err_msg(&cc->error, cc->stream.skip_fn(cc->stream.user, UFBXI_MAX_SKIP_SIZE - 1), "Truncated file");

			// Check that we can read at least one byte in case the file is broken
			// and causes us to seek indefinitely forwards as `fseek()` does not
			// report if we hit EOF...
			char single_byte[1]; // ufbxi_uninit
			size_t num_read = cc->stream.read_fn(cc->stream.user, single_byte, 1);
			ufbxi_check_err_msg(&cc->error, num_read <= 1, "IO error");
			ufbxi_check_err_msg(&cc->error, num_read == 1, "Truncated file");
		}

		if (size > 0) {
			ufbxi_check_err_msg(&cc->error, cc->stream.skip_fn(cc->stream.user, (size_t)size), "Truncated file");
		}

	} else {
		char skip_buf[2048]; // ufbxi_uninit
		while (size > 0) {
			size_t to_skip = (size_t)ufbxi_min64(size, sizeof(skip_buf));
			size -= to_skip;
			ufbxi_check_err_msg(&cc->error, cc->stream.read_fn(cc->stream.user, skip_buf, to_skip), "Truncated file");
		}
	}

	return 1;
}

#define ufbxi_cache_mc_tag(a,b,c,d) ((uint32_t)(a)<<24u | (uint32_t)(b)<<16 | (uint32_t)(c)<<8u | (uint32_t)(d))

ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_mc_read_tag(ufbxi_cache_context *cc, uint32_t *p_tag)
{
	char buf[4]; // ufbxi_uninit
	ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, buf, 4, true));
	*p_tag = (uint32_t)(uint8_t)buf[0]<<24u | (uint32_t)(uint8_t)buf[1]<<16 | (uint32_t)(uint8_t)buf[2]<<8u | (uint32_t)(uint8_t)buf[3];
	if (*p_tag == ufbxi_cache_mc_tag('F','O','R','8')) {
		cc->mc_for8 = true;
	}
	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_mc_read_u32(ufbxi_cache_context *cc, uint32_t *p_value)
{
	char buf[4]; // ufbxi_uninit
	ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, buf, 4, false));
	*p_value = (uint32_t)(uint8_t)buf[0]<<24u | (uint32_t)(uint8_t)buf[1]<<16 | (uint32_t)(uint8_t)buf[2]<<8u | (uint32_t)(uint8_t)buf[3];
	if (cc->mc_for8) {
		ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, buf, 4, false));
	}
	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_mc_read_u64(ufbxi_cache_context *cc, uint64_t *p_value)
{
	if (!cc->mc_for8) {
		uint32_t v32; // ufbxi_uninit
		ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u32(cc, &v32));
		*p_value = v32;
	} else {
		char buf[8]; // ufbxi_uninit
		ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, buf, 8, false));
		uint32_t hi = (uint32_t)(uint8_t)buf[0]<<24u | (uint32_t)(uint8_t)buf[1]<<16 | (uint32_t)(uint8_t)buf[2]<<8u | (uint32_t)(uint8_t)buf[3];
		uint32_t lo = (uint32_t)(uint8_t)buf[4]<<24u | (uint32_t)(uint8_t)buf[5]<<16 | (uint32_t)(uint8_t)buf[6]<<8u | (uint32_t)(uint8_t)buf[7];
		*p_value = (uint64_t)hi << 32u | (uint64_t)lo;
	}
	return 1;
}

static const uint8_t ufbxi_cache_data_format_size[] = {
	0, 4, 12, 8, 24,
};

ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_load_mc(ufbxi_cache_context *cc)
{
	uint32_t version = 0, time_start = 0, time_end = 0;
	uint32_t count = 0, time = 0;
	char skip_buf[8]; // ufbxi_uninit

	for (;;) {
		uint32_t tag; // ufbxi_uninit
		uint64_t size; // ufbxi_uninit
		ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_tag(cc, &tag));
		if (tag == 0) break;

		if (tag == ufbxi_cache_mc_tag('C','A','C','H') || tag == ufbxi_cache_mc_tag('M','Y','C','H')) {
			continue;
		}
		if (cc->mc_for8) {
			ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, skip_buf, 4, false));
		}

		ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u64(cc, &size));
		uint64_t begin = cc->file_offset;

		size_t alignment = cc->mc_for8 ? 8 : 4;

		ufbx_cache_data_format format = UFBX_CACHE_DATA_FORMAT_UNKNOWN;
		switch (tag) {
		case ufbxi_cache_mc_tag('F','O','R','4'): cc->mc_for8 = false; break;
		case ufbxi_cache_mc_tag('F','O','R','8'): cc->mc_for8 = true; break;
		case ufbxi_cache_mc_tag('V','R','S','N'): ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u32(cc, &version)); break;
		case ufbxi_cache_mc_tag('S','T','I','M'):
			ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u32(cc, &time_start));
			time = time_start;
			break;
		case ufbxi_cache_mc_tag('E','T','I','M'): ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u32(cc, &time_end)); break;
		case ufbxi_cache_mc_tag('T','I','M','E'): ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u32(cc, &time)); break;
		case ufbxi_cache_mc_tag('C','H','N','M'): {
			ufbxi_check_err(&cc->error, size > 0 && size < SIZE_MAX);
			size_t length = (size_t)size - 1;
			size_t padded_length = ((size_t)size + alignment - 1) & ~(alignment - 1);
			ufbxi_check_err(&cc->error, ufbxi_grow_array(cc->ator_tmp, &cc->name_buf, &cc->name_cap, padded_length));
			ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, cc->name_buf, padded_length, false));
			cc->channel_name.data = cc->name_buf;
			cc->channel_name.length = length;
			ufbxi_check_err(&cc->error, ufbxi_push_string_place_str(&cc->string_pool, &cc->channel_name, false));
		} break;
		case ufbxi_cache_mc_tag('S','I','Z','E'): ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u32(cc, &count)); break;
		case ufbxi_cache_mc_tag('F','V','C','A'): format = UFBX_CACHE_DATA_FORMAT_VEC3_FLOAT; break;
		case ufbxi_cache_mc_tag('D','V','C','A'): format = UFBX_CACHE_DATA_FORMAT_VEC3_DOUBLE; break;
		case ufbxi_cache_mc_tag('F','B','C','A'): format = UFBX_CACHE_DATA_FORMAT_REAL_FLOAT; break;
		case ufbxi_cache_mc_tag('D','B','C','A'): format = UFBX_CACHE_DATA_FORMAT_REAL_DOUBLE; break;
		case ufbxi_cache_mc_tag('D','B','L','A'): format = UFBX_CACHE_DATA_FORMAT_REAL_DOUBLE; break;
		default: ufbxi_fail_err(&cc->error, "Unknown tag");
		}

		if (format != UFBX_CACHE_DATA_FORMAT_UNKNOWN) {
			ufbx_cache_frame *frame = ufbxi_push_zero(&cc->tmp_stack, ufbx_cache_frame, 1);
			ufbxi_check_err(&cc->error, frame);

			uint32_t elem_size = ufbxi_cache_data_format_size[format];
			uint64_t total_size = (uint64_t)elem_size * (uint64_t)count;
			ufbxi_check_err(&cc->error, size >= elem_size * count);

			frame->channel = cc->channel_name;
			frame->time = (double)time * (1.0/6000.0);
			frame->filename = cc->stream_filename;
			frame->data_format = format;
			frame->data_encoding = UFBX_CACHE_DATA_ENCODING_BIG_ENDIAN;
			frame->data_offset = cc->file_offset;
			frame->data_count = count;
			frame->data_element_bytes = elem_size;
			frame->data_total_bytes = total_size;
			frame->file_format = UFBX_CACHE_FILE_FORMAT_MC;

			uint64_t end = begin + ((size + alignment - 1) & ~(uint64_t)(alignment - 1));
			ufbxi_check_err(&cc->error, end >= cc->file_offset);
			uint64_t left = end - cc->file_offset;
			ufbxi_check_err(&cc->error, ufbxi_cache_skip(cc, left));
		}
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_load_pc2(ufbxi_cache_context *cc)
{
	char header[32]; // ufbxi_uninit
	ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, header, sizeof(header), false));

	uint32_t version = ufbxi_read_u32(header + 12);
	uint32_t num_points = ufbxi_read_u32(header + 16);
	double start_frame = ufbxi_read_f32(header + 20);
	double frames_per_sample = ufbxi_read_f32(header + 24);
	uint32_t num_samples = ufbxi_read_u32(header + 28);

	(void)version;

	ufbx_cache_frame *frames = ufbxi_push_zero(&cc->tmp_stack, ufbx_cache_frame, num_samples);
	ufbxi_check_err(&cc->error, frames);

	uint64_t total_points = (uint64_t)num_points * (uint64_t)num_samples;
	ufbxi_check_err(&cc->error, total_points < UINT64_MAX / 12);

	uint64_t offset = cc->file_offset;

	// Skip almost to the end of the data and try to read one byte as there's
	// nothing after the data so we can't detect EOF..
	if (total_points > 0) {
		char last_byte[1]; // ufbxi_uninit
		ufbxi_check_err(&cc->error, ufbxi_cache_skip(cc, total_points * 12 - 1));
		ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, last_byte, 1, false));
	}

	for (uint32_t i = 0; i < num_samples; i++) {
		ufbx_cache_frame *frame = &frames[i];

		double sample_frame = start_frame + (double)i * frames_per_sample;
		frame->channel = cc->channel_name;
		frame->time = sample_frame / cc->frames_per_second;
		frame->filename = cc->stream_filename;
		frame->data_format = UFBX_CACHE_DATA_FORMAT_VEC3_FLOAT;
		frame->data_encoding = UFBX_CACHE_DATA_ENCODING_LITTLE_ENDIAN;
		frame->data_offset = offset;
		frame->data_count = num_points;
		frame->data_element_bytes = 12;
		frame->data_total_bytes = num_points * 12;
		frame->file_format = UFBX_CACHE_FILE_FORMAT_PC2;
		offset += num_points * 12;
	}

	return 1;
}

static ufbxi_noinline bool ufbxi_tmp_channel_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbxi_cache_tmp_channel *a = (const ufbxi_cache_tmp_channel *)va, *b = (const ufbxi_cache_tmp_channel *)vb;
	return ufbxi_str_less(a->name, b->name);
}

static ufbxi_noinline int ufbxi_cache_sort_tmp_channels(ufbxi_cache_context *cc, ufbxi_cache_tmp_channel *channels, size_t count)
{
	ufbxi_check_err(&cc->error, ufbxi_grow_array(cc->ator_tmp, &cc->tmp_arr, &cc->tmp_arr_size, count * sizeof(ufbxi_cache_tmp_channel)));
	ufbxi_stable_sort(sizeof(ufbxi_cache_tmp_channel), 16, channels, cc->tmp_arr, count, &ufbxi_tmp_channel_less, NULL);
	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_load_xml_imp(ufbxi_cache_context *cc, ufbxi_xml_document *doc)
{
	cc->xml_ticks_per_frame = 250;
	cc->xml_filename = cc->stream_filename;

	ufbxi_xml_tag *tag_root = ufbxi_xml_find_child(doc->root, "Autodesk_Cache_File");
	if (tag_root) {
		ufbxi_xml_tag *tag_type = ufbxi_xml_find_child(tag_root, "cacheType");
		ufbxi_xml_tag *tag_fps = ufbxi_xml_find_child(tag_root, "cacheTimePerFrame");
		ufbxi_xml_tag *tag_channels = ufbxi_xml_find_child(tag_root, "Channels");

		size_t num_extra = 0;
		ufbxi_for(ufbxi_xml_tag, tag, tag_root->children, tag_root->num_children) {
			if (tag->num_children != 1) continue;
			if (strcmp(tag->name.data, "extra") != 0) continue;
			ufbx_string *extra = ufbxi_push(&cc->tmp_stack, ufbx_string, 1);
			ufbxi_check_err(&cc->error, extra);
			*extra = tag->children[0].text;
			ufbxi_check_err(&cc->error, ufbxi_push_string_place_str(&cc->string_pool, extra, false));
			num_extra++;
		}
		cc->cache.extra_info.count = num_extra;
		cc->cache.extra_info.data = ufbxi_push_pop(&cc->result, &cc->tmp_stack, ufbx_string, num_extra);
		ufbxi_check_err(&cc->error, cc->cache.extra_info.data);

		if (tag_type) {
			ufbxi_xml_attrib *type = ufbxi_xml_find_attrib(tag_type, "Type");
			ufbxi_xml_attrib *format = ufbxi_xml_find_attrib(tag_type, "Format");
			if (type) {
				if (!strcmp(type->value.data, "OneFilePerFrame")) {
					cc->xml_type = UFBXI_CACHE_XML_TYPE_FILE_PER_FRAME;
				} else if (!strcmp(type->value.data, "OneFile")) {
					cc->xml_type = UFBXI_CACHE_XML_TYPE_SINGLE_FILE;
				}
			}
			if (format) {
				if (!strcmp(format->value.data, "mcc")) {
					cc->xml_format = UFBXI_CACHE_XML_FORMAT_MCC;
				} else if (!strcmp(format->value.data, "mcx")) {
					cc->xml_format = UFBXI_CACHE_XML_FORMAT_MCX;
				}
			}
		}

		if (tag_fps) {
			ufbxi_xml_attrib *fps = ufbxi_xml_find_attrib(tag_fps, "TimePerFrame");
			if (fps) {
				uint32_t value = ufbxi_parse_uint32_radix(fps->value.data, 10);
				if (value > 0) {
					cc->xml_ticks_per_frame = value;
				}
			}
		}

		if (tag_channels) {
			cc->channels = ufbxi_push_zero(&cc->tmp, ufbxi_cache_tmp_channel, tag_channels->num_children);
			ufbxi_check_err(&cc->error, cc->channels);

			ufbxi_for(ufbxi_xml_tag, tag, tag_channels->children, tag_channels->num_children) {
				ufbxi_xml_attrib *name = ufbxi_xml_find_attrib(tag, "ChannelName");
				ufbxi_xml_attrib *type = ufbxi_xml_find_attrib(tag, "ChannelType");
				ufbxi_xml_attrib *interpretation = ufbxi_xml_find_attrib(tag, "ChannelInterpretation");
				if (!(name && type && interpretation)) continue;

				ufbxi_cache_tmp_channel *channel = &cc->channels[cc->num_channels++];
				channel->name = name->value;
				channel->interpretation = interpretation->value;
				ufbxi_check_err(&cc->error, ufbxi_push_string_place_str(&cc->string_pool, &channel->name, false));
				ufbxi_check_err(&cc->error, ufbxi_push_string_place_str(&cc->string_pool, &channel->interpretation, false));

				ufbxi_xml_attrib *sampling_rate = ufbxi_xml_find_attrib(tag, "SamplingRate");
				ufbxi_xml_attrib *start_time = ufbxi_xml_find_attrib(tag, "StartTime");
				ufbxi_xml_attrib *end_time = ufbxi_xml_find_attrib(tag, "EndTime");
				if (sampling_rate && start_time && end_time) {
					channel->sample_rate = ufbxi_parse_uint32_radix(sampling_rate->value.data, 10);
					channel->start_time = ufbxi_parse_uint32_radix(start_time->value.data, 10);
					channel->end_time = ufbxi_parse_uint32_radix(end_time->value.data, 10);
					channel->current_time = channel->start_time;
					channel->try_load = true;
				}
			}
		}
	}

	ufbxi_check_err(&cc->error, ufbxi_cache_sort_tmp_channels(cc, cc->channels, cc->num_channels));
	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_load_xml(ufbxi_cache_context *cc)
{
	ufbxi_xml_load_opts opts = { 0 };
	opts.ator = cc->ator_tmp;
	opts.read_fn = cc->stream.read_fn;
	opts.read_user = cc->stream.user;
	opts.prefix = cc->pos;
	opts.prefix_length = ufbxi_to_size(cc->pos_end - cc->pos);
	ufbxi_xml_document *doc = ufbxi_load_xml(&opts, &cc->error);
	ufbxi_check_err(&cc->error, doc);

	int xml_ok = ufbxi_cache_load_xml_imp(cc, doc);
	ufbxi_free_xml(doc);
	ufbxi_check_err(&cc->error, xml_ok);

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_load_file(ufbxi_cache_context *cc, ufbx_string filename)
{
	cc->stream_filename = filename;
	ufbxi_check_err(&cc->error, ufbxi_push_string_place_str(&cc->string_pool, &cc->stream_filename, false));

	// Assume all files have at least 16 bytes of header
	size_t magic_len = cc->stream.read_fn(cc->stream.user, cc->buffer, 16);
	ufbxi_check_err_msg(&cc->error, magic_len <= 16, "IO error");
	ufbxi_check_err_msg(&cc->error, magic_len == 16, "Truncated file");
	cc->pos = cc->buffer;
	cc->pos_end = cc->buffer + 16;

	cc->file_offset = 0;

	if (!memcmp(cc->buffer, "POINTCACHE2", 11)) {
		ufbxi_check_err(&cc->error, ufbxi_cache_load_pc2(cc));
	} else if (!memcmp(cc->buffer, "FOR4", 4) || !memcmp(cc->buffer, "FOR8", 4)) {
		ufbxi_check_err(&cc->error, ufbxi_cache_load_mc(cc));
	} else {
		ufbxi_check_err(&cc->error, ufbxi_cache_load_xml(cc));
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_try_open_file(ufbxi_cache_context *cc, ufbx_string filename, const ufbx_blob *original_filename, bool *p_found)
{
	memset(&cc->stream, 0, sizeof(cc->stream));
	ufbxi_regression_assert(strlen(filename.data) == filename.length);
	if (!ufbxi_open_file(&cc->open_file_cb, &cc->stream, filename.data, filename.length, original_filename, cc->ator_tmp, UFBX_OPEN_FILE_GEOMETRY_CACHE)) {
		return 1;
	}

	int ok = ufbxi_cache_load_file(cc, filename);
	*p_found = true;

	if (cc->stream.close_fn) {
		cc->stream.close_fn(cc->stream.user);
	}

	return ok;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_load_frame_files(ufbxi_cache_context *cc)
{
	if (cc->xml_filename.length == 0) return 1;

	const char *extension = NULL;
	switch (cc->xml_format) {
	case UFBXI_CACHE_XML_FORMAT_MCC: extension = "mc"; break;
	case UFBXI_CACHE_XML_FORMAT_MCX: extension = "mcx"; break;
	default: return 1;
	}

	// Ensure worst case space for `path/filenameFrame123Tick456.mcx`
	size_t name_buf_len = cc->xml_filename.length + 64;
	char *name_buf = ufbxi_push(&cc->tmp, char, name_buf_len);
	ufbxi_check_err(&cc->error, name_buf);

	// Find the prefix before `.xml`
	size_t prefix_len = cc->xml_filename.length;
	for (size_t i = prefix_len; i > 0; --i) {
		if (cc->xml_filename.data[i - 1] == '.') {
			prefix_len = i - 1;
			break;
		}
	}
	memcpy(name_buf, cc->xml_filename.data, prefix_len);

	char *suffix_data = name_buf + prefix_len;
	size_t suffix_len = name_buf_len - prefix_len;

	ufbx_string filename;
	filename.data = name_buf;

	if (cc->xml_type == UFBXI_CACHE_XML_TYPE_SINGLE_FILE) {
		filename.length = prefix_len + (size_t)ufbxi_snprintf(suffix_data, suffix_len, ".%s", extension);
		bool found = false;
		ufbxi_check_err(&cc->error, ufbxi_cache_try_open_file(cc, filename, NULL, &found));
	} else if (cc->xml_type == UFBXI_CACHE_XML_TYPE_FILE_PER_FRAME) {
		uint32_t lowest_time = 0;
		for (;;) {
			// Find the first `time >= lowest_time` value that has data in some channel
			uint32_t time = UINT32_MAX;
			ufbxi_for(ufbxi_cache_tmp_channel, chan, cc->channels, cc->num_channels) {
				if (!chan->try_load || chan->consecutive_fails > 10) continue;
				uint32_t sample_rate = chan->sample_rate ? chan->sample_rate : cc->xml_ticks_per_frame;
				if (chan->current_time < lowest_time) {
					uint32_t delta = (lowest_time - chan->current_time - 1) / sample_rate;
					chan->current_time += delta * sample_rate;
					if (UINT32_MAX - chan->current_time >= sample_rate) {
						chan->current_time += sample_rate;
					} else {
						chan->try_load = false;
						continue;
					}
				}
				if (chan->current_time <= chan->end_time) {
					time = ufbxi_min32(time, chan->current_time);
				}
			}
			if (time == UINT32_MAX) break;

			// Try to load a file at the specified frame/tick
			uint32_t frame = time / cc->xml_ticks_per_frame;
			uint32_t tick = time % cc->xml_ticks_per_frame;
			if (tick == 0) {
				filename.length = prefix_len + (size_t)ufbxi_snprintf(suffix_data, suffix_len, "Frame%u.%s", frame, extension);
			} else {
				filename.length = prefix_len + (size_t)ufbxi_snprintf(suffix_data, suffix_len, "Frame%uTick%u.%s", frame, tick, extension);
			}
			bool found = false;
			ufbxi_check_err(&cc->error, ufbxi_cache_try_open_file(cc, filename, NULL, &found));

			// Update channel status
			ufbxi_for(ufbxi_cache_tmp_channel, chan, cc->channels, cc->num_channels) {
				if (chan->current_time == time) {
					chan->consecutive_fails = found ? 0 : chan->consecutive_fails + 1;
				}
			}

			lowest_time = time + 1;
		}
	}

	return 1;
}

static ufbxi_noinline bool ufbxi_cmp_cache_frame_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_cache_frame *a = (const ufbx_cache_frame *)va, *b = (const ufbx_cache_frame *)vb;
	if (a->channel.data != b->channel.data) {
		// Channel names should be interned
		ufbxi_regression_assert(!ufbxi_str_equal(a->channel, b->channel));
		return ufbxi_str_less(a->channel, b->channel);
	}
	return a->time < b->time;
}

static ufbxi_noinline int ufbxi_cache_sort_frames(ufbxi_cache_context *cc, ufbx_cache_frame *frames, size_t count)
{
	ufbxi_check_err(&cc->error, ufbxi_grow_array(cc->ator_tmp, &cc->tmp_arr, &cc->tmp_arr_size, count * sizeof(ufbx_cache_frame)));
	ufbxi_stable_sort(sizeof(ufbx_cache_frame), 16, frames, cc->tmp_arr, count, &ufbxi_cmp_cache_frame_less, NULL);
	return 1;
}

typedef struct {
	ufbx_cache_interpretation interpretation;
	const char *pattern;
} ufbxi_cache_interpretation_name;

static const ufbxi_cache_interpretation_name ufbxi_cache_interpretation_names[] = {
	{ UFBX_CACHE_INTERPRETATION_POINTS, "\\cpoints?" },
	{ UFBX_CACHE_INTERPRETATION_VERTEX_POSITION, "\\cpositions?" },
	{ UFBX_CACHE_INTERPRETATION_VERTEX_NORMAL, "\\cnormals?" },
};

static ufbxi_noinline int ufbxi_cache_setup_channels(ufbxi_cache_context *cc)
{
	ufbxi_cache_tmp_channel *tmp_chan = cc->channels, *tmp_end = ufbxi_add_ptr(tmp_chan, cc->num_channels);

	size_t begin = 0, num_channels = 0;
	while (begin < cc->cache.frames.count) {
		ufbx_cache_frame *frame = &cc->cache.frames.data[begin];
		size_t end = begin + 1;
		while (end < cc->cache.frames.count && cc->cache.frames.data[end].channel.data == frame->channel.data) {
			end++;
		}

		ufbx_cache_channel *chan = ufbxi_push_zero(&cc->tmp_stack, ufbx_cache_channel, 1);
		ufbxi_check_err(&cc->error, chan);

		chan->name = frame->channel;
		chan->interpretation_name = ufbx_empty_string;
		chan->frames.data = frame;
		chan->frames.count = end - begin;

		while (tmp_chan < tmp_end && ufbxi_str_less(tmp_chan->name, chan->name)) {
			tmp_chan++;
		}
		if (tmp_chan < tmp_end && ufbxi_str_equal(tmp_chan->name, chan->name)) {
			chan->interpretation_name = tmp_chan->interpretation;
		}

		if (frame->file_format == UFBX_CACHE_FILE_FORMAT_PC2) {
			chan->interpretation = UFBX_CACHE_INTERPRETATION_VERTEX_POSITION;
		} else {
			ufbxi_for(const ufbxi_cache_interpretation_name, name, ufbxi_cache_interpretation_names, ufbxi_arraycount(ufbxi_cache_interpretation_names)) {
				if (ufbxi_match(&chan->interpretation_name, name->pattern)) {
					chan->interpretation = name->interpretation;
					break;
				}
			}
		}

		ufbx_mirror_axis mirror_axis = UFBX_MIRROR_AXIS_NONE;
		ufbx_real scale_factor = 1.0f;
		if (chan->interpretation != UFBX_CACHE_INTERPRETATION_UNKNOWN) {
			mirror_axis = cc->opts.mirror_axis;
			if (cc->opts.use_scale_factor) {
				scale_factor = cc->opts.scale_factor;
			}
		}
		chan->mirror_axis = mirror_axis;
		chan->scale_factor = scale_factor;
		ufbxi_for_list(ufbx_cache_frame, f, chan->frames) {
			f->mirror_axis = mirror_axis;
			f->scale_factor = scale_factor;
		}

		num_channels++;
		begin = end;
	}

	cc->cache.channels.data = ufbxi_push_pop(&cc->result, &cc->tmp_stack, ufbx_cache_channel, num_channels);
	ufbxi_check_err(&cc->error, cc->cache.channels.data);
	cc->cache.channels.count = num_channels;

	return 1;
}


static ufbxi_noinline int ufbxi_cache_load_imp(ufbxi_cache_context *cc, ufbx_string filename)
{
	cc->tmp.ator = cc->ator_tmp;
	cc->tmp_stack.ator = cc->ator_tmp;

	cc->channel_name.data = ufbxi_empty_char;

	if (!cc->open_file_cb.fn) {
		cc->open_file_cb.fn = ufbx_default_open_file;
	}

	// Make sure the filename we pass to `open_file_fn()` is NULL-terminated
	char *filename_data = ufbxi_push(&cc->tmp, char, filename.length + 1);
	ufbxi_check_err(&cc->error, filename_data);
	memcpy(filename_data, filename.data, filename.length);
	filename_data[filename.length] = '\0';
	ufbx_string filename_copy = { filename_data, filename.length };

	// TODO: NULL termination!
	bool found = false;
	ufbxi_check_err(&cc->error, ufbxi_cache_try_open_file(cc, filename_copy, NULL, &found));
	if (!found) {
		ufbxi_set_err_info(&cc->error, filename.data, filename.length);
		ufbxi_fail_err_msg(&cc->error, "open_file_fn()", "File not found");
	}

	cc->cache.root_filename = cc->stream_filename;

	ufbxi_check_err(&cc->error, ufbxi_cache_load_frame_files(cc));

	size_t num_frames = cc->tmp_stack.num_items;
	cc->cache.frames.count = num_frames;
	cc->cache.frames.data = ufbxi_push_pop(&cc->result, &cc->tmp_stack, ufbx_cache_frame, num_frames);
	ufbxi_check_err(&cc->error, cc->cache.frames.data);

	ufbxi_check_err(&cc->error, ufbxi_cache_sort_frames(cc, cc->cache.frames.data, cc->cache.frames.count));
	ufbxi_check_err(&cc->error, ufbxi_cache_setup_channels(cc));

	// Must be last allocation!
	cc->imp = ufbxi_push(&cc->result, ufbxi_geometry_cache_imp, 1);
	ufbxi_check_err(&cc->error, cc->imp);

	ufbxi_init_ref(&cc->imp->refcount, UFBXI_CACHE_IMP_MAGIC, NULL);

	cc->imp->cache = cc->cache;
	cc->imp->magic = UFBXI_CACHE_IMP_MAGIC;
	cc->imp->owned_by_scene = cc->owned_by_scene;
	cc->imp->refcount.ator = cc->ator_result;
	cc->imp->refcount.buf = cc->result;
	cc->imp->refcount.buf.ator = &cc->imp->refcount.ator;
	cc->imp->string_buf = cc->string_pool.buf;
	cc->imp->string_buf.ator = &cc->imp->refcount.ator;

	return 1;
}

ufbxi_noinline static ufbx_geometry_cache *ufbxi_cache_load(ufbxi_cache_context *cc, ufbx_string filename)
{
	int ok = ufbxi_cache_load_imp(cc, filename);

	ufbxi_buf_free(&cc->tmp);
	ufbxi_buf_free(&cc->tmp_stack);
	ufbxi_free(cc->ator_tmp, char, cc->name_buf, cc->name_cap);
	ufbxi_free(cc->ator_tmp, char, cc->tmp_arr, cc->tmp_arr_size);
	if (!cc->owned_by_scene) {
		ufbxi_string_pool_temp_free(&cc->string_pool);
		ufbxi_free_ator(cc->ator_tmp);
	}

	if (ok) {
		return &cc->imp->cache;
	} else {
		ufbxi_fix_error_type(&cc->error, "Failed to load geometry cache", NULL);
		if (!cc->owned_by_scene) {
			ufbxi_buf_free(&cc->string_pool.buf);
			ufbxi_free_ator(&cc->ator_result);
		}
		return NULL;
	}
}

ufbxi_noinline static ufbx_geometry_cache *ufbxi_load_geometry_cache(ufbx_string filename, const ufbx_geometry_cache_opts *user_opts, ufbx_error *p_error)
{
	ufbx_geometry_cache_opts opts; // ufbxi_uninit
	if (user_opts) {
		opts = *user_opts;
	} else {
		memset(&opts, 0, sizeof(opts));
	}

	ufbxi_cache_context cc = { UFBX_ERROR_NONE };
	ufbxi_allocator ator_tmp = { 0 };
	ufbxi_init_ator(&cc.error, &ator_tmp, &opts.temp_allocator, "temp");
	ufbxi_init_ator(&cc.error, &cc.ator_result, &opts.result_allocator, "result");
	cc.ator_tmp = &ator_tmp;

	cc.opts = opts;

	cc.open_file_cb = opts.open_file_cb;

	cc.string_pool.error = &cc.error;
	ufbxi_map_init(&cc.string_pool.map, cc.ator_tmp, &ufbxi_map_cmp_string, NULL);
	cc.string_pool.buf.ator = &cc.ator_result;
	cc.string_pool.buf.unordered = true;
	cc.string_pool.initial_size = 64;
	cc.result.ator = &cc.ator_result;

	cc.frames_per_second = opts.frames_per_second > 0.0 ? opts.frames_per_second : 30.0;

	ufbx_geometry_cache *cache = ufbxi_cache_load(&cc, filename);
	if (p_error) {
		if (cache) {
			ufbxi_clear_error(p_error);
		} else {
			*p_error = cc.error;
		}
	}
	return cache;
}

static ufbxi_noinline void ufbxi_free_geometry_cache_imp(ufbxi_geometry_cache_imp *imp)
{
	ufbx_assert(imp->magic == UFBXI_CACHE_IMP_MAGIC);
	ufbxi_buf_free(&imp->string_buf);
}

#else

typedef struct {
	ufbxi_refcount refcount;
	uint32_t magic;
	bool owned_by_scene;
} ufbxi_geometry_cache_imp;

static ufbxi_noinline ufbx_geometry_cache *ufbxi_load_geometry_cache(ufbx_string filename, const ufbx_geometry_cache_opts *user_opts, ufbx_error *p_error)
{
	if (p_error) {
		memset(p_error, 0, sizeof(ufbx_error));
		ufbxi_fmt_err_info(p_error, "UFBX_ENABLE_GEOMETRY_CACHE");
		ufbxi_report_err_msg(p_error, "UFBXI_FEATURE_GEOMETRY_CACHE", "Feature disabled");
	}
	return NULL;
}

static ufbxi_forceinline void ufbxi_free_geometry_cache_imp(ufbxi_geometry_cache_imp *imp)
{
}

#endif

// -- External files

typedef enum {
	UFBXI_EXTERNAL_FILE_GEOMETRY_CACHE,
} ufbxi_external_file_type;

typedef struct {
	ufbxi_external_file_type type;
	ufbx_string filename;
	ufbx_string absolute_filename;
	size_t index;
	void *data;
	size_t data_size;
} ufbxi_external_file;

static bool ufbxi_less_external_file(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbxi_external_file *a = (const ufbxi_external_file*)va, *b = (const ufbxi_external_file*)vb;
	if (a->type != b->type) return a->type < b->type;
	int cmp = ufbxi_str_cmp(a->filename, b->filename);
	if (cmp != 0) return cmp < 0;
	if (a->index != b->index) return a->index < b->index;
	return false;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_external_cache(ufbxi_context *uc, ufbxi_external_file *file)
{
#if UFBXI_FEATURE_GEOMETRY_CACHE
	ufbxi_cache_context cc = { UFBX_ERROR_NONE };
	cc.owned_by_scene = true;

	cc.open_file_cb = uc->opts.open_file_cb;
	cc.frames_per_second = uc->scene.settings.frames_per_second;

	// Temporarily "borrow" allocators for the geometry cache
	cc.ator_tmp = &uc->ator_tmp;
	cc.string_pool = uc->string_pool;
	cc.result = uc->result;

	cc.opts.mirror_axis = uc->mirror_axis;
	cc.opts.use_scale_factor = true;
	cc.opts.scale_factor = uc->scene.metadata.geometry_scale;

	ufbx_geometry_cache *cache = ufbxi_cache_load(&cc, file->filename);
	if (!cache) {
		if (cc.error.type == UFBX_ERROR_FILE_NOT_FOUND) {
			memset(&cc.error, 0, sizeof(cc.error));
			cache = ufbxi_cache_load(&cc, file->absolute_filename);
		}
	}

	// Return the "borrowed" allocators
	uc->string_pool = cc.string_pool;
	uc->result = cc.result;

	if (!cache) {
		if (cc.error.type == UFBX_ERROR_FILE_NOT_FOUND) {
			if (uc->opts.ignore_missing_external_files) {
				ufbxi_check(ufbxi_warnf(UFBX_WARNING_MISSING_EXTERNAL_FILE, "Failed to open geometry cache: %s", file->filename.data));
				return 1;
			} else {
				cc.error.type = UFBX_ERROR_EXTERNAL_FILE_NOT_FOUND;
				cc.error.description.data = "External file not found";
				cc.error.description.length = strlen("External file not found");
			}
		}

		uc->error = cc.error;
		return 0;
	}

	file->data = cache;
	return 1;
#else
	if (uc->opts.ignore_missing_external_files) return 1;

	ufbxi_fmt_err_info(&uc->error, "UFBX_ENABLE_GEOMETRY_CACHE");
	ufbxi_fail_msg("UFBXI_FEATURE_GEOMETRY_CACHE", "Feature disabled");
#endif
}

static ufbxi_noinline ufbxi_external_file *ufbxi_find_external_file(ufbxi_external_file *files, size_t num_files, ufbxi_external_file_type type, const char *name)
{
	size_t ix = SIZE_MAX;
	ufbxi_macro_lower_bound_eq(ufbxi_external_file, 32, &ix, files, 0, num_files,
		( type != a->type ? type < a->type : strcmp(a->filename.data, name) < 0 ),
		( a->type == type && a->filename.data == name ));
	return ix != SIZE_MAX ? &files[ix] : NULL;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_external_files(ufbxi_context *uc)
{
	size_t num_files = 0;

	// Gather external files to deduplicate them
	ufbxi_for_ptr_list(ufbx_cache_file, p_cache, uc->scene.cache_files) {
		ufbx_cache_file *cache = *p_cache;
		if (cache->filename.length > 0) {
			ufbxi_external_file *file = ufbxi_push_zero(&uc->tmp_stack, ufbxi_external_file, 1);
			ufbxi_check(file);
			file->index = num_files++;
			file->type = UFBXI_EXTERNAL_FILE_GEOMETRY_CACHE;
			file->filename = cache->filename;
			file->absolute_filename = cache->absolute_filename;
		}
	}

	// Sort and load the external files
	ufbxi_external_file *files = ufbxi_push_pop(&uc->tmp, &uc->tmp_stack, ufbxi_external_file, num_files);
	ufbxi_check(files);
	ufbxi_unstable_sort(files, num_files, sizeof(ufbxi_external_file), &ufbxi_less_external_file, NULL);

	ufbxi_external_file_type prev_type = UFBXI_EXTERNAL_FILE_GEOMETRY_CACHE;
	const char *prev_name = NULL;
	ufbxi_for(ufbxi_external_file, file, files, num_files) {
		if (file->filename.data == prev_name && file->type == prev_type) continue;
		if (file->type == UFBXI_EXTERNAL_FILE_GEOMETRY_CACHE) {
			ufbxi_check(ufbxi_load_external_cache(uc, file));
		}
		prev_name = file->filename.data;
		prev_type = file->type;
	}

	// Patch the loaded files
	ufbxi_for_ptr_list(ufbx_cache_file, p_cache, uc->scene.cache_files) {
		ufbx_cache_file *cache = *p_cache;
		ufbxi_external_file *file = ufbxi_find_external_file(files, num_files,
			UFBXI_EXTERNAL_FILE_GEOMETRY_CACHE, cache->filename.data);
		if (file && file->data) {
			cache->external_cache = (ufbx_geometry_cache*)file->data;
		}
	}

	// Patch the geometry deformers
	ufbxi_for_ptr_list(ufbx_cache_deformer, p_deformer, uc->scene.cache_deformers) {
		ufbx_cache_deformer *deformer = *p_deformer;
		if (!deformer->file || !deformer->file->external_cache) continue;
		ufbx_geometry_cache *cache = deformer->file->external_cache;
		deformer->external_cache = cache;

		// HACK: It seems like channels may be connected even if the name is wrong
		// and they work when exporting from Marvelous to Maya...
		if (cache->channels.count == 1) {
			deformer->external_channel = &cache->channels.data[0];
		} else {
			ufbx_string channel = deformer->channel;
			size_t ix = SIZE_MAX;
			ufbxi_macro_lower_bound_eq(ufbx_cache_channel, 16, &ix, cache->channels.data, 0, cache->channels.count,
				( ufbxi_str_less(a->name, channel) ), ( a->name.data == channel.data ));
			if (ix != SIZE_MAX) {
				deformer->external_channel = &cache->channels.data[ix];
			}
		}
	}

	return 1;
}

static ufbxi_noinline void ufbxi_transform_to_axes(ufbxi_context *uc, ufbx_coordinate_axes dst_axes)
{
	if (!ufbx_coordinate_axes_valid(uc->scene.settings.axes)) return;
	if (!ufbxi_axis_matrix(&uc->axis_matrix, uc->scene.settings.axes, dst_axes)) return;

	if (ufbx_matrix_determinant(&uc->axis_matrix) < 0.0f) {
		if (uc->opts.handedness_conversion_axis != UFBX_MIRROR_AXIS_NONE) {
			ufbx_mirror_axis mirror_axis = uc->opts.handedness_conversion_axis;
			uc->mirror_axis = mirror_axis;
			uc->scene.metadata.mirror_axis = uc->mirror_axis;

			ufbxi_mirror_matrix_dst(&uc->axis_matrix, uc->mirror_axis);
			ufbxi_dev_assert(ufbx_matrix_determinant(&uc->axis_matrix) >= 0.0f);

			ufbxi_for_ptr_list(ufbx_node, p_node, uc->scene.nodes) {
				ufbx_node *node = *p_node;
				if (!node->is_root) {
					node->adjust_mirror_axis = mirror_axis;
				}
			}
		}
	}

	if (uc->opts.space_conversion == UFBX_SPACE_CONVERSION_TRANSFORM_ROOT) {
		ufbx_matrix axis_mat = uc->axis_matrix;
		if (!ufbxi_is_transform_identity(&uc->scene.root_node->local_transform)) {
			ufbx_matrix root_mat = ufbx_transform_to_matrix(&uc->scene.root_node->local_transform);
			axis_mat = ufbx_matrix_mul(&root_mat, &axis_mat);
		}

		ufbxi_mirror_matrix(&axis_mat, uc->mirror_axis);

		uc->scene.root_node->local_transform = ufbx_matrix_to_transform(&axis_mat);
		uc->scene.root_node->node_to_parent = axis_mat;
	}
}

static ufbxi_noinline int ufbxi_scale_units(ufbxi_context *uc, ufbx_real target_meters)
{
	if (uc->scene.settings.unit_meters <= 0.0f) return 1;
	target_meters = ufbxi_round_if_near(ufbxi_pow10_targets, ufbxi_arraycount(ufbxi_pow10_targets), target_meters);

	ufbx_real ratio = uc->scene.settings.unit_meters / target_meters;
	ratio = ufbxi_round_if_near(ufbxi_pow10_targets, ufbxi_arraycount(ufbxi_pow10_targets), ratio);
	if (ratio == 1.0f) return 1;

	uc->unit_scale = ratio;

	if (uc->opts.space_conversion == UFBX_SPACE_CONVERSION_TRANSFORM_ROOT) {
		uc->scene.root_node->local_transform.scale.x *= ratio;
		uc->scene.root_node->local_transform.scale.y *= ratio;
		uc->scene.root_node->local_transform.scale.z *= ratio;
		uc->scene.root_node->node_to_parent.m00 *= ratio;
		uc->scene.root_node->node_to_parent.m01 *= ratio;
		uc->scene.root_node->node_to_parent.m02 *= ratio;
		uc->scene.root_node->node_to_parent.m10 *= ratio;
		uc->scene.root_node->node_to_parent.m11 *= ratio;
		uc->scene.root_node->node_to_parent.m12 *= ratio;
		uc->scene.root_node->node_to_parent.m20 *= ratio;
		uc->scene.root_node->node_to_parent.m21 *= ratio;
		uc->scene.root_node->node_to_parent.m22 *= ratio;
	}

	return 1;
}

// -- Curve evaluation

static ufbxi_forceinline double ufbxi_find_cubic_bezier_t(double p1, double p2, double x0)
{
	double p1_3 = p1 * 3.0, p2_3 = p2 * 3.0;
	double a = p1_3 - p2_3 + 1.0;
	double b = p2_3 - p1_3 - p1_3;
	double c = p1_3;

	double a_3 = 3.0*a, b_2 = 2.0*b;
	double t = x0;
	double x1, t2, t3;

	// Manually unroll three iterations of Newton-Rhapson, this is enough
	// for most tangents
	t2 = t*t; t3 = t2*t; x1 = a*t3 + b*t2 + c*t - x0;
	t -= x1 / (a_3*t2 + b_2*t + c);

	t2 = t*t; t3 = t2*t; x1 = a*t3 + b*t2 + c*t - x0;
	t -= x1 / (a_3*t2 + b_2*t + c);

	t2 = t*t; t3 = t2*t; x1 = a*t3 + b*t2 + c*t - x0;
	t -= x1 / (a_3*t2 + b_2*t + c);

	// 4 ULP from 1.0
	const double eps = 8.881784197001252e-16;
	if (ufbx_fabs(x1) <= eps) return t;

	// Perform more iterations until we reach desired accuracy
	for (size_t i = 0; i < 4; i++) {

		t2 = t*t; t3 = t2*t; x1 = a*t3 + b*t2 + c*t - x0;
		t -= x1 / (a_3*t2 + b_2*t + c);

		t2 = t*t; t3 = t2*t; x1 = a*t3 + b*t2 + c*t - x0;
		t -= x1 / (a_3*t2 + b_2*t + c);

		if (ufbx_fabs(x1) <= eps) return t;
	}

	return t;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_evaluate_skinning(ufbx_scene *scene, ufbx_error *error, ufbxi_buf *buf_result, ufbxi_buf *buf_tmp,
	double time, bool load_caches, ufbx_geometry_cache_data_opts *cache_opts)
{
#if UFBXI_FEATURE_SKINNING_EVALUATION
	size_t max_skinned_indices = 0;

	ufbxi_for_ptr_list(ufbx_mesh, p_mesh, scene->meshes) {
		ufbx_mesh *mesh = *p_mesh;
		if (mesh->blend_deformers.count == 0 && mesh->skin_deformers.count == 0 && (mesh->cache_deformers.count == 0 || !load_caches)) continue;
		max_skinned_indices = ufbxi_max_sz(max_skinned_indices, mesh->num_indices);
	}

	ufbx_topo_edge *topo = ufbxi_push(buf_tmp, ufbx_topo_edge, max_skinned_indices);
	ufbxi_check_err(error, topo);

	ufbxi_for_ptr_list(ufbx_mesh, p_mesh, scene->meshes) {
		ufbx_mesh *mesh = *p_mesh;
		if (mesh->blend_deformers.count == 0 && mesh->skin_deformers.count == 0 && (mesh->cache_deformers.count == 0 || !load_caches)) continue;
		if (mesh->num_vertices == 0) continue;

		size_t num_vertices = mesh->num_vertices;
		ufbx_vec3 *result_pos = ufbxi_push(buf_result, ufbx_vec3, num_vertices + 1);
		ufbxi_check_err(error, result_pos);

		result_pos[0] = ufbx_zero_vec3;
		result_pos++;

		bool cached_position = false, cached_normals = false;
		if (load_caches && mesh->cache_deformers.count > 0) {
			ufbxi_for_ptr_list(ufbx_cache_deformer, p_cache, mesh->cache_deformers) {
				ufbx_cache_channel *channel = (*p_cache)->external_channel;
				if (!channel) continue;

				if ((channel->interpretation == UFBX_CACHE_INTERPRETATION_VERTEX_POSITION || channel->interpretation == UFBX_CACHE_INTERPRETATION_POINTS) && !cached_position) {
					size_t num_read = ufbx_sample_geometry_cache_vec3(channel, time, result_pos, num_vertices, cache_opts);
					if (num_read == num_vertices) {
						mesh->skinned_is_local = true;
						cached_position = true;
					}
				} else if (channel->interpretation == UFBX_CACHE_INTERPRETATION_VERTEX_NORMAL && !cached_normals) {
					// TODO: Is this right at all?
					size_t num_normals = mesh->skinned_normal.values.count;
					ufbx_vec3 *normal_data = ufbxi_push(buf_result, ufbx_vec3, num_normals + 1);
					ufbxi_check_err(error, normal_data);
					normal_data[0] = ufbx_zero_vec3;
					normal_data++;

					size_t num_read = ufbx_sample_geometry_cache_vec3(channel, time, normal_data, num_normals, cache_opts);
					if (num_read == num_normals) {
						cached_normals = true;
						mesh->skinned_normal.values.data = normal_data;
					}
				}
			}
		}

		if (!cached_position) {
			memcpy(result_pos, mesh->vertices.data, num_vertices * sizeof(ufbx_vec3));

			ufbxi_for_ptr_list(ufbx_blend_deformer, p_blend, mesh->blend_deformers) {
				ufbx_add_blend_vertex_offsets(*p_blend, result_pos, num_vertices, 1.0f);
			}

			// TODO: What should we do about multiple skins??
			if (mesh->skin_deformers.count > 0) {
				ufbx_matrix *fallback = mesh->instances.count > 0 ? &mesh->instances.data[0]->geometry_to_world : NULL;
				ufbx_skin_deformer *skin = mesh->skin_deformers.data[0];
				for (size_t i = 0; i < num_vertices; i++) {
					ufbx_matrix mat = ufbx_get_skin_vertex_matrix(skin, i, fallback);
					result_pos[i] = ufbx_transform_position(&mat, result_pos[i]);
				}

				mesh->skinned_is_local = false;
			}
		}

		mesh->skinned_position.values.data = result_pos;

		if (!cached_normals) {
			size_t num_indices = mesh->num_indices;
			uint32_t *normal_indices = ufbxi_push(buf_result, uint32_t, num_indices);
			ufbxi_check_err(error, normal_indices);

			ufbx_compute_topology(mesh, topo, num_indices);
			size_t num_normals = ufbx_generate_normal_mapping(mesh, topo, num_indices, normal_indices, num_indices, false);

			if (num_normals == mesh->num_vertices) {
				mesh->skinned_normal.unique_per_vertex = true;
			}

			ufbx_vec3 *normal_data = ufbxi_push(buf_result, ufbx_vec3, num_normals + 1);
			ufbxi_check_err(error, normal_data);

			normal_data[0] = ufbx_zero_vec3;
			normal_data++;

			ufbx_compute_normals(mesh, &mesh->skinned_position, normal_indices, num_indices, normal_data, num_normals);

			mesh->generated_normals = true;
			mesh->skinned_normal.exists = true;
			mesh->skinned_normal.values.data = normal_data;
			mesh->skinned_normal.values.count = num_normals;
			mesh->skinned_normal.indices.data = normal_indices;
			mesh->skinned_normal.indices.count = num_indices;
			mesh->skinned_normal.value_reals = 3;
		}
	}

	return 1;
#else
	ufbxi_fmt_err_info(error, "UFBX_ENABLE_SKINNING_EVALUATION");
	ufbxi_report_err_msg(error, "UFBXI_FEATURE_SKINNING_EVALUATION", "Feature disabled");
	return 0;
#endif
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_fixup_opts_string(ufbxi_context *uc, ufbx_string *str, bool push)
{
	if (str->length > 0) {
		if (str->length == SIZE_MAX) {
			str->length = str->data ? strlen(str->data) : 0;
		}
		if (push) {
			ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, str, false));
		}
	} else {
		str->data = ufbxi_empty_char;
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_resolve_warning_elements(ufbxi_context *uc)
{
	size_t num_elements = uc->tmp_element_id.num_items;
	uint32_t *element_ids = ufbxi_push_pop(&uc->tmp, &uc->tmp_element_id, uint32_t, num_elements);
	ufbxi_check(element_ids);

	ufbxi_for_list(ufbx_warning, warning, uc->scene.metadata.warnings) {
		uint32_t element_id = warning->element_id;
		// Decode `element_id`, see HACK(warning-element) in `ufbxi_vwarnf_imp()` for the encoding.
		if ((element_id & 0x80000000u) != 0 && element_id != ~0u) {
			warning->element_id = element_ids[element_id & ~0x80000000u];
		}
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_imp(ufbxi_context *uc)
{
	// Check for deferred failure
	if (uc->deferred_failure) return 0;
	if (uc->deferred_load) {
		ufbx_stream stream = { 0 };
		ufbx_open_file_opts opts = { 0 };
		const char *filename = uc->load_filename;
		size_t filename_len = uc->load_filename_len;
		bool ok = false;
		if (filename_len == SIZE_MAX) {
			opts.filename_null_terminated = true;
			filename_len = strlen(filename);
		}
		if (uc->opts.filename.length == 0 || uc->opts.filename.data == NULL) {
			uc->opts.filename.data = filename;
			uc->opts.filename.length = filename_len;
		}
		ufbx_error error;
		error.type = UFBX_ERROR_NONE;
		if (uc->opts.open_main_file_with_default || uc->opts.open_file_cb.fn == &ufbx_default_open_file) {
			ufbx_open_file_context ctx = (ufbx_open_file_context)&uc->ator_tmp;
			ok = ufbx_open_file_ctx(&stream, ctx, filename, filename_len, &opts, &error);
		} else {
			ok = ufbxi_open_file(&uc->opts.open_file_cb, &stream, uc->load_filename, filename_len, NULL, &uc->ator_tmp, UFBX_OPEN_FILE_MAIN_MODEL);
		}
		if (!ok) {
			if (error.type != UFBX_ERROR_NONE) {
				// cppcheck-suppress uninitStructMember
				uc->error = error;
			} else {
				ufbxi_set_err_info(&uc->error, filename, filename_len);
			}
			ufbxi_fail_msg("open_file_fn()", "File not found");
		}
		uc->read_fn = stream.read_fn;
		uc->skip_fn = stream.skip_fn;
		uc->size_fn = stream.size_fn;
		uc->close_fn = stream.close_fn;
		uc->read_user = stream.user;
	}

	if (uc->opts.progress_cb.fn && uc->progress_bytes_total == 0 && uc->size_fn) {
		uint64_t total = uc->size_fn(uc->read_user);
		ufbxi_check(total != UINT64_MAX);
		uc->progress_bytes_total = total;
	}

	ufbxi_check(uc->opts.path_separator >= 0x20 && uc->opts.path_separator <= 0x7e);

	ufbxi_check(ufbxi_fixup_opts_string(uc, &uc->opts.filename, false));
	ufbxi_check(ufbxi_fixup_opts_string(uc, &uc->opts.obj_mtl_path, true));
	ufbxi_check(ufbxi_fixup_opts_string(uc, &uc->opts.geometry_transform_helper_name, true));
	ufbxi_check(ufbxi_fixup_opts_string(uc, &uc->opts.scale_helper_name, true));

	ufbxi_check(ufbxi_thread_pool_init(&uc->thread_pool, &uc->error, &uc->ator_tmp, &uc->opts.thread_opts));

	if (!uc->opts.allow_unsafe) {
		ufbxi_check_msg(uc->opts.index_error_handling != UFBX_INDEX_ERROR_HANDLING_UNSAFE_IGNORE, "Unsafe options");
		ufbxi_check_msg(uc->opts.unicode_error_handling != UFBX_UNICODE_ERROR_HANDLING_UNSAFE_IGNORE, "Unsafe options");
	} else {
		uc->scene.metadata.is_unsafe = true;
	}

	if (uc->opts.index_error_handling == UFBX_INDEX_ERROR_HANDLING_NO_INDEX) {
		uc->scene.metadata.may_contain_no_index = true;
	}

	uc->retain_mesh_parts = !uc->opts.ignore_geometry && !uc->opts.skip_mesh_parts;
	uc->scene.metadata.may_contain_missing_vertex_position = uc->opts.allow_missing_vertex_position;
	uc->scene.metadata.may_contain_broken_elements = uc->opts.connect_broken_elements;

	uc->scene.metadata.creator.data = ufbxi_empty_char;

	uc->unit_scale = 1.0f;
	if (uc->data == NULL) {
		ufbxi_dev_assert(uc->data_begin == NULL);
		uc->data_begin = uc->data = ufbxi_zero_size_buffer;
	}

	uc->retain_vertex_w = (uc->opts.retain_dom || uc->opts.retain_vertex_attrib_w) && !uc->opts.ignore_geometry;

	ufbxi_check(ufbxi_load_strings(uc));
	ufbxi_check(ufbxi_load_maps(uc));
	ufbxi_check(ufbxi_determine_format(uc));

	ufbx_file_format format = uc->scene.metadata.file_format;

	if (format == UFBX_FILE_FORMAT_FBX) {
		ufbxi_check(ufbxi_begin_parse(uc));
		if (uc->version < 6000) {
			ufbxi_check(ufbxi_read_legacy_root(uc));
		} else {
			ufbxi_check(ufbxi_read_root(uc));
		}
		ufbxi_update_scene_metadata(&uc->scene.metadata);
		ufbxi_check(ufbxi_init_file_paths(uc));
	} else if (format == UFBX_FILE_FORMAT_OBJ) {
		ufbxi_check(ufbxi_obj_load(uc));
		ufbxi_update_scene_metadata(&uc->scene.metadata);
	} else if (format == UFBX_FILE_FORMAT_MTL) {
		ufbxi_check(ufbxi_mtl_load(uc));
		ufbxi_update_scene_metadata(&uc->scene.metadata);
	}

	// Fake DOM root if necessary
	if (uc->opts.retain_dom && !uc->scene.dom_root) {
		ufbx_dom_node *dom_root = ufbxi_push_zero(&uc->result, ufbx_dom_node, 1);
		ufbxi_check(dom_root);
		dom_root->name.data = ufbxi_empty_char;
		uc->scene.dom_root = dom_root;
	}

	ufbxi_check(ufbxi_pre_finalize_scene(uc));

	// We can free `tmp_parse` already here as all parsing is done by now.
	ufbxi_buf_free(&uc->tmp_parse);

	ufbxi_check(ufbxi_finalize_scene(uc));

	ufbxi_update_scene_settings(&uc->scene.settings);
	if (uc->scene.metadata.file_format == UFBX_FILE_FORMAT_OBJ) {
		ufbxi_update_scene_settings_obj(uc);
	}

	// Axis conversion
	if (ufbx_coordinate_axes_valid(uc->opts.target_axes)) {
		ufbxi_transform_to_axes(uc, uc->opts.target_axes);
	}

	// Unit conversion
	if (uc->opts.target_unit_meters > 0.0f) {
		ufbxi_check(ufbxi_scale_units(uc, uc->opts.target_unit_meters));
	}

	// TODO: This could be done in evaluate as well with refactoring
	ufbxi_update_adjust_transforms(uc, &uc->scene);

	ufbxi_check(ufbxi_modify_geometry(uc));
	ufbxi_postprocess_scene(uc);

	ufbxi_update_scene(&uc->scene, true, NULL, 0);

	// Force a non-NULL anim pointer
	if (!uc->scene.anim) {
		uc->scene.anim = ufbxi_push_zero(&uc->result, ufbx_anim, 1);
	}

	if (uc->opts.load_external_files) {
		ufbxi_check(ufbxi_load_external_files(uc));
	}

	// Evaluate skinning if requested
	if (uc->opts.evaluate_skinning) {
		ufbx_geometry_cache_data_opts cache_opts = { 0 };
		cache_opts.open_file_cb = uc->opts.open_file_cb;
		ufbxi_check(ufbxi_evaluate_skinning(&uc->scene, &uc->error, &uc->result, &uc->tmp,
			0.0, uc->opts.load_external_files && uc->opts.evaluate_caches, &cache_opts));
	}

	// Pop warnings to metadata
	ufbxi_check(ufbxi_pop_warnings(&uc->warnings, &uc->scene.metadata.warnings, uc->scene.metadata.has_warning));
	ufbxi_check(ufbxi_resolve_warning_elements(uc));

	// Copy local data to the scene
	uc->scene.metadata.version = uc->version;
	uc->scene.metadata.ascii = uc->from_ascii;
	uc->scene.metadata.big_endian = uc->file_big_endian;
	uc->scene.metadata.geometry_ignored = uc->opts.ignore_geometry;
	uc->scene.metadata.animation_ignored = uc->opts.ignore_animation;
	uc->scene.metadata.embedded_ignored = uc->opts.ignore_embedded;

	// Retain the scene, this must be the final allocation as we copy
	// `ator_result` to `ufbx_scene_imp`.
	ufbxi_scene_imp *imp = ufbxi_push(&uc->result, ufbxi_scene_imp, 1);
	ufbxi_check(imp);

	ufbxi_init_ref(&imp->refcount, UFBXI_SCENE_IMP_MAGIC, NULL);

	imp->magic = UFBXI_SCENE_IMP_MAGIC;
	imp->scene = uc->scene;
	imp->refcount.ator = uc->ator_result;
	imp->refcount.ator.error = NULL;

	// Copy retained buffers and translate the allocator struct to the one
	// contained within `ufbxi_scene_imp`
	imp->refcount.buf = uc->result;
	imp->refcount.buf.ator = &imp->refcount.ator;
	imp->string_buf = uc->string_pool.buf;
	imp->string_buf.ator = &imp->refcount.ator;

	imp->scene.metadata.result_memory_used = imp->refcount.ator.current_size;
	imp->scene.metadata.temp_memory_used = uc->ator_tmp.current_size;
	imp->scene.metadata.result_allocs = imp->refcount.ator.num_allocs;
	imp->scene.metadata.temp_allocs = uc->ator_tmp.num_allocs;

	ufbxi_for_ptr_list(ufbx_element, p_elem, imp->scene.elements) {
		(*p_elem)->scene = &imp->scene;
	}

	uc->scene_imp = imp;

	return 1;
}

static ufbxi_noinline void ufbxi_free_temp(ufbxi_context *uc)
{
	ufbxi_thread_pool_free(&uc->thread_pool);

	ufbxi_string_pool_temp_free(&uc->string_pool);
	ufbxi_buf_free(&uc->warnings.tmp_stack);

	ufbxi_map_free(&uc->prop_type_map);
	ufbxi_map_free(&uc->fbx_id_map);
	ufbxi_map_free(&uc->texture_file_map);
	ufbxi_map_free(&uc->anim_stack_map);
	ufbxi_map_free(&uc->fbx_attr_map);
	ufbxi_map_free(&uc->node_prop_set);
	ufbxi_map_free(&uc->dom_node_map);

	ufbxi_buf_free(&uc->tmp);
	ufbxi_buf_free(&uc->tmp_parse);
	for (size_t i = 0; i < UFBX_THREAD_GROUP_COUNT; i++) {
		ufbxi_buf_free(&uc->tmp_thread_parse[i]);
	}
	ufbxi_buf_free(&uc->tmp_stack);
	ufbxi_buf_free(&uc->tmp_connections);
	ufbxi_buf_free(&uc->tmp_node_ids);
	ufbxi_buf_free(&uc->tmp_elements);
	ufbxi_buf_free(&uc->tmp_element_offsets);
	ufbxi_buf_free(&uc->tmp_element_fbx_ids);
	ufbxi_buf_free(&uc->tmp_element_ptrs);
	for (size_t i = 0; i < UFBX_ELEMENT_TYPE_COUNT; i++) {
		ufbxi_buf_free(&uc->tmp_typed_element_offsets[i]);
	}
	ufbxi_buf_free(&uc->tmp_mesh_textures);
	ufbxi_buf_free(&uc->tmp_full_weights);
	ufbxi_buf_free(&uc->tmp_dom_nodes);
	ufbxi_buf_free(&uc->tmp_element_id);
	ufbxi_buf_free(&uc->tmp_ascii_spans);

	ufbxi_free(&uc->ator_tmp, ufbxi_node, uc->top_nodes, uc->top_nodes_cap);
	ufbxi_free(&uc->ator_tmp, void*, uc->element_extra_arr, uc->element_extra_cap);

	ufbxi_free(&uc->ator_tmp, char, uc->ascii.token.str_data, uc->ascii.token.str_cap);
	ufbxi_free(&uc->ator_tmp, char, uc->ascii.prev_token.str_data, uc->ascii.prev_token.str_cap);

	ufbxi_free(&uc->ator_tmp, char, uc->read_buffer, uc->read_buffer_size);
	ufbxi_free(&uc->ator_tmp, char, uc->tmp_arr, uc->tmp_arr_size);
	ufbxi_free(&uc->ator_tmp, char, uc->swap_arr, uc->swap_arr_size);

	ufbxi_obj_free(uc);

	ufbxi_free_ator(&uc->ator_tmp);
}

static ufbxi_noinline void ufbxi_free_result(ufbxi_context *uc)
{
	ufbxi_buf_free(&uc->result);
	ufbxi_buf_free(&uc->string_pool.buf);

	ufbxi_free_ator(&uc->ator_result);
}

static ufbxi_noinline ufbx_scene *ufbxi_load(ufbxi_context *uc, const ufbx_load_opts *user_opts, ufbx_error *p_error)
{
	// Test endianness
	{
		uint8_t buf[2];
		uint16_t val = 0xbbaa;
		memcpy(buf, &val, 2);
		uc->local_big_endian = buf[0] == 0xbb;
	}

	uc->double_parse_flags = ufbxi_parse_double_init_flags();

	if (user_opts) {
		uc->opts = *user_opts;
	} else {
		memset(&uc->opts, 0, sizeof(uc->opts));
	}

	if (uc->opts.file_size_estimate) {
		uc->progress_bytes_total = uc->opts.file_size_estimate;
	}

	if (uc->opts.ignore_all_content) {
		uc->opts.ignore_geometry = true;
		uc->opts.ignore_animation = true;
		uc->opts.ignore_embedded = true;
	}

	ufbx_inflate_retain inflate_retain;
	inflate_retain.initialized = false;

	ufbxi_init_ator(&uc->error, &uc->ator_tmp, &uc->opts.temp_allocator, "temp");
	ufbxi_init_ator(&uc->error, &uc->ator_result, &uc->opts.result_allocator, "result");

	if (uc->opts.read_buffer_size == 0) {
		uc->opts.read_buffer_size = 0x4000;
	}

	if (uc->opts.file_format_lookahead == 0) {
		uc->opts.file_format_lookahead = 0x4000;
	} else if (uc->opts.file_format_lookahead < UFBXI_MIN_FILE_FORMAT_LOOKAHEAD) {
		uc->opts.file_format_lookahead = UFBXI_MIN_FILE_FORMAT_LOOKAHEAD;
	}

	if (!uc->opts.path_separator) {
		uc->opts.path_separator = UFBX_PATH_SEPARATOR;
	}

	if (!uc->opts.progress_cb.fn || uc->opts.progress_interval_hint >= SIZE_MAX) {
		uc->progress_interval = SIZE_MAX;
	} else if (uc->opts.progress_interval_hint > 0) {
		uc->progress_interval = (size_t)uc->opts.progress_interval_hint;
	} else {
		uc->progress_interval = 0x4000;
	}

	if (!uc->opts.open_file_cb.fn) {
		uc->opts.open_file_cb.fn = &ufbx_default_open_file;
	}

	if (!uc->opts.thread_opts.memory_limit) {
		uc->opts.thread_opts.memory_limit = 32*1024*1024;
	}

	uc->string_pool.error = &uc->error;
	ufbxi_map_init(&uc->string_pool.map, &uc->ator_tmp, &ufbxi_map_cmp_string, NULL);
	uc->string_pool.buf.ator = &uc->ator_result;
	uc->string_pool.buf.unordered = true;
	uc->string_pool.initial_size = 1024;
	uc->string_pool.error_handling = uc->opts.unicode_error_handling;

	ufbxi_map_init(&uc->prop_type_map, &uc->ator_tmp, &ufbxi_map_cmp_const_char_ptr, NULL);
	ufbxi_map_init(&uc->fbx_id_map, &uc->ator_tmp, &ufbxi_map_cmp_uint64, NULL);
	ufbxi_map_init(&uc->texture_file_map, &uc->ator_tmp, &ufbxi_map_cmp_const_char_ptr, NULL);
	ufbxi_map_init(&uc->anim_stack_map, &uc->ator_tmp, &ufbxi_map_cmp_const_char_ptr, NULL);
	ufbxi_map_init(&uc->fbx_attr_map, &uc->ator_tmp, &ufbxi_map_cmp_uint64, NULL);
	ufbxi_map_init(&uc->node_prop_set, &uc->ator_tmp, &ufbxi_map_cmp_const_char_ptr, NULL);
	ufbxi_map_init(&uc->dom_node_map, &uc->ator_tmp, &ufbxi_map_cmp_uintptr, NULL);

	uc->tmp.ator = &uc->ator_tmp;
	uc->tmp_parse.ator = &uc->ator_tmp;
	uc->tmp_stack.ator = &uc->ator_tmp;
	uc->tmp_connections.ator = &uc->ator_tmp;
	uc->tmp_node_ids.ator = &uc->ator_tmp;
	uc->tmp_elements.ator = &uc->ator_tmp;
	uc->tmp_element_offsets.ator = &uc->ator_tmp;
	uc->tmp_element_fbx_ids.ator = &uc->ator_tmp;
	uc->tmp_element_ptrs.ator = &uc->ator_tmp;
	for (size_t i = 0; i < UFBX_ELEMENT_TYPE_COUNT; i++) {
		uc->tmp_typed_element_offsets[i].ator = &uc->ator_tmp;
	}
	uc->tmp_mesh_textures.ator = &uc->ator_tmp;
	uc->tmp_full_weights.ator = &uc->ator_tmp;
	uc->tmp_dom_nodes.ator = &uc->ator_tmp;
	uc->tmp_element_id.ator = &uc->ator_tmp;
	uc->tmp_ascii_spans.ator = &uc->ator_tmp;

	for (size_t i = 0; i < UFBX_THREAD_GROUP_COUNT; i++) {
		uc->tmp_thread_parse[i].ator = &uc->ator_tmp;
		uc->tmp_thread_parse[i].unordered = true;
		uc->tmp_thread_parse[i].clearable = true;
	}

	uc->result.ator = &uc->ator_result;

	uc->tmp.unordered = true;
	uc->tmp_parse.unordered = true;
	uc->tmp_parse.clearable = true;
	uc->result.unordered = true;

	uc->warnings.error = &uc->error;
	uc->warnings.result = &uc->result;
	uc->warnings.tmp_stack.ator = &uc->ator_tmp;
	uc->string_pool.warnings = &uc->warnings;

	// Set zero size `swap_arr` to a non-NULL buffer so we can tell the difference between empty
	// array and an allocation failure.
	uc->swap_arr = (char*)ufbxi_zero_size_buffer;

	// NOTE: Though `inflate_retain` leaks out of the scope we don't use it outside this function.
	// cppcheck-suppress autoVariables
	uc->inflate_retain = &inflate_retain;

	int ok = ufbxi_load_imp(uc);

	if (uc->close_fn) {
		uc->close_fn(uc->read_user);
	}

	ufbxi_free_temp(uc);

	if (ok) {
		if (p_error) {
			ufbxi_clear_error(p_error);
		}
		return &uc->scene_imp->scene;
	} else {
		ufbxi_fix_error_type(&uc->error, "Failed to load", p_error);
		ufbxi_free_result(uc);
		return NULL;
	}
}

// -- Animation evaluation

static ufbxi_forceinline bool ufbxi_override_less_than_prop(const ufbx_prop_override *over, uint32_t element_id, const ufbx_prop *prop)
{
	if (over->element_id != element_id) return over->element_id < element_id;
	if (over->_internal_key != prop->_internal_key) return over->_internal_key < prop->_internal_key;
	return strcmp(over->prop_name.data, prop->name.data);
}

static ufbxi_forceinline bool ufbxi_override_equals_to_prop(const ufbx_prop_override *over, uint32_t element_id, const ufbx_prop *prop)
{
	if (over->element_id != element_id) return false;
	if (over->_internal_key != prop->_internal_key) return false;
	return !strcmp(over->prop_name.data, prop->name.data);
}

static ufbxi_noinline bool ufbxi_find_prop_override(const ufbx_prop_override_list *overrides, uint32_t element_id, ufbx_prop *prop)
{
	size_t ix = SIZE_MAX;
	ufbxi_macro_lower_bound_eq(ufbx_prop_override, 16, &ix, overrides->data, 0, overrides->count,
		( ufbxi_override_less_than_prop(a, element_id, prop) ),
		( ufbxi_override_equals_to_prop(a, element_id, prop) ));

	if (ix != SIZE_MAX) {
		const ufbx_prop_override *over = &overrides->data[ix];
		const uint32_t clear_flags = UFBX_PROP_FLAG_NO_VALUE | UFBX_PROP_FLAG_NOT_FOUND;
		prop->flags = (ufbx_prop_flags)(((uint32_t)prop->flags & ~clear_flags) | UFBX_PROP_FLAG_OVERRIDDEN);
		prop->value_vec4 = over->value;
		prop->value_real_arr[3] = 0.0f;
		prop->value_int = over->value_int;
		prop->value_str = over->value_str;
		prop->value_blob.data = prop->value_str.data;
		prop->value_blob.size = prop->value_str.length;
		return true;
	} else {
		return false;
	}
}

static ufbxi_noinline ufbx_prop_override_list ufbxi_find_element_prop_overrides(const ufbx_prop_override_list *overrides, uint32_t element_id)
{
	size_t begin = overrides->count, end = begin;

	ufbxi_macro_lower_bound_eq(ufbx_prop_override, 32, &begin, overrides->data, 0, overrides->count,
		(a->element_id < element_id),
		(a->element_id == element_id));

	ufbxi_macro_upper_bound_eq(ufbx_prop_override, 32, &end, overrides->data, begin, overrides->count,
		(a->element_id == element_id));

	ufbx_prop_override_list result = { overrides->data + begin, end - begin };
	return result;
}

typedef struct ufbxi_anim_layer_combine_ctx {
	const ufbx_anim *anim;
	const ufbx_element *element;
	double time;
	ufbx_rotation_order rotation_order;
	bool has_rotation_order;
} ufbxi_anim_layer_combine_ctx;

static ufbxi_noinline double ufbxi_pow_abs(double v, double e)
{
	if (e <= 0.0) return 1.0;
	if (e >= 1.0) return v;
	double sign = v < 0.0 ? -1.0 : 1.0;
	return sign * ufbx_pow(v * sign, e);
}

// Recursion is limited by the fact that we recurse only when the property name is "Lcl Rotation"
// and when recursing we always evaluate the property "RotationOrder"
static ufbxi_noinline void ufbxi_combine_anim_layer(ufbxi_anim_layer_combine_ctx *ctx, ufbx_anim_layer *layer, ufbx_real weight, const char *prop_name, ufbx_vec3 *result, const ufbx_vec3 *value)
	ufbxi_recursive_function_void(ufbxi_combine_anim_layer, (ctx, layer, weight, prop_name, result, value), 2,
		(ufbxi_anim_layer_combine_ctx *ctx, ufbx_anim_layer *layer, ufbx_real weight, const char *prop_name, ufbx_vec3 *result, const ufbx_vec3 *value))
{
	if (layer->compose_rotation && layer->blended && prop_name == ufbxi_Lcl_Rotation && !ctx->has_rotation_order) {
		ufbx_prop rp = ufbx_evaluate_prop_len(ctx->anim, ctx->element, ufbxi_RotationOrder, sizeof(ufbxi_RotationOrder) - 1, ctx->time);
		// NOTE: Defaults to 0 (UFBX_ROTATION_XYZ) gracefully if property is not found
		if (rp.value_int >= 0 && rp.value_int <= UFBX_ROTATION_ORDER_SPHERIC) {
			ctx->rotation_order = (ufbx_rotation_order)rp.value_int;
		} else {
			ctx->rotation_order = UFBX_ROTATION_ORDER_XYZ;
		}
		ctx->has_rotation_order = true;
	}

	if (layer->additive) {
		if (layer->compose_scale && prop_name == ufbxi_Lcl_Scaling) {
			result->x *= (ufbx_real)ufbxi_pow_abs(value->x, weight);
			result->y *= (ufbx_real)ufbxi_pow_abs(value->y, weight);
			result->z *= (ufbx_real)ufbxi_pow_abs(value->z, weight);
		} else if (layer->compose_rotation && prop_name == ufbxi_Lcl_Rotation) {
			ufbx_quat a = ufbx_euler_to_quat(*result, ctx->rotation_order);
			ufbx_quat b = ufbx_euler_to_quat(*value, ctx->rotation_order);
			b = ufbx_quat_slerp(ufbx_identity_quat, b, weight);
			ufbx_quat res = ufbxi_mul_quat(a, b);
			*result = ufbx_quat_to_euler(res, ctx->rotation_order);
		} else {
			result->x += value->x * weight;
			result->y += value->y * weight;
			result->z += value->z * weight;
		}
	} else if (layer->blended) {
		ufbx_real res_weight = 1.0f - weight;
		if (layer->compose_scale && prop_name == ufbxi_Lcl_Scaling) {
			result->x = (ufbx_real)(ufbxi_pow_abs(result->x, res_weight) * ufbxi_pow_abs(value->x, weight));
			result->y = (ufbx_real)(ufbxi_pow_abs(result->y, res_weight) * ufbxi_pow_abs(value->y, weight));
			result->z = (ufbx_real)(ufbxi_pow_abs(result->z, res_weight) * ufbxi_pow_abs(value->z, weight));
		} else if (layer->compose_rotation && prop_name == ufbxi_Lcl_Rotation) {
			ufbx_quat a = ufbx_euler_to_quat(*result, ctx->rotation_order);
			ufbx_quat b = ufbx_euler_to_quat(*value, ctx->rotation_order);
			ufbx_quat res = ufbx_quat_slerp(a, b, weight);
			*result = ufbx_quat_to_euler(res, ctx->rotation_order);
		} else {
			result->x = result->x * res_weight + value->x * weight;
			result->y = result->y * res_weight + value->y * weight;
			result->z = result->z * res_weight + value->z * weight;
		}
	} else {
		*result = *value;
	}
}

static ufbxi_forceinline bool ufbxi_anim_layer_might_contain_id(const ufbx_anim_layer *layer, uint32_t id)
{
	uint32_t id_mask = ufbxi_arraycount(layer->_element_id_bitmask) - 1;
	bool ok = id - layer->_min_element_id <= (layer->_max_element_id - layer->_min_element_id);
	ok &= (layer->_element_id_bitmask[(id >> 5) & id_mask] & (1u << (id & 31))) != 0;
	return ok;
}

static ufbxi_noinline void ufbxi_evaluate_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *props, size_t num_props)
{
	ufbxi_anim_layer_combine_ctx combine_ctx = { anim, element, time };

	uint32_t element_id = element->element_id;
	size_t num_layers = anim->layers.count;
	for (size_t layer_ix = 0; layer_ix < num_layers; layer_ix++) {
		ufbx_anim_layer *layer = anim->layers.data[layer_ix];
		if (!ufbxi_anim_layer_might_contain_id(layer, element_id)) continue;

		// Find the weight for the current layer
		// TODO: Should this be searched from multiple layers?
		ufbx_real weight = layer_ix < anim->override_layer_weights.count ? anim->override_layer_weights.data[layer_ix] : layer->weight;
		if (layer->weight_is_animated && layer->blended) {
			ufbx_anim_prop *weight_aprop = ufbxi_find_anim_prop_start(layer, &layer->element);
			if (weight_aprop) {
				weight = ufbx_evaluate_anim_value_real(weight_aprop->anim_value, time) / (ufbx_real)100.0;
				if (weight < 0.0f) weight = 0.0f;
				if (weight > 0.99999f) weight = 1.0f;
			}
		}

		ufbx_anim_prop *aprop = ufbxi_find_anim_prop_start(layer, element);
		if (!aprop) continue;

		for (size_t i = 0; i < num_props; i++) {
			ufbx_prop *prop = &props[i];

			// Don't evaluate on top of overridden properties
			if ((prop->flags & UFBX_PROP_FLAG_OVERRIDDEN) != 0) continue;

			// Connections override animation by default
			if ((prop->flags & UFBX_PROP_FLAG_CONNECTED) != 0 && !anim->ignore_connections) continue;

			// Skip until we reach `aprop >= prop`
			// NOTE: No need to check for end as `anim_props` is terminated with a NULL sentinel.
			while (aprop->element == element && aprop->_internal_key < prop->_internal_key) aprop++;
			if (aprop->prop_name.data != prop->name.data) {
				while (aprop->element == element && strcmp(aprop->prop_name.data, prop->name.data) < 0) aprop++;
			}

			// TODO: Should we skip the blending for the first layer _per property_
			// This could be done by having `UFBX_PROP_FLAG_ANIMATION_EVALUATED`
			// that gets set for the first layer of animation that is applied.
			if (aprop->prop_name.data == prop->name.data) {
				ufbx_vec3 v = ufbx_evaluate_anim_value_vec3(aprop->anim_value, time);
				if (layer_ix == 0) {
					prop->value_vec3 = v;
				} else {
					ufbxi_combine_anim_layer(&combine_ctx, layer, weight, prop->name.data, &prop->value_vec3, &v);
				}
			}
		}
	}

	ufbxi_for(ufbx_prop, prop, props, num_props) {
		if (prop->flags & UFBX_PROP_FLAG_OVERRIDDEN) continue;
		prop->value_int = ufbxi_f64_to_i64(prop->value_real);
	}
}

// Recursion limited by not calling `ufbx_evaluate_prop_len()` with a connected property,
// meaning it will never call `ufbxi_evaluate_connected_prop()` again indirectly.
static ufbxi_noinline void ufbxi_evaluate_connected_prop(ufbx_prop *prop, const ufbx_anim *anim, const ufbx_element *element, const char *name, double time)
	ufbxi_recursive_function_void(ufbxi_evaluate_connected_prop, (prop, anim, element, name, time), 3,
		(ufbx_prop *prop, const ufbx_anim *anim, const ufbx_element *element, const char *name, double time))
{
	ufbx_connection *conn = ufbxi_find_prop_connection(element, name);

	for (size_t i = 0; i < 1000 && conn; i++) {
		ufbx_connection *next_conn = ufbxi_find_prop_connection(conn->src, conn->src_prop.data);
		if (!next_conn) break;
		conn = next_conn;
	}

	// Found a non-cyclic connection
	if (conn && !ufbxi_find_prop_connection(conn->src, conn->src_prop.data)) {
		ufbx_prop ep = ufbx_evaluate_prop_len(anim, conn->src, conn->src_prop.data, conn->src_prop.length, time);
		prop->value_vec4 = ep.value_vec4;
		prop->value_int = ep.value_int;
		prop->value_str = ep.value_str;
		prop->value_blob = ep.value_blob;
	} else {
		// Connection not found, maybe it's animated?
		prop->flags = (ufbx_prop_flags)((uint32_t)prop->flags & ~(uint32_t)UFBX_PROP_FLAG_CONNECTED);
	}
}

typedef struct {
	const ufbx_prop *prop, *prop_end;
	const ufbx_prop_override *over, *over_end;
	ufbx_prop tmp;
} ufbxi_prop_iter;

static ufbxi_noinline void ufbxi_init_prop_iter_slow(ufbxi_prop_iter *iter, const ufbx_anim *anim, const ufbx_element *element)
{
	iter->prop = element->props.props.data;
	iter->prop_end = element->props.props.data + element->props.props.count;

	ufbx_prop_override_list over = ufbxi_find_element_prop_overrides(&anim->prop_overrides, element->element_id);
	iter->over = over.data;
	iter->over_end = over.data + over.count;
	if (over.count > 0) {
		memset(&iter->tmp, 0, sizeof(ufbx_prop));
	}
}

static ufbxi_forceinline void ufbxi_init_prop_iter(ufbxi_prop_iter *iter, const ufbx_anim *anim, const ufbx_element *element)
{
	iter->prop = element->props.props.data;
	iter->prop_end = ufbxi_add_ptr(element->props.props.data, element->props.props.count);
	iter->over = iter->over_end = NULL;
	if (anim->prop_overrides.count > 0) {
		ufbxi_init_prop_iter_slow(iter, anim, element);
	}
}

static ufbxi_noinline const ufbx_prop *ufbxi_next_prop_slow(ufbxi_prop_iter *iter)
{
	const ufbx_prop *prop = iter->prop;
	const ufbx_prop_override *over = iter->over;
	if (prop == iter->prop_end && over == iter->over_end) return NULL;

	// We can use `UINT32_MAX` as a terminating key (aka prefix) as prop names must
	// be valid UTF-8 and the byte sequence "\xff\xff\xff\xff" is not valid.
	uint32_t prop_key = prop != iter->prop_end ? prop->_internal_key : UINT32_MAX;
	uint32_t over_key = over != iter->over_end ? over->_internal_key : UINT32_MAX;

	int cmp = 0;
	if (prop_key != over_key) {
		cmp = prop_key < over_key ? -1 : 1;
	} else {
		cmp = strcmp(prop->name.data, over->prop_name.data);
	}

	if (cmp >= 0) {
		ufbx_prop *dst = &iter->tmp;
		dst->name = over->prop_name;
		dst->_internal_key = over->_internal_key;
		dst->type = UFBX_PROP_UNKNOWN;
		dst->flags = UFBX_PROP_FLAG_OVERRIDDEN;
		dst->value_str = over->value_str;
		dst->value_blob.data = dst->value_str.data;
		dst->value_blob.size = dst->value_str.length;
		dst->value_int = over->value_int;
		dst->value_vec4 = over->value;
		iter->over = over + 1;
		if (cmp == 0) {
			iter->prop = prop + 1;
		}
		return dst;
	} else {
		iter->prop = prop + 1;
		return prop;
	}
}

static ufbxi_forceinline const ufbx_prop *ufbxi_next_prop(ufbxi_prop_iter *iter)
{
	if (iter->over == iter->over_end) {
		if (iter->prop == iter->prop_end) return NULL;
		return iter->prop++;
	} else {
		return ufbxi_next_prop_slow(iter);
	}
}

static ufbxi_noinline ufbx_props ufbxi_evaluate_selected_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *props, const char *const *prop_names, size_t max_props)
{
	const char *name = prop_names[0];
	uint32_t key = ufbxi_get_name_key_c(name);
	size_t num_props = 0;

#if defined(UFBX_REGRESSION)
	for (size_t i = 1; i < max_props; i++) {
		ufbx_assert(strcmp(prop_names[i - 1], prop_names[i]) < 0);
	}
#endif

	size_t name_ix = 0;

	ufbxi_prop_iter iter; // ufbxi_uninit
	ufbxi_init_prop_iter(&iter, anim, element);
	const ufbx_prop *prop = NULL;
	while ((prop = ufbxi_next_prop(&iter)) != NULL) {
		while (name_ix < max_props) {
			if (key > prop->_internal_key) break;
			if (name == prop->name.data) {
				if ((prop->flags & UFBX_PROP_FLAG_CONNECTED) != 0 && !anim->ignore_connections) {
					ufbx_prop *dst = &props[num_props++];
					*dst = *prop;
					ufbxi_evaluate_connected_prop(dst, anim, element, name, time);
				} else if ((prop->flags & (UFBX_PROP_FLAG_ANIMATED|UFBX_PROP_FLAG_OVERRIDDEN)) != 0) {
					props[num_props++] = *prop;
				}
				break;
			} else if (strcmp(name, prop->name.data) < 0) {
				name_ix++;
				if (name_ix < max_props) {
					name = prop_names[name_ix];
					key = ufbxi_get_name_key_c(name);
				}
			} else {
				break;
			}
		}
	}

	ufbxi_evaluate_props(anim, element, time, props, num_props);

	ufbx_props prop_list;
	prop_list.props.data = props;
	prop_list.props.count = prop_list.num_animated = num_props;
	prop_list.defaults = (ufbx_props*)&element->props;
	return prop_list;
}

#if UFBXI_FEATURE_SCENE_EVALUATION

typedef struct {
	char *src_element;
	char *dst_element;

	ufbxi_scene_imp *src_imp;
	ufbx_scene src_scene;
	ufbx_evaluate_opts opts;
	ufbx_anim *anim;
	double time;

	ufbx_error error;

	// Allocators
	ufbxi_allocator ator_result;
	ufbxi_allocator ator_tmp;

	ufbxi_buf result;
	ufbxi_buf tmp;

	ufbx_scene scene;

	ufbxi_scene_imp *scene_imp;
} ufbxi_eval_context;

static ufbxi_forceinline ufbx_element *ufbxi_translate_element(ufbxi_eval_context *ec, void *elem)
{
	return elem ? (ufbx_element*)(ec->dst_element + ((char*)elem - ec->src_element)) : NULL;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_translate_element_list(ufbxi_eval_context *ec, void *p_list)
{
	ufbx_element_list *list = (ufbx_element_list*)p_list;
	size_t count = list->count;
	ufbx_element **src = list->data;
	ufbx_element **dst = ufbxi_push(&ec->result, ufbx_element*, count);
	ufbxi_check_err(&ec->error, dst);
	list->data = dst;
	for (size_t i = 0; i < count; i++) {
		dst[i] = ufbxi_translate_element(ec, src[i]);
	}
	return 1;
}

static ufbxi_noinline void ufbxi_translate_maps(ufbxi_eval_context *ec, ufbx_material_map *maps, size_t count)
{
	ufbxi_nounroll ufbxi_for(ufbx_material_map, map, maps, count) {
		map->texture = (ufbx_texture*)ufbxi_translate_element(ec, map->texture);
	}
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_translate_anim(ufbxi_eval_context *ec, ufbx_anim **p_anim)
{
	ufbx_anim *anim = ufbxi_push_copy(&ec->result, ufbx_anim, 1, *p_anim);
	ufbxi_check_err(&ec->error, anim);
	ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &anim->layers));
	*p_anim = anim;
	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_evaluate_imp(ufbxi_eval_context *ec)
{
	ec->scene = ec->src_scene;
	size_t num_elements = ec->scene.elements.count;

	char *element_data = (char*)ufbxi_push(&ec->result, uint64_t, ec->scene.metadata.element_buffer_size/8);
	ufbxi_check_err(&ec->error, element_data);

	ec->scene.elements.data = ufbxi_push(&ec->result, ufbx_element*, num_elements);
	ufbxi_check_err(&ec->error, ec->scene.elements.data);

	ec->src_element = (char*)ec->src_scene.elements.data[0];
	ec->dst_element = element_data;

	for (size_t i = 0; i < UFBX_ELEMENT_TYPE_COUNT; i++) {
		ec->scene.elements_by_type[i].data = ufbxi_push(&ec->result, ufbx_element*, ec->scene.elements_by_type[i].count);
		ufbxi_check_err(&ec->error, ec->scene.elements_by_type[i].data);
	}

	size_t num_connections = ec->scene.connections_dst.count;
	ec->scene.connections_src.data = ufbxi_push(&ec->result, ufbx_connection, num_connections);
	ec->scene.connections_dst.data = ufbxi_push(&ec->result, ufbx_connection, num_connections);
	ufbxi_check_err(&ec->error, ec->scene.connections_src.data);
	ufbxi_check_err(&ec->error, ec->scene.connections_dst.data);
	for (size_t i = 0; i < num_connections; i++) {
		ufbx_connection *src = &ec->scene.connections_src.data[i];
		ufbx_connection *dst = &ec->scene.connections_dst.data[i];
		*src = ec->src_scene.connections_src.data[i];
		*dst = ec->src_scene.connections_dst.data[i];
		src->src = ufbxi_translate_element(ec, src->src);
		src->dst = ufbxi_translate_element(ec, src->dst);
		dst->src = ufbxi_translate_element(ec, dst->src);
		dst->dst = ufbxi_translate_element(ec, dst->dst);
	}

	ec->scene.elements_by_name.data = ufbxi_push(&ec->result, ufbx_name_element, num_elements);
	ufbxi_check_err(&ec->error, ec->scene.elements_by_name.data);

	ec->scene.root_node = (ufbx_node*)ufbxi_translate_element(ec, ec->scene.root_node);
	ufbxi_check_err(&ec->error, ufbxi_translate_anim(ec, &ec->scene.anim));

	for (size_t i = 0; i < num_elements; i++) {
		ufbx_element *src = ec->src_scene.elements.data[i];
		ufbx_element *dst = ufbxi_translate_element(ec, src);
		size_t size = ufbx_element_type_size[src->type];
		ufbx_assert(size > 0);
		memcpy(dst, src, size);

		ec->scene.elements.data[i] = dst;
		ec->scene.elements_by_type[src->type].data[src->typed_id] = dst;

		dst->connections_src.data = ec->scene.connections_src.data + (dst->connections_src.data - ec->src_scene.connections_src.data);
		dst->connections_dst.data = ec->scene.connections_dst.data + (dst->connections_dst.data - ec->src_scene.connections_dst.data);
		if (dst->instances.count > 0) {
			ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &dst->instances));
		}

		ufbx_name_element named = ec->src_scene.elements_by_name.data[i];
		named.element = ufbxi_translate_element(ec, named.element);
		ec->scene.elements_by_name.data[i] = named;
	}

	ufbxi_for_ptr_list(ufbx_node, p_node, ec->scene.nodes) {
		ufbx_node *node = *p_node;
		node->parent = (ufbx_node*)ufbxi_translate_element(ec, node->parent);
		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &node->children));

		node->attrib = ufbxi_translate_element(ec, node->attrib);
		node->mesh = (ufbx_mesh*)ufbxi_translate_element(ec, node->mesh);
		node->light = (ufbx_light*)ufbxi_translate_element(ec, node->light);
		node->camera = (ufbx_camera*)ufbxi_translate_element(ec, node->camera);
		node->bone = (ufbx_bone*)ufbxi_translate_element(ec, node->bone);
		node->inherit_scale_node = (ufbx_node*)ufbxi_translate_element(ec, node->inherit_scale_node);
		node->scale_helper = (ufbx_node*)ufbxi_translate_element(ec, node->scale_helper);
		node->bind_pose = (ufbx_pose*)ufbxi_translate_element(ec, node->bind_pose);

		if (node->all_attribs.count > 1) {
			ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &node->all_attribs));
		} else if (node->all_attribs.count == 1) {
			node->all_attribs.data = &node->attrib;
		}

		node->geometry_transform_helper = (ufbx_node*)ufbxi_translate_element(ec, node->geometry_transform_helper);

		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &node->materials));
	}

	ufbxi_for_ptr_list(ufbx_mesh, p_mesh, ec->scene.meshes) {
		ufbx_mesh *mesh = *p_mesh;

		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &mesh->materials));
		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &mesh->skin_deformers));
		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &mesh->blend_deformers));
		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &mesh->cache_deformers));
		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &mesh->all_deformers));
	}

	ufbxi_for_ptr_list(ufbx_stereo_camera, p_stereo, ec->scene.stereo_cameras) {
		ufbx_stereo_camera *stereo = *p_stereo;
		stereo->left = (ufbx_camera*)ufbxi_translate_element(ec, stereo->left);
		stereo->right = (ufbx_camera*)ufbxi_translate_element(ec, stereo->right);
	}

	ufbxi_for_ptr_list(ufbx_skin_deformer, p_skin, ec->scene.skin_deformers) {
		ufbx_skin_deformer *skin = *p_skin;
		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &skin->clusters));
	}

	ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, ec->scene.skin_clusters) {
		ufbx_skin_cluster *cluster = *p_cluster;
		cluster->bone_node = (ufbx_node*)ufbxi_translate_element(ec, cluster->bone_node);
	}

	ufbxi_for_ptr_list(ufbx_blend_deformer, p_blend, ec->scene.blend_deformers) {
		ufbx_blend_deformer *blend = *p_blend;
		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &blend->channels));
	}

	ufbxi_for_ptr_list(ufbx_blend_channel, p_chan, ec->scene.blend_channels) {
		ufbx_blend_channel *chan = *p_chan;

		ufbx_blend_keyframe *keys = ufbxi_push(&ec->result, ufbx_blend_keyframe, chan->keyframes.count);
		ufbxi_check_err(&ec->error, keys);
		for (size_t i = 0; i < chan->keyframes.count; i++) {
			keys[i] = chan->keyframes.data[i];
			keys[i].shape = (ufbx_blend_shape*)ufbxi_translate_element(ec, keys[i].shape);
		}
		chan->keyframes.data = keys;
		chan->target_shape = (ufbx_blend_shape*)ufbxi_translate_element(ec, chan->target_shape);
	}

	ufbxi_for_ptr_list(ufbx_cache_deformer, p_deformer, ec->scene.cache_deformers) {
		ufbx_cache_deformer *deformer = *p_deformer;
		deformer->file = (ufbx_cache_file*)ufbxi_translate_element(ec, deformer->file);
	}

	ufbxi_for_ptr_list(ufbx_material, p_material, ec->scene.materials) {
		ufbx_material *material = *p_material;

		material->shader = (ufbx_shader*)ufbxi_translate_element(ec, material->shader);
		ufbxi_translate_maps(ec, material->fbx.maps, UFBX_MATERIAL_FBX_MAP_COUNT);
		ufbxi_translate_maps(ec, material->pbr.maps, UFBX_MATERIAL_PBR_MAP_COUNT);

		ufbx_material_texture *textures = ufbxi_push(&ec->result, ufbx_material_texture, material->textures.count);
		ufbxi_check_err(&ec->error, textures);
		for (size_t i = 0; i < material->textures.count; i++) {
			textures[i] = material->textures.data[i];
			textures[i].texture = (ufbx_texture*)ufbxi_translate_element(ec, textures[i].texture);
		}
		material->textures.data = textures;
	}

	ufbxi_for_ptr_list(ufbx_texture, p_texture, ec->scene.textures) {
		ufbx_texture *texture = *p_texture;
		texture->video = (ufbx_video*)ufbxi_translate_element(ec, texture->video);

		ufbx_texture_layer *layers = ufbxi_push(&ec->result, ufbx_texture_layer, texture->layers.count);
		ufbxi_check_err(&ec->error, layers);
		for (size_t i = 0; i < texture->layers.count; i++) {
			layers[i] = texture->layers.data[i];
			layers[i].texture = (ufbx_texture*)ufbxi_translate_element(ec, layers[i].texture);
		}
		texture->layers.data = layers;

		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &texture->file_textures));

		if (texture->shader) {
			ufbx_shader_texture *shader = texture->shader;
			shader = ufbxi_push_copy(&ec->result, ufbx_shader_texture, 1, shader);
			ufbxi_check_err(&ec->error, shader);
			texture->shader = shader;

			ufbx_shader_texture_input *inputs = ufbxi_push_copy(&ec->result, ufbx_shader_texture_input, shader->inputs.count, shader->inputs.data);
			ufbxi_check_err(&ec->error, inputs);
			shader->inputs.data = inputs;
		}
	}

	ufbxi_for_ptr_list(ufbx_shader, p_shader, ec->scene.shaders) {
		ufbx_shader *shader = *p_shader;
		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &shader->bindings));
	}

	ufbxi_for_ptr_list(ufbx_display_layer, p_layer, ec->scene.display_layers) {
		ufbx_display_layer *layer = *p_layer;

		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &layer->nodes));
	}

	ufbxi_for_ptr_list(ufbx_selection_set, p_set, ec->scene.selection_sets) {
		ufbx_selection_set *set = *p_set;

		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &set->nodes));
	}

	ufbxi_for_ptr_list(ufbx_selection_node, p_node, ec->scene.selection_nodes) {
		ufbx_selection_node *node = *p_node;

		node->target_node = (ufbx_node*)ufbxi_translate_element(ec, node->target_node);
		node->target_mesh = (ufbx_mesh*)ufbxi_translate_element(ec, node->target_mesh);
	}

	ufbxi_for_ptr_list(ufbx_constraint, p_constraint, ec->scene.constraints) {
		ufbx_constraint *constraint = *p_constraint;

		constraint->node = (ufbx_node*)ufbxi_translate_element(ec, constraint->node);
		constraint->aim_up_node = (ufbx_node*)ufbxi_translate_element(ec, constraint->aim_up_node);
		constraint->ik_effector = (ufbx_node*)ufbxi_translate_element(ec, constraint->ik_effector);
		constraint->ik_end_node = (ufbx_node*)ufbxi_translate_element(ec, constraint->ik_end_node);

		ufbx_constraint_target *targets = ufbxi_push(&ec->result, ufbx_constraint_target, constraint->targets.count);
		ufbxi_check_err(&ec->error, targets);
		for (size_t i = 0; i < constraint->targets.count; i++) {
			targets[i] = constraint->targets.data[i];
			targets[i].node = (ufbx_node*)ufbxi_translate_element(ec, targets[i].node);
		}
		constraint->targets.data = targets;
	}

	ufbxi_for_ptr_list(ufbx_audio_layer, p_layer, ec->scene.audio_layers) {
		ufbx_audio_layer *layer = *p_layer;

		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &layer->clips));
	}

	ufbxi_for_ptr_list(ufbx_anim_stack, p_stack, ec->scene.anim_stacks) {
		ufbx_anim_stack *stack = *p_stack;

		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &stack->layers));
		ufbxi_check_err(&ec->error, ufbxi_translate_anim(ec, &stack->anim));
	}

	ufbxi_for_ptr_list(ufbx_anim_layer, p_layer, ec->scene.anim_layers) {
		ufbx_anim_layer *layer = *p_layer;

		ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &layer->anim_values));
		ufbx_anim_prop *props = ufbxi_push(&ec->result, ufbx_anim_prop, layer->anim_props.count + 1);
		ufbxi_check_err(&ec->error, props);
		for (size_t i = 0; i < layer->anim_props.count; i++) {
			props[i] = layer->anim_props.data[i];
			props[i].element = ufbxi_translate_element(ec, props[i].element);
			props[i].anim_value = (ufbx_anim_value*)ufbxi_translate_element(ec, props[i].anim_value);
		}
		// Maintain NULL sentinel
		memset(props + layer->anim_props.count, 0, sizeof(ufbx_anim_prop));
		layer->anim_props.data = props;
	}

	ufbxi_for_ptr_list(ufbx_pose, p_pose, ec->scene.poses) {
		ufbx_pose *pose = *p_pose;

		ufbx_bone_pose *bones = ufbxi_push(&ec->result, ufbx_bone_pose, pose->bone_poses.count);
		ufbxi_check_err(&ec->error, bones);
		for (size_t i = 0; i < pose->bone_poses.count; i++) {
			bones[i] = pose->bone_poses.data[i];
			bones[i].bone_node = (ufbx_node*)ufbxi_translate_element(ec, bones[i].bone_node);
		}
		pose->bone_poses.data = bones;
	}

	ufbxi_check_err(&ec->error, ufbxi_translate_anim(ec, &ec->anim));

	ufbxi_for_ptr_list(ufbx_anim_value, p_value, ec->scene.anim_values) {
		ufbx_anim_value *value = *p_value;
		value->curves[0] = (ufbx_anim_curve*)ufbxi_translate_element(ec, value->curves[0]);
		value->curves[1] = (ufbx_anim_curve*)ufbxi_translate_element(ec, value->curves[1]);
		value->curves[2] = (ufbx_anim_curve*)ufbxi_translate_element(ec, value->curves[2]);
	}

	ufbx_anim anim = *ec->anim;
	ufbx_prop_override *over = anim.prop_overrides.data, *over_end = ufbxi_add_ptr(over, anim.prop_overrides.count);

	// Evaluate the properties
	ufbxi_for_ptr_list(ufbx_element, p_elem, ec->scene.elements) {
		ufbx_element *elem = *p_elem;
		size_t num_animated = elem->props.num_animated;
		size_t num_override = 0;

		// Setup the overrides for this element if found
		while (over != over_end && over->element_id == elem->element_id) {
			num_override++;
			over++;
		}

		num_animated += num_override;
		if (num_animated == 0) continue;

		anim.prop_overrides.data = ufbxi_sub_ptr(over, num_override);
		anim.prop_overrides.count = num_override;

		ufbx_prop *props = ufbxi_push(&ec->result, ufbx_prop, num_animated);
		ufbxi_check_err(&ec->error, props);

		elem->props = ufbx_evaluate_props(&anim, elem, ec->time, props, num_animated);
		elem->props.defaults = &ec->src_scene.elements.data[elem->element_id]->props;
	}

	// Update all derived values
	ufbxi_update_scene(&ec->scene, false, anim.transform_overrides.data, anim.transform_overrides.count);

	// Evaluate skinning if requested
	if (ec->opts.evaluate_skinning) {
		ufbx_geometry_cache_data_opts cache_opts = { 0 };
		cache_opts.open_file_cb = ec->opts.open_file_cb;
		ufbxi_check_err(&ec->error, ufbxi_evaluate_skinning(&ec->scene, &ec->error, &ec->result, &ec->tmp,
			ec->time, ec->opts.load_external_files && ec->opts.evaluate_caches, &cache_opts));
	}

	// Retain the scene, this must be the final allocation as we copy
	// `ator_result` to `ufbx_scene_imp`.
	ufbxi_scene_imp *imp = ufbxi_push_zero(&ec->result, ufbxi_scene_imp, 1);
	ufbxi_check_err(&ec->error, imp);

	ufbx_assert(ec->src_imp->magic == UFBXI_SCENE_IMP_MAGIC);
	ufbxi_init_ref(&imp->refcount, UFBXI_SCENE_IMP_MAGIC, &ec->src_imp->refcount);

	imp->magic = UFBXI_SCENE_IMP_MAGIC;
	imp->scene = ec->scene;
	imp->refcount.ator = ec->ator_result;
	imp->refcount.ator.error = NULL;

	// Copy retained buffers and translate the allocator struct to the one
	// contained within `ufbxi_scene_imp`
	imp->refcount.buf = ec->result;
	imp->refcount.buf.ator = &imp->refcount.ator;

	imp->scene.metadata.result_memory_used = imp->refcount.ator.current_size;
	imp->scene.metadata.temp_memory_used = ec->ator_tmp.current_size;
	imp->scene.metadata.result_allocs = imp->refcount.ator.num_allocs;
	imp->scene.metadata.temp_allocs = ec->ator_tmp.num_allocs;

	ufbxi_for_ptr_list(ufbx_element, p_elem, imp->scene.elements) {
		(*p_elem)->scene = &imp->scene;
	}

	ec->scene_imp = imp;
	ec->result.ator = &ec->ator_result;

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline ufbx_scene *ufbxi_evaluate_scene(ufbxi_eval_context *ec, ufbx_scene *scene, const ufbx_anim *anim, double time, const ufbx_evaluate_opts *user_opts, ufbx_error *p_error)
{
	if (user_opts) {
		ec->opts = *user_opts;
	} else {
		memset(&ec->opts, 0, sizeof(ec->opts));
	}

	ec->src_imp = ufbxi_get_imp(ufbxi_scene_imp, scene);
	ec->src_scene = *scene;
	ec->anim = anim ? (ufbx_anim*)anim : scene->anim;
	ec->time = time;

	ufbxi_init_ator(&ec->error, &ec->ator_tmp, &ec->opts.temp_allocator, "temp");
	ufbxi_init_ator(&ec->error, &ec->ator_result, &ec->opts.result_allocator, "result");

	ec->result.ator = &ec->ator_result;
	ec->tmp.ator = &ec->ator_tmp;

	ec->result.unordered = true;
	ec->tmp.unordered = true;

	if (ufbxi_evaluate_imp(ec)) {
		ufbxi_buf_free(&ec->tmp);
		ufbxi_free_ator(&ec->ator_tmp);
		if (p_error) {
			ufbxi_clear_error(p_error);
		}
		return &ec->scene_imp->scene;
	} else {
		ufbxi_fix_error_type(&ec->error, "Failed to evaluate", p_error);
		ufbxi_buf_free(&ec->tmp);
		ufbxi_buf_free(&ec->result);
		ufbxi_free_ator(&ec->ator_tmp);
		ufbxi_free_ator(&ec->ator_result);
		return NULL;
	}
}

#endif

typedef struct {
	ufbx_error error;
	ufbxi_allocator ator_result;
	ufbxi_buf result;
	const ufbx_scene *scene;
	ufbx_anim_opts opts;

	ufbx_anim anim;
	ufbxi_anim_imp *imp;
} ufbxi_create_anim_context;

ufbxi_nodiscard static ufbxi_noinline int ufbxi_check_string(ufbx_error *error, ufbx_string *dst, const ufbx_string *src)
{
	size_t length = src->length != SIZE_MAX ? src->length : strlen(src->data);
	const char *data = length != 0 ? src->data : ufbxi_empty_char;
	if (length > 0) {
		size_t valid_length = ufbxi_utf8_valid_length(data, length);
		ufbxi_check_err_msg(error, valid_length == length, "Invalid UTF-8");
	}

	dst->data = data;
	dst->length = length;
	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_anim_string(ufbxi_create_anim_context *ac, ufbx_string *str)
{
	size_t length = str->length;
	if (length > 0) {
		char *copy = ufbxi_push(&ac->result, char, length + 1);
		ufbxi_check_err(&ac->error, copy);
		memcpy(copy, str->data, length);
		copy[str->length] = '\0';
		str->data = copy;
	} else {
		ufbx_assert(str->data == ufbxi_empty_char);
	}

	return 1;
}

static bool ufbxi_prop_override_prop_name_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_prop_override *a = (const ufbx_prop_override*)va, *b = (const ufbx_prop_override*)vb;
	if (a->_internal_key != b->_internal_key) return a->_internal_key < b->_internal_key;
	return ufbxi_str_less(a->prop_name, b->prop_name);
}

static bool ufbxi_prop_override_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_prop_override *a = (const ufbx_prop_override*)va, *b = (const ufbx_prop_override*)vb;
	if (a->element_id != b->element_id) return a->element_id < b->element_id;
	if (a->_internal_key != b->_internal_key) return a->_internal_key < b->_internal_key;
	return strcmp(a->prop_name.data, b->prop_name.data) < 0;
}

static bool ufbxi_transform_override_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_transform_override *a = (const ufbx_transform_override*)va, *b = (const ufbx_transform_override*)vb;
	return a->node_id < b->node_id;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_create_anim_imp(ufbxi_create_anim_context *ac)
{
	const ufbx_scene *scene = ac->scene;
	ufbx_anim *anim = &ac->anim;

	ufbxi_init_ator(&ac->error, &ac->ator_result, &ac->opts.result_allocator, "result");
	ac->result.unordered = true;
	ac->result.ator = &ac->ator_result;

	anim->ignore_connections = ac->opts.ignore_connections;
	anim->custom = true;

	size_t num_layers = ac->opts.layer_ids.count;
	anim->layers.count = num_layers;
	anim->layers.data = ufbxi_push_zero(&ac->result, ufbx_anim_layer*, num_layers);
	ufbxi_check_err(&ac->error, anim->layers.data);

	if (ac->opts.override_layer_weights.count > 0) {
		ufbxi_check_err_msg(&ac->error, ac->opts.override_layer_weights.count == num_layers, "override_layer_weights[] count must match layer_ids[] count");
		anim->override_layer_weights.data = ufbxi_push_copy(&ac->result, ufbx_real, num_layers, ac->opts.override_layer_weights.data);
		ufbxi_check_err(&ac->error, anim->override_layer_weights.data);
		anim->override_layer_weights.count = num_layers;
	}

	for (size_t i = 0; i < num_layers; i++) {
		uint32_t index = ac->opts.layer_ids.data[i];
		ufbxi_check_err_msg(&ac->error, index < scene->anim_layers.count, "layer_ids out of bounds");
		anim->layers.data[i] = ac->scene->anim_layers.data[index];
	}

	ufbx_const_prop_override_desc_list prop_overrides = ac->opts.prop_overrides;
	if (prop_overrides.count > 0) {
		anim->prop_overrides.count = prop_overrides.count;
		anim->prop_overrides.data = ufbxi_push_zero(&ac->result, ufbx_prop_override, prop_overrides.count);
		ufbxi_check_err(&ac->error, anim->prop_overrides.data);

		for (size_t i = 0; i < prop_overrides.count; i++) {
			const ufbx_prop_override_desc *src = &prop_overrides.data[i];
			ufbx_prop_override *dst = &anim->prop_overrides.data[i];

			dst->element_id = src->element_id;
			dst->value = src->value;
			dst->value_int = src->value_int;

			if (dst->value.x != 0.0f && dst->value_int == 0) {
				dst->value_int = (int64_t)dst->value.x;
			} else if (dst->value_int != 0 && dst->value.x == 0.0f) {
				dst->value.x = (ufbx_real)dst->value_int;
			}

			ufbxi_check_err(&ac->error, ufbxi_check_string(&ac->error, &dst->prop_name, &src->prop_name));
			ufbxi_check_err(&ac->error, ufbxi_check_string(&ac->error, &dst->value_str, &src->value_str));

			dst->_internal_key = ufbxi_get_name_key(dst->prop_name.data, dst->prop_name.length);
		}

		// Sort `anim->prop_overrides` first by `prop_name` only so we can deduplicate and
		// convert them to global strings in `ufbxi_strings[]` if possible.
		ufbxi_unstable_sort(anim->prop_overrides.data, anim->prop_overrides.count, sizeof(ufbx_prop_override), &ufbxi_prop_override_prop_name_less, NULL);

		const ufbx_string *global_str = ufbxi_strings, *global_end = global_str + ufbxi_arraycount(ufbxi_strings);
		ufbx_string prev_name = { ufbxi_empty_char };
		ufbxi_for_list(ufbx_prop_override, over, anim->prop_overrides) {
			if (over->value_str.length > 0) {
				ufbxi_check_err(&ac->error, ufbxi_push_anim_string(ac, &over->value_str));
			}

			if (ufbxi_str_equal(over->prop_name, prev_name)) {
				over->prop_name = prev_name;
				continue;
			}

			while (global_str != global_end && ufbxi_str_less(*global_str, over->prop_name)) {
				++global_str;
			}

			if (global_str != global_end && ufbxi_str_equal(*global_str, over->prop_name)) {
				over->prop_name = *global_str;
			} else {
				ufbxi_check_err(&ac->error, ufbxi_push_anim_string(ac, &over->prop_name));
			}

			prev_name = over->prop_name;
		}

		// Sort `anim->prop_overrides` to the actual order expected by evaluation.
		ufbxi_unstable_sort(anim->prop_overrides.data, anim->prop_overrides.count, sizeof(ufbx_prop_override), &ufbxi_prop_override_less, NULL);

		for (size_t i = 1; i < prop_overrides.count; i++) {
			const ufbx_prop_override *prev = &anim->prop_overrides.data[i - 1];
			const ufbx_prop_override *next = &anim->prop_overrides.data[i];
			if (prev->element_id == next->element_id && prev->prop_name.data == next->prop_name.data) {
				ufbxi_fmt_err_info(&ac->error, "element %u prop \"%s\"", prev->element_id, prev->prop_name.data);
				ufbxi_fail_err_msg(&ac->error, "Duplicate override", "Duplicate override");
			}
		}
	}

	if (ac->opts.transform_overrides.count > 0) {
		anim->transform_overrides.count = ac->opts.transform_overrides.count;
		anim->transform_overrides.data = ufbxi_push_copy(&ac->result, ufbx_transform_override, anim->transform_overrides.count, ac->opts.transform_overrides.data);
		ufbxi_check_err(&ac->error, anim->transform_overrides.data);
		ufbxi_unstable_sort(anim->transform_overrides.data, anim->transform_overrides.count, sizeof(ufbx_transform_override), &ufbxi_transform_override_less, NULL);
	}

	ac->imp = ufbxi_push(&ac->result, ufbxi_anim_imp, 1);
	ufbxi_check_err(&ac->error, ac->imp);

	ufbxi_init_ref(&ac->imp->refcount, UFBXI_ANIM_IMP_MAGIC, &(ufbxi_get_imp(ufbxi_scene_imp, scene))->refcount);

	ac->imp->magic = UFBXI_ANIM_IMP_MAGIC;
	ac->imp->anim = ac->anim;
	ac->imp->refcount.ator = ac->ator_result;
	ac->imp->refcount.buf = ac->result;

	return 1;
}

// -- Animation baking

typedef struct {
	ufbxi_refcount refcount;
	ufbx_baked_anim bake;
	uint32_t magic;
} ufbxi_baked_anim_imp;

#if UFBXI_FEATURE_ANIMATION_BAKING

typedef struct {
	double time;
	uint32_t flags;
} ufbxi_bake_time;

UFBX_LIST_TYPE(ufbxi_bake_time_list, ufbxi_bake_time);

typedef struct {
	ufbx_error error;
	ufbxi_allocator ator_tmp;
	ufbxi_allocator ator_result;

	ufbxi_buf result;
	ufbxi_buf tmp;
	ufbxi_buf tmp_prop;
	ufbxi_buf tmp_times;
	ufbxi_buf tmp_bake_props;
	ufbxi_buf tmp_nodes;
	ufbxi_buf tmp_elements;
	ufbxi_buf tmp_props;
	ufbxi_buf tmp_bake_stack;

	ufbxi_bake_time_list layer_weight_times;

	ufbx_baked_node **baked_nodes;
	bool *nodes_to_bake;

	char *tmp_arr;
	size_t tmp_arr_size;

	const ufbx_scene *scene;
	const ufbx_anim *anim;
	ufbx_bake_opts opts;

	double ktime_offset;

	double time_begin;
	double time_end;
	double time_min;
	double time_max;

	ufbx_baked_anim bake;
	ufbxi_baked_anim_imp *imp;
} ufbxi_bake_context;

typedef struct {
	uint32_t sort_id;
	uint32_t element_id;
	const char *prop_name;
	ufbx_anim_value *anim_value;
} ufbxi_bake_prop;

static bool ufbxi_bake_prop_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbxi_bake_prop *a = (const ufbxi_bake_prop*)va;
	const ufbxi_bake_prop *b = (const ufbxi_bake_prop*)vb;
	if (a->sort_id != b->sort_id) return a->sort_id < b->sort_id;
	if (a->element_id != b->element_id) return a->element_id < b->element_id;
	if (a->prop_name != b->prop_name) return strcmp(a->prop_name, b->prop_name) < 0;
	return false;
}

ufbx_static_assert(bake_step_left, UFBX_BAKED_KEY_STEP_LEFT == 0x1);
ufbx_static_assert(bake_step_right, UFBX_BAKED_KEY_STEP_RIGHT == 0x2);
ufbx_static_assert(bake_step_key, UFBX_BAKED_KEY_STEP_KEY == 0x4);
static ufbxi_forceinline int ufbxi_cmp_bake_time(ufbxi_bake_time a, ufbxi_bake_time b)
{
	if (a.time != b.time) return a.time < b.time ? -1 : 1;
	// Bit twiddling for a fast sorting of `0x1 (LEFT) < 0x0 < 0x2 (RIGHT)`
	// by `step ^ 1`: `0x0 (LEFT) < 0x1 < 0x3 (RIGHT)`
	uint32_t a_step = a.flags & 0x3, b_step = b.flags & 0x3;
	if (a_step != b_step) return (a_step ^ 0x1) < (b_step ^ 0x1) ? -1 : 1;
	return 0;
}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_bake_push_time(ufbxi_bake_context *bc, double time, uint32_t flags)
{
	ufbxi_bake_time *p_key = ufbxi_push_fast(&bc->tmp_times, ufbxi_bake_time, 1);
	if (!p_key) return 0;
	p_key->time = time;
	p_key->flags = flags;
	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_times(ufbxi_bake_context *bc, const ufbx_anim_value *anim_value, bool resample_linear, uint32_t key_flag)
{
	double sample_rate = bc->opts.resample_rate;
	double min_duration = bc->opts.minimum_sample_rate > 0.0 ? 1.0 / bc->opts.minimum_sample_rate : 0.0;

	for (size_t curve_ix = 0; curve_ix < 3; curve_ix++) {
		ufbx_anim_curve *curve = anim_value->curves[curve_ix];
		if (!curve) continue;

		const ufbx_keyframe *keys = curve->keyframes.data;
		size_t num_keys = curve->keyframes.count;
		for (size_t key_ix = 0; key_ix < num_keys; key_ix++) {
			ufbx_keyframe a = keys[key_ix];
			double a_time = a.time;
			ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, a_time, key_flag));
			if (key_ix + 1 >= num_keys) break;
			ufbx_keyframe b = keys[key_ix + 1];
			double b_time = b.time;

			// Skip fully flat sections
			if (a.value == b.value && a.right.dy == 0.0f && b.left.dy == 0.0f) continue;

			if (a.interpolation == UFBX_INTERPOLATION_CONSTANT_PREV) {
				ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, b_time, UFBX_BAKED_KEY_STEP_LEFT));
			} else if (a.interpolation == UFBX_INTERPOLATION_CONSTANT_NEXT) {
				ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, a_time, UFBX_BAKED_KEY_STEP_RIGHT));
			} else if ((resample_linear || a.interpolation == UFBX_INTERPOLATION_CUBIC) && sample_rate > 0.0) {
				double duration = b_time - a_time;
				if (duration <= min_duration) continue;

				double factor = 1.0;
				while (duration * sample_rate / factor >= (double)bc->opts.max_keyframe_segments) {
					factor *= 2.0;
				}

				double padding = 0.5 / sample_rate;
				double start = ufbx_ceil((a_time + padding) * sample_rate / factor) * factor;
				double stop = b_time - padding;
				for (size_t i = 0; i < bc->opts.max_keyframe_segments; i++) {
					double time = (start + (double)i * factor) / sample_rate;
					if (time >= stop) break;
					ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, time, 0));
				}
			}
		}
	}

	return 1;
}

static const char *const ufbxi_transform_props[] = {
	ufbxi_Lcl_Translation, ufbxi_Lcl_Rotation, ufbxi_Lcl_Scaling, ufbxi_PreRotation, ufbxi_PostRotation,
	ufbxi_RotationOffset, ufbxi_ScalingOffset, ufbxi_RotationPivot, ufbxi_ScalingPivot, ufbxi_RotationOrder,
};

static const char *const ufbxi_complex_translation_props[] = {
	ufbxi_ScalingPivot, ufbxi_RotationPivot, ufbxi_RotationOffset, ufbxi_ScalingOffset,
};

static const char *const ufbxi_complex_rotation_props[] = {
	ufbxi_PreRotation, ufbxi_PostRotation, ufbxi_RotationOrder,
};

static const char *const ufbxi_complex_rotation_sources[] = {
	ufbxi_Lcl_Rotation, ufbxi_PreRotation, ufbxi_PostRotation, ufbxi_RotationOrder,
};

ufbxi_nodiscard static ufbxi_noinline bool ufbxi_in_list(const char *const *items, size_t count, const char *item)
{
	for (size_t i = 0; i < count; i++) {
		if (items[i] == item) return true;
	}
	return false;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_sort_bake_times(ufbxi_bake_context *bc, ufbxi_bake_time *times, size_t count)
{
	ufbxi_check_err(&bc->error, ufbxi_grow_array(&bc->ator_tmp, &bc->tmp_arr, &bc->tmp_arr_size, count * sizeof(ufbxi_bake_time)));
	ufbxi_macro_stable_sort(ufbxi_bake_time, 32, times, bc->tmp_arr, count, ( ufbxi_cmp_bake_time(*a, *b) < 0 ));
	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_bake_times(ufbxi_bake_context *bc, ufbxi_bake_time_list *p_dst)
{
	if (bc->layer_weight_times.count > 0) {
		ufbxi_check_err(&bc->error, ufbxi_push_copy(&bc->tmp_times, ufbxi_bake_time, bc->layer_weight_times.count, bc->layer_weight_times.data));
	}

	if (bc->tmp_times.num_items == 0) {
		ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, bc->time_begin, 0));
		ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, bc->time_end, 0));
	}

	size_t num_times = bc->tmp_times.num_items;
	ufbxi_bake_time *times = ufbxi_push_pop(&bc->tmp_prop, &bc->tmp_times, ufbxi_bake_time, num_times);
	ufbxi_check_err(&bc->error, times);

	ufbxi_check_err(&bc->error, ufbxi_sort_bake_times(bc, times, num_times));

	// Deduplicate times
	if (num_times > 0) {
		size_t dst = 0;
		ufbxi_bake_time prev = times[0];
		for (size_t src = 1; src < num_times; src++) {
			ufbxi_bake_time next = times[src];
			// Merge keys with the same time and step flags `(0x1, 0x2)`
			if (next.time == prev.time) {
				if (((next.flags ^ prev.flags) & 0x3) == 0) {
					prev.flags |= next.flags;
					continue;
				} else if (prev.flags & UFBX_BAKED_KEY_STEP_LEFT) {
					next.flags |= UFBX_BAKED_KEY_STEP_KEY;
				} else if (next.flags & UFBX_BAKED_KEY_STEP_RIGHT) {
					prev.flags |= UFBX_BAKED_KEY_STEP_KEY;
				}
			}

			times[dst++] = prev;
			prev = next;
		}
		times[dst++] = prev;
		num_times = dst;
	}

	// Cull too close resampled keys, these may arise during merging multiple times
	if (num_times > 0) {
		double min_dist = 0.25 / bc->opts.resample_rate;
		uint32_t keep_flags = UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT|UFBX_BAKED_KEY_STEP_KEY|UFBX_BAKED_KEY_KEYFRAME;

		size_t dst = 0;
		for (size_t src = 0; src < num_times; src++) {
			ufbxi_bake_time cur = times[src];
			double delta = UFBX_INFINITY;

			bool keep = true;
			if ((cur.flags & keep_flags) == 0) {
				if (dst > 0) delta = cur.time - times[dst - 1].time;
				if (src + 1 < num_times) delta = ufbx_fmin(delta, times[src + 1].time - cur.time);
				if (delta < min_dist) keep = false;
			}
			if (keep) {
				times[dst++] = cur;
			}
		}
		num_times = dst;
	}

	// Enforce maximum sample rate
	if (bc->opts.maximum_sample_rate > 0.0) {
		const double epsilon = 0.0078125 / bc->opts.maximum_sample_rate;
		double sample_rate = bc->opts.maximum_sample_rate;
		double max_interval = 1.0 / bc->opts.maximum_sample_rate;
		double min_interval = 1.0 / bc->opts.maximum_sample_rate - epsilon;
		size_t dst = 0, src = 0;

		// Pre-expand constant keyframes
		for (size_t i = 0; i < num_times; i++) {
			if ((times[i].flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0) {
				double sign = (times[i].flags & UFBX_BAKED_KEY_STEP_LEFT) != 0 ? -1.0 : 1.0;
				double time = times[i].time + sign * max_interval;
				if (i > 0) time = ufbx_fmax(time, times[i - 1].time);
				if (i + 1 < num_times) time = ufbx_fmin(time, times[i + 1].time);
				times[i].time = time;
				times[i].flags = UFBX_BAKED_KEY_REDUCED;
			}
		}

		ufbxi_bake_time prev_time = { -UFBX_INFINITY };
		while (src < num_times) {
			ufbxi_bake_time src_time = times[src];
			src++;

			size_t start_src = src;
			ufbxi_bake_time next_time;
			next_time.time = ufbx_ceil(src_time.time * sample_rate - epsilon) / sample_rate;
			next_time.flags = UFBX_BAKED_KEY_REDUCED;
			while (src < num_times && times[src].time <= next_time.time + epsilon) {
				src++;
			}

			if (src != start_src || src_time.time - prev_time.time <= min_interval) {
				prev_time = next_time;
			} else {
				prev_time = src_time;
			}

			if (dst == 0 || prev_time.time > times[dst - 1].time) {
				times[dst++] = prev_time;
			}
		}

		num_times = dst;
	}

	if (num_times > 0) {
		if (times[0].time < bc->time_min) bc->time_min = times[0].time;
		if (times[num_times - 1].time > bc->time_max) bc->time_max = times[num_times - 1].time;
	}

	p_dst->data = times;
	p_dst->count = num_times;

	return 1;
}

#define ufbxi_add_epsilon(a, epsilon) ((a)>0 ? (a)*(epsilon) : (a)/(epsilon))
#define ufbxi_sub_epsilon(a, epsilon) ((a)>0 ? (a)/(epsilon) : (a)*(epsilon))

static ufbxi_noinline bool ufbxi_postprocess_step(ufbxi_bake_context *bc, double prev_time, double next_time, double *p_time, ufbx_baked_key_flags flags)
{
	ufbxi_dev_assert((flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0);
	bool left = (flags & UFBX_BAKED_KEY_STEP_LEFT) != 0;

	double step = 0.001;
	double epsilon = 1.0 + UFBX_FLT_EPSILON * 4.0f;

	double time = *p_time;
	switch (bc->opts.step_handling) {
	case UFBX_BAKE_STEP_HANDLING_DEFAULT:
		break;
	case UFBX_BAKE_STEP_HANDLING_CUSTOM_DURATION:
		step = bc->opts.step_custom_duration;
		epsilon = 1.0 + bc->opts.step_custom_epsilon;
		break;
	case UFBX_BAKE_STEP_HANDLING_IDENTICAL_TIME:
		return true;
	case UFBX_BAKE_STEP_HANDLING_ADJACENT_DOUBLE:
		if (left) {
			*p_time = time = ufbx_nextafter(time, -UFBX_INFINITY);
			return time > prev_time;
		} else {
			*p_time = time = ufbx_nextafter(time, UFBX_INFINITY);
			return time < next_time;
		}
	case UFBX_BAKE_STEP_HANDLING_IGNORE:
		return false;
	default:
		ufbxi_unreachable("Unhandled bake step handling");
		return false;
	}

	if (left) {
		double min_time = ufbx_fmax(prev_time + step, ufbxi_add_epsilon(prev_time, epsilon));
		*p_time = time = ufbx_fmin(time - step, ufbxi_sub_epsilon(time, epsilon));
		return time > min_time;
	} else {
		double max_time = ufbx_fmin(next_time - step, ufbxi_sub_epsilon(next_time, epsilon));
		*p_time = time = ufbx_fmax(time + step, ufbxi_add_epsilon(time, epsilon));
		return time < max_time;
	}
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_postprocess_vec3(ufbxi_bake_context *bc, ufbx_baked_vec3_list *p_dst, bool *p_constant, ufbx_baked_vec3_list src)
{
	if (src.count == 0) return 1;

	// Offset times
	if (bc->ktime_offset != 0.0) {
		double scale = (double)bc->scene->metadata.ktime_second;
		double offset = bc->ktime_offset;
		for (size_t i = 0; i < src.count; i++) {
			src.data[i].time = ufbx_rint(src.data[i].time * scale + offset) / scale;
		}
	}

	// Postprocess stepped tangents
	{
		size_t dst = 0;
		double prev_time = src.data[0].time;
		for (size_t i = 0; i < src.count; i++) {
			ufbx_baked_vec3 cur = src.data[i];
			double next_time = i + 1 < src.count ? src.data[i + 1].time : UFBX_INFINITY;
			bool keep = true;
			if ((cur.flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0) {
				keep = ufbxi_postprocess_step(bc, prev_time, next_time, &cur.time, cur.flags);
			}
			if (keep) {
				src.data[dst] = cur;
				dst++;
				prev_time = cur.time;
			}
		}
		src.count = dst;
	}

	if (bc->opts.key_reduction_enabled) {
		double threshold = bc->opts.key_reduction_threshold * bc->opts.key_reduction_threshold;
		for (size_t pass = 0; pass < bc->opts.key_reduction_passes; pass++) {
			size_t dst = 1;
			for (size_t i = 1; i < src.count; i++) {
				ufbx_baked_vec3 prev = src.data[i - 1];
				ufbx_baked_vec3 cur = src.data[i];
				if (i + 1 < src.count) {
					ufbx_baked_vec3 next = src.data[i + 1];
					double delta = (cur.time - prev.time) / (next.time - prev.time);
					ufbx_vec3 tmp = ufbxi_lerp3(prev.value, next.value, (ufbx_real)delta);
					double error = 0.0;
					error += ((double)tmp.x - (double)cur.value.x) * ((double)tmp.x - (double)cur.value.x);
					error += ((double)tmp.y - (double)cur.value.y) * ((double)tmp.y - (double)cur.value.y);
					error += ((double)tmp.z - (double)cur.value.z) * ((double)tmp.z - (double)cur.value.z);
					if (error <= threshold) {
						src.data[dst] = src.data[i + 1];
						i += 1;
						dst += 1;
						continue;
					}
				}

				src.data[dst] = src.data[i];
				dst += 1;
			}
			if (dst == src.count) break;
			src.count = dst;
		}
	}

	bool constant = true;
	ufbx_vec3 ref = src.data[0].value;
	for (size_t i = 1; i < src.count; i++) {
		ufbx_vec3 v = src.data[i].value;
		if (v.x != ref.x || v.y != ref.y || v.z != ref.z) {
			constant = false;
			break;
		}
	}
	*p_constant = constant;

	p_dst->count = src.count;
	p_dst->data = ufbxi_push_copy(&bc->result, ufbx_baked_vec3, src.count, src.data);
	ufbxi_check_err(&bc->error, p_dst->data);

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_postprocess_quat(ufbxi_bake_context *bc, ufbx_baked_quat_list *p_dst, bool *p_constant, ufbx_baked_quat_list src)
{
	if (src.count == 0) return 1;

	// Offset times
	if (bc->ktime_offset != 0.0) {
		double scale = (double)bc->scene->metadata.ktime_second;
		double offset = bc->ktime_offset;
		for (size_t i = 0; i < src.count; i++) {
			src.data[i].time = ufbx_rint(src.data[i].time * scale + offset) / scale;
		}
	}

	// Postprocess stepped tangents
	{
		size_t dst = 0;
		double prev_time = src.data[0].time;
		for (size_t i = 0; i < src.count; i++) {
			ufbx_baked_quat cur = src.data[i];
			double next_time = i + 1 < src.count ? src.data[i + 1].time : UFBX_INFINITY;
			bool keep = true;
			if ((cur.flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0) {
				keep = ufbxi_postprocess_step(bc, prev_time, next_time, &cur.time, cur.flags);
			}
			if (keep) {
				prev_time = cur.time;
				src.data[dst] = cur;
				dst++;
			}
		}
		src.count = dst;
	}

	// Fix quaternion antipodality
	for (size_t i = 1; i < src.count; i++) {
		src.data[i].value = ufbx_quat_fix_antipodal(src.data[i].value, src.data[i - 1].value);
	}

	if (bc->opts.key_reduction_enabled) {
		double threshold = bc->opts.key_reduction_threshold * bc->opts.key_reduction_threshold;
		for (size_t pass = 0; pass < bc->opts.key_reduction_passes; pass++) {
			size_t dst = 1;
			for (size_t i = 1; i < src.count; i++) {
				ufbx_baked_quat prev = src.data[i - 1];
				ufbx_baked_quat cur = src.data[i];
				if (i + 1 < src.count) {
					ufbx_baked_quat next = src.data[i + 1];
					double delta = (cur.time - prev.time) / (next.time - prev.time);
					double error = 0.0;

					if (bc->opts.key_reduction_rotation) {
						ufbx_quat tmp = ufbx_quat_slerp(prev.value, next.value, (ufbx_real)delta);
						error += ((double)tmp.x - (double)cur.value.x) * ((double)tmp.x - (double)cur.value.x);
						error += ((double)tmp.y - (double)cur.value.y) * ((double)tmp.y - (double)cur.value.y);
						error += ((double)tmp.z - (double)cur.value.z) * ((double)tmp.z - (double)cur.value.z);
						error += ((double)tmp.w - (double)cur.value.w) * ((double)tmp.w - (double)cur.value.w);
					} else {
						error += ((double)prev.value.x - (double)cur.value.x) * ((double)prev.value.x - (double)cur.value.x);
						error += ((double)prev.value.y - (double)cur.value.y) * ((double)prev.value.y - (double)cur.value.y);
						error += ((double)prev.value.z - (double)cur.value.z) * ((double)prev.value.z - (double)cur.value.z);
						error += ((double)prev.value.w - (double)cur.value.w) * ((double)prev.value.w - (double)cur.value.w);
						error += ((double)next.value.x - (double)cur.value.x) * ((double)next.value.x - (double)cur.value.x);
						error += ((double)next.value.y - (double)cur.value.y) * ((double)next.value.y - (double)cur.value.y);
						error += ((double)next.value.z - (double)cur.value.z) * ((double)next.value.z - (double)cur.value.z);
						error += ((double)next.value.w - (double)cur.value.w) * ((double)next.value.w - (double)cur.value.w);
						error *= 0.5;
					}

					if (error <= threshold) {
						src.data[dst] = src.data[i + 1];
						i += 1;
						dst += 1;
						continue;
					}
				}

				src.data[dst] = src.data[i];
				dst += 1;
			}
			if (dst == src.count) break;
			src.count = dst;
		}
	}

	bool constant = true;
	ufbx_quat ref = src.data[0].value;
	for (size_t i = 1; i < src.count; i++) {
		ufbx_quat v = src.data[i].value;
		if (v.x != ref.x || v.y != ref.y || v.z != ref.z || v.y != ref.w) {
			constant = false;
			break;
		}
	}
	*p_constant = constant;

	p_dst->count = src.count;
	p_dst->data = ufbxi_push_copy(&bc->result, ufbx_baked_quat, src.count, src.data);
	ufbxi_check_err(&bc->error, p_dst->data);

	return 1;
}

static ufbxi_forceinline double ufbxi_bake_time_sample_time(ufbxi_bake_time time)
{
	// Move an infinitesimal step for stepped tangents
	if ((time.flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0) {
		double dir = (time.flags & UFBX_BAKED_KEY_STEP_LEFT) != 0 ? -UFBX_INFINITY : UFBX_INFINITY;
		return ufbx_nextafter(time.time, dir);
	} else {
		return time.time;
	}
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_resampled_times(ufbxi_bake_context *bc, const ufbx_baked_vec3_list *p_keys)
{
	ufbx_baked_vec3_list keys = *p_keys;

	ufbxi_bake_time *times = ufbxi_push(&bc->tmp_times, ufbxi_bake_time, keys.count);
	ufbxi_check_err(&bc->error, times);
	for (size_t i = 0; i < keys.count; i++) {
		ufbx_baked_key_flags flags = keys.data[i].flags;
		double time = keys.data[i].time;
		if ((flags & UFBX_BAKED_KEY_STEP_LEFT) != 0 && i + 1 < keys.count && (keys.data[i + 1].flags & UFBX_BAKED_KEY_STEP_KEY) != 0) {
			time = keys.data[i + 1].time;
		} else if ((flags & UFBX_BAKED_KEY_STEP_RIGHT) != 0 && i > 0 && (keys.data[i - 1].flags & UFBX_BAKED_KEY_STEP_KEY) != 0) {
			time = keys.data[i - 1].time;
		}
		times[i].time = time;
		times[i].flags = flags & 0x7;
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context *bc, uint32_t element_id, ufbxi_bake_prop *props, size_t count)
{
	ufbx_assert(bc->baked_nodes && bc->nodes_to_bake);

	ufbx_node *node = (ufbx_node*)bc->scene->elements.data[element_id];
	ufbxi_dev_assert(node->element.type == UFBX_ELEMENT_NODE);

	bool complex_translation = false;
	bool complex_rotation = false;

	for (size_t i = 0; i < ufbxi_arraycount(ufbxi_complex_translation_props); i++) {
		const char *name = ufbxi_complex_translation_props[i];
		ufbx_prop *prop = ufbxi_find_prop(&node->props, name);
		if (prop && !ufbxi_is_vec3_zero(prop->value_vec3)) {
			complex_translation = true;
		}
		ufbxi_for(ufbxi_bake_prop, bprop, props, count) {
			if (bprop->prop_name == name) {
				complex_translation = true;
			}
		}
	}

	for (size_t i = 0; i < ufbxi_arraycount(ufbxi_complex_rotation_props); i++) {
		const char *name = ufbxi_complex_rotation_props[i];
		ufbxi_for(ufbxi_bake_prop, bprop, props, count) {
			if (bprop->prop_name == name) {
				complex_rotation = true;
			}
		}
	}

	ufbxi_bake_time_list times_t, times_r, times_s;

	// Translation
	bool resample_translation = false;

	// Account for the _resampled_ scale helper scale animation to keep the
	// translation scale consistent with the parent scaling.
	ufbx_baked_node *scale_helper_t = NULL;
	ufbx_vec3 constant_scale_t = { 1.0f, 1.0f, 1.0f };
	if (!node->is_scale_helper && node->parent && node->parent->scale_helper) {
		scale_helper_t = bc->baked_nodes[node->parent->scale_helper->typed_id];
		if (scale_helper_t) {
			if (!scale_helper_t->constant_scale) {
				resample_translation = true;
			}
			ufbxi_check_err(&bc->error, ufbxi_push_resampled_times(bc, &scale_helper_t->scale_keys));
		} else {
			constant_scale_t = node->parent->scale_helper->inherit_scale;
		}
	}

	if (complex_translation) {
		ufbxi_for(ufbxi_bake_prop, prop, props, count) {
			// Literally any transform related property can affect complex translation
			if (ufbxi_in_list(ufbxi_transform_props, ufbxi_arraycount(ufbxi_transform_props), prop->prop_name)) {
				bool resample_linear = resample_translation || prop->prop_name != ufbxi_Lcl_Translation;
				uint32_t key_flag = prop->prop_name == ufbxi_Lcl_Translation ? UFBX_BAKED_KEY_KEYFRAME : 0;
				ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_linear, key_flag));
			}
		}
	} else {
		ufbxi_for(ufbxi_bake_prop, prop, props, count) {
			if (prop->prop_name == ufbxi_Lcl_Translation) {
				ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_translation, UFBX_BAKED_KEY_KEYFRAME));
			}
		}
	}

	ufbxi_check_err(&bc->error, ufbxi_finalize_bake_times(bc, &times_t));

	// Rotation
	if (complex_rotation) {
		ufbxi_for(ufbxi_bake_prop, prop, props, count) {
			if (ufbxi_in_list(ufbxi_complex_rotation_sources, ufbxi_arraycount(ufbxi_complex_rotation_sources), prop->prop_name)) {
				bool resample_linear = !bc->opts.no_resample_rotation || prop->prop_name != ufbxi_Lcl_Rotation;
				uint32_t key_flag = prop->prop_name == ufbxi_Lcl_Rotation ? UFBX_BAKED_KEY_KEYFRAME : 0;
				ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_linear, key_flag));
			}
		}
	} else {
		ufbxi_for(ufbxi_bake_prop, prop, props, count) {
			if (prop->prop_name == ufbxi_Lcl_Rotation) {
				ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, !bc->opts.no_resample_rotation, UFBX_BAKED_KEY_KEYFRAME));
			}
		}
	}
	ufbxi_check_err(&bc->error, ufbxi_finalize_bake_times(bc, &times_r));

	// Scaling
	bool resample_scale = false;

	// Account for the resampled scale
	ufbx_baked_node *scale_helper_s = NULL;
	ufbx_vec3 constant_scale_s = { 1.0f, 1.0f, 1.0f };
	if (node->is_scale_helper && node->parent && node->parent->inherit_scale_node && node->parent->inherit_scale_node->scale_helper) {
		ufbx_node *inherit_helper = node->parent->inherit_scale_node->scale_helper;
		scale_helper_s = bc->baked_nodes[inherit_helper->typed_id];
		if (scale_helper_s) {
			if (!scale_helper_s->constant_scale) {
				resample_scale = true;
			}
			ufbxi_check_err(&bc->error, ufbxi_push_resampled_times(bc, &scale_helper_s->scale_keys));
		} else {
			constant_scale_s = inherit_helper->local_transform.scale;
		}
	}

	ufbxi_for(ufbxi_bake_prop, prop, props, count) {
		if (prop->prop_name == ufbxi_Lcl_Scaling) {
			ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_scale, UFBX_BAKED_KEY_KEYFRAME));
		}
	}
	ufbxi_check_err(&bc->error, ufbxi_finalize_bake_times(bc, &times_s));

	ufbx_baked_vec3_list keys_t;
	ufbx_baked_quat_list keys_r;
	ufbx_baked_vec3_list keys_s;

	keys_t.count = times_t.count;
	keys_t.data = ufbxi_push(&bc->tmp_prop, ufbx_baked_vec3, keys_t.count);
	ufbxi_check_err(&bc->error, keys_t.data);

	keys_r.count = times_r.count;
	keys_r.data = ufbxi_push(&bc->tmp_prop, ufbx_baked_quat, keys_r.count);
	ufbxi_check_err(&bc->error, keys_r.data);

	keys_s.count = times_s.count;
	keys_s.data = ufbxi_push(&bc->tmp_prop, ufbx_baked_vec3, keys_s.count);
	ufbxi_check_err(&bc->error, keys_s.data);

	size_t ix_t = 0, ix_r = 0, ix_s = 0;
	while (ix_t < times_t.count || ix_r < times_r.count || ix_s < times_s.count) {
		ufbxi_bake_time bake_time = { UFBX_INFINITY };
		uint32_t flags_r = 0, flags_t = 0, flags_s = 0;

		uint32_t flags = 0;
		if (ix_r < times_r.count) {
			bake_time = times_r.data[ix_r];
			flags_r = bake_time.flags;
			bake_time.flags &= 0x7;
			flags |= UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION;
		}
		if (ix_t < times_t.count) {
			ufbxi_bake_time t = times_t.data[ix_t];
			int cmp = ufbxi_cmp_bake_time(t, bake_time);
			if (cmp <= 0) {
				if (cmp < 0) {
					bake_time = t;
					flags = 0;
				}
				bake_time.flags |= t.flags & 0x7;
				flags_t = t.flags;
				flags |= UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION;
			}
		}
		if (ix_s < times_s.count) {
			ufbxi_bake_time t = times_s.data[ix_s];
			int cmp = ufbxi_cmp_bake_time(t, bake_time);
			if (cmp <= 0) {
				if (cmp < 0) {
					bake_time = t;
					flags = 0;
				}
				bake_time.flags |= t.flags & 0x7;
				flags_s = t.flags;
				flags |= UFBX_TRANSFORM_FLAG_INCLUDE_SCALE;
			}
		}

		flags |= UFBX_TRANSFORM_FLAG_IGNORE_SCALE_HELPER|UFBX_TRANSFORM_FLAG_IGNORE_COMPONENTWISE_SCALE|UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES;

		double eval_time = ufbxi_bake_time_sample_time(bake_time);
		ufbx_transform transform = ufbx_evaluate_transform_flags(bc->anim, node, eval_time, flags);

		if (flags & UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION) {
			if (scale_helper_t) {
				ufbx_vec3 scale = ufbx_evaluate_baked_vec3(scale_helper_t->scale_keys, eval_time);
				transform.translation.x *= scale.x;
				transform.translation.y *= scale.y;
				transform.translation.z *= scale.z;
			}

			transform.translation.x *= constant_scale_t.x;
			transform.translation.y *= constant_scale_t.y;
			transform.translation.z *= constant_scale_t.z;

			keys_t.data[ix_t].time = bake_time.time;
			keys_t.data[ix_t].value = transform.translation;
			keys_t.data[ix_t].flags = (ufbx_baked_key_flags)(bake_time.flags | flags_t);
			ix_t++;
		}
		if (flags & UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION) {
			keys_r.data[ix_r].time = bake_time.time;
			keys_r.data[ix_r].value = transform.rotation;
			keys_r.data[ix_r].flags = (ufbx_baked_key_flags)(bake_time.flags | flags_r);
			ix_r++;
		}
		if (flags & UFBX_TRANSFORM_FLAG_INCLUDE_SCALE) {
			if (scale_helper_s) {
				ufbx_vec3 scale = ufbx_evaluate_baked_vec3(scale_helper_s->scale_keys, eval_time);
				transform.scale.x *= scale.x;
				transform.scale.y *= scale.y;
				transform.scale.z *= scale.z;
			}

			transform.scale.x *= constant_scale_s.x;
			transform.scale.y *= constant_scale_s.y;
			transform.scale.z *= constant_scale_s.z;

			keys_s.data[ix_s].time = bake_time.time;
			keys_s.data[ix_s].value = transform.scale;
			keys_s.data[ix_s].flags = (ufbx_baked_key_flags)(bake_time.flags | flags_s);
			ix_s++;
		}
	}

	ufbx_baked_node *baked_node = ufbxi_push_zero(&bc->tmp_nodes, ufbx_baked_node, 1);
	ufbxi_check_err(&bc->error, baked_node);

	baked_node->element_id = node->element_id;
	baked_node->typed_id = node->typed_id;
	ufbxi_check_err(&bc->error, ufbxi_bake_postprocess_vec3(bc, &baked_node->translation_keys, &baked_node->constant_translation, keys_t));
	ufbxi_check_err(&bc->error, ufbxi_bake_postprocess_quat(bc, &baked_node->rotation_keys, &baked_node->constant_rotation, keys_r));
	ufbxi_check_err(&bc->error, ufbxi_bake_postprocess_vec3(bc, &baked_node->scale_keys, &baked_node->constant_scale, keys_s));

	bc->baked_nodes[node->typed_id] = baked_node;

	ufbxi_buf_clear(&bc->tmp_prop);

	// If this node is a scale helper, make sure to bake its siblings and
	// potentially their scale helpers if they are not a part of the animation.
	if (node->is_scale_helper) {
		ufbx_assert(node->parent);
		ufbxi_for_ptr_list(ufbx_node, p_child, node->parent->children) {
			ufbx_node *child = *p_child;
			if (child == node) continue;
			if (!bc->nodes_to_bake[child->typed_id]) {
				bc->nodes_to_bake[child->typed_id] = true;
				ufbxi_check_err(&bc->error, ufbxi_push_copy(&bc->tmp_bake_stack, uint32_t, 1, &child->element_id));
			}
			if (child->inherit_scale_node && child->inherit_scale_node->scale_helper && child->scale_helper
					&& bc->nodes_to_bake[child->inherit_scale_node->scale_helper->typed_id]) {
				ufbx_assert(bc->baked_nodes[child->inherit_scale_node->scale_helper->typed_id]);
				if (!bc->nodes_to_bake[child->scale_helper->typed_id]) {
					bc->nodes_to_bake[child->scale_helper->typed_id] = true;
					ufbxi_check_err(&bc->error, ufbxi_push_copy(&bc->tmp_bake_stack, uint32_t, 1, &child->scale_helper->element_id));
				}
			}
		}
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node(ufbxi_bake_context *bc, uint32_t element_id, ufbxi_bake_prop *props, size_t count)
{
	ufbxi_check_err(&bc->error, ufbxi_bake_node_imp(bc, element_id, props, count));

	// Baking a node may cause further nodes to be baked, so keep going
	// until all dependencies are baked.
	while (bc->tmp_bake_stack.num_items > 0) {
		uint32_t child_id = 0;
		ufbxi_pop(&bc->tmp_bake_stack, uint32_t, 1, &child_id);
		ufbxi_check_err(&bc->error, ufbxi_bake_node_imp(bc, child_id, NULL, 0));
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim_prop(ufbxi_bake_context *bc, ufbx_element *element, const char *prop_name, ufbxi_bake_prop *props, size_t count)
{
	ufbxi_for(ufbxi_bake_prop, prop, props, count) {
		ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, false, UFBX_BAKED_KEY_KEYFRAME));
	}

	ufbxi_bake_time_list times;
	ufbxi_check_err(&bc->error, ufbxi_finalize_bake_times(bc, &times));

	ufbx_baked_vec3_list keys;
	keys.count = times.count;
	keys.data = ufbxi_push(&bc->tmp_prop, ufbx_baked_vec3, keys.count);
	ufbxi_check_err(&bc->error, keys.data);

	ufbx_string name;
	name.data = prop_name;
	name.length = strlen(prop_name);

	for (size_t i = 0; i < times.count; i++) {
		ufbxi_bake_time bake_time = times.data[i];
		double eval_time = ufbxi_bake_time_sample_time(bake_time);
		ufbx_prop prop = ufbx_evaluate_prop_len(bc->anim, element, name.data, name.length, eval_time);
		keys.data[i].time = bake_time.time;
		keys.data[i].value = prop.value_vec3;
		keys.data[i].flags = (ufbx_baked_key_flags)bake_time.flags;
	}

	ufbx_baked_prop *baked_prop = ufbxi_push_zero(&bc->tmp_props, ufbx_baked_prop, 1);
	ufbxi_check_err(&bc->error, baked_prop);

	baked_prop->name.length = strlen(prop_name);
	baked_prop->name.data = ufbxi_push_copy(&bc->result, char, baked_prop->name.length + 1, prop_name);
	ufbxi_check_err(&bc->error, baked_prop->name.data);

	ufbxi_check_err(&bc->error, ufbxi_bake_postprocess_vec3(bc, &baked_prop->keys, &baked_prop->constant_value, keys));

	ufbxi_buf_clear(&bc->tmp_prop);

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_element(ufbxi_bake_context *bc, uint32_t element_id, ufbxi_bake_prop *props, size_t count)
{
	ufbx_element *element = bc->scene->elements.data[element_id];
	if (element->type == UFBX_ELEMENT_NODE && !bc->opts.skip_node_transforms) {
		ufbxi_check_err(&bc->error, ufbxi_bake_node(bc, element_id, props, count));
	}

	size_t begin = 0;
	while (begin < count) {
		const char *prop_name = props[begin].prop_name;
		size_t end = begin + 1;
		while (end < count && props[end].prop_name == prop_name) {
			end++;
		}

		// Don't bake transform related props for nodes unless specifically requested
		if (element->type == UFBX_ELEMENT_NODE && !bc->opts.bake_transform_props && ufbxi_in_list(ufbxi_transform_props, ufbxi_arraycount(ufbxi_transform_props), prop_name)) {
			begin = end;
			continue;
		}

		ufbxi_check_err(&bc->error, ufbxi_bake_anim_prop(bc, element, prop_name, props + begin, end - begin));
		begin = end;
	}

	size_t num_props = bc->tmp_props.num_items;
	if (num_props > 0) {
		ufbx_baked_element *baked_elem = ufbxi_push_zero(&bc->tmp_elements, ufbx_baked_element, 1);
		ufbxi_check_err(&bc->error, baked_elem);

		baked_elem->element_id = element->element_id;
		baked_elem->props.count = num_props;
		baked_elem->props.data = ufbxi_push_pop(&bc->result, &bc->tmp_props, ufbx_baked_prop, num_props);
		ufbxi_check_err(&bc->error, baked_elem->props.data);
	}

	return 1;
}

static ufbxi_noinline bool ufbxi_baked_node_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_baked_node *a = (const ufbx_baked_node*)va, *b = (const ufbx_baked_node*)vb;
	return a->typed_id < b->typed_id;
}

static ufbxi_noinline bool ufbxi_baked_element_less(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_baked_element *a = (const ufbx_baked_element*)va, *b = (const ufbx_baked_element*)vb;
	return a->element_id < b->element_id;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim(ufbxi_bake_context *bc)
{
	const ufbx_anim *anim = bc->anim;
	const ufbx_scene *scene = bc->scene;

	if (!bc->opts.skip_node_transforms) {
		bc->baked_nodes = ufbxi_push_zero(&bc->result, ufbx_baked_node*, scene->nodes.count);
		ufbxi_check_err(&bc->error, bc->baked_nodes);
		bc->nodes_to_bake = ufbxi_push_zero(&bc->result, bool, scene->nodes.count);
		ufbxi_check_err(&bc->error, bc->nodes_to_bake);
	}

	ufbxi_for_ptr_list(ufbx_anim_layer, p_layer, anim->layers) {
		ufbx_anim_layer *layer = *p_layer;

		ufbxi_for_list(ufbx_anim_prop, anim_prop, layer->anim_props) {
			ufbxi_bake_prop *prop = ufbxi_push(&bc->tmp_bake_props, ufbxi_bake_prop, 1);
			ufbxi_check_err(&bc->error, prop);

			ufbx_element *element = anim_prop->element;

			// Sort nodes by `typed_id` to make sure we process them in order.
			if (element->type == UFBX_ELEMENT_NODE) {
				if (bc->nodes_to_bake) {
					bc->nodes_to_bake[element->typed_id] = true;
				}
				prop->sort_id = element->typed_id;
			} else {
				prop->sort_id = UINT32_MAX;
			}

			prop->element_id = element->element_id;
			prop->prop_name = anim_prop->prop_name.data;
			prop->anim_value = anim_prop->anim_value;
		}
	}

	size_t num_props = bc->tmp_bake_props.num_items;
	ufbxi_bake_prop *props = ufbxi_push_pop(&bc->tmp, &bc->tmp_bake_props, ufbxi_bake_prop, num_props);
	ufbxi_check_err(&bc->error, props);

	ufbxi_unstable_sort(props, num_props, sizeof(ufbxi_bake_prop), &ufbxi_bake_prop_less, NULL);

	// Pre-bake layer weight times
	if (!bc->opts.ignore_layer_weight_animation) {
		bool has_weight_times = false;
		ufbxi_for(ufbxi_bake_prop, prop, props, num_props) {
			if (prop->prop_name != ufbxi_Weight) continue;
			ufbx_element *element = scene->elements.data[prop->element_id];
			if (element->type == UFBX_ELEMENT_ANIM_LAYER) {
				ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, true, 0));
				has_weight_times = true;
			}
		}

		if (has_weight_times) {
			ufbxi_bake_time_list weight_times = { 0 };
			ufbxi_check_err(&bc->error, ufbxi_finalize_bake_times(bc, &weight_times));

			bc->layer_weight_times.count = weight_times.count;
			bc->layer_weight_times.data = ufbxi_push_copy(&bc->tmp, ufbxi_bake_time, weight_times.count, weight_times.data);
			ufbxi_check_err(&bc->error, bc->layer_weight_times.data);

			ufbxi_buf_clear(&bc->tmp_prop);
		}
	}

	size_t begin = 0;
	while (begin < num_props) {
		uint32_t element_id = props[begin].element_id;
		size_t end = begin + 1;
		while (end < num_props && props[end].element_id == element_id) {
			end++;
		}
		ufbxi_check_err(&bc->error, ufbxi_bake_element(bc, element_id, props + begin, end - begin));
		begin = end;
	}

	size_t num_nodes = bc->tmp_nodes.num_items;
	size_t num_elements = bc->tmp_elements.num_items;

	bc->bake.nodes.count = num_nodes;
	bc->bake.nodes.data = ufbxi_push_pop(&bc->result, &bc->tmp_nodes, ufbx_baked_node, num_nodes);
	ufbxi_check_err(&bc->error, bc->bake.nodes.data);

	bc->bake.elements.count = num_elements;
	bc->bake.elements.data = ufbxi_push_pop(&bc->result, &bc->tmp_elements, ufbx_baked_element, num_elements);
	ufbxi_check_err(&bc->error, bc->bake.elements.data);

	ufbxi_unstable_sort(bc->bake.nodes.data, bc->bake.nodes.count, sizeof(ufbx_baked_node), &ufbxi_baked_node_less, NULL);
	ufbxi_unstable_sort(bc->bake.elements.data, bc->bake.elements.count, sizeof(ufbx_baked_element), &ufbxi_baked_element_less, NULL);

	if (bc->time_min < bc->time_max) {
		bc->bake.key_time_min = bc->time_min;
		bc->bake.key_time_max = bc->time_max;
	}

	if (bc->time_begin < bc->time_end) {
		bc->bake.playback_time_begin = bc->time_begin;
		bc->bake.playback_time_end = bc->time_end;
		bc->bake.playback_duration = bc->time_end - bc->time_begin;
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim_imp(ufbxi_bake_context *bc, const ufbx_anim *anim)
{
	if (bc->opts.resample_rate <= 0.0) bc->opts.resample_rate = 30.0;
	if (bc->opts.minimum_sample_rate <= 0.0) bc->opts.minimum_sample_rate = 19.5;
	if (bc->opts.max_keyframe_segments == 0) bc->opts.max_keyframe_segments = 32;
	if (bc->opts.key_reduction_threshold == 0) bc->opts.key_reduction_threshold = 0.000001;
	if (bc->opts.key_reduction_passes == 0) bc->opts.key_reduction_passes = 4;

	if (bc->opts.trim_start_time && anim->time_begin > 0.0) {
		bc->ktime_offset = -anim->time_begin * (double)bc->scene->metadata.ktime_second;
	}

	ufbxi_init_ator(&bc->error, &bc->ator_tmp, &bc->opts.temp_allocator, "temp");
	ufbxi_init_ator(&bc->error, &bc->ator_result, &bc->opts.result_allocator, "result");

	bc->result.unordered = true;
	bc->result.ator = &bc->ator_result;

	bc->tmp.unordered = true;
	bc->tmp.ator = &bc->ator_tmp;

	bc->tmp_prop.ator = &bc->ator_tmp;
	bc->tmp_prop.unordered = true;
	bc->tmp_prop.clearable = true;

	bc->tmp_times.ator = &bc->ator_tmp;
	bc->tmp_bake_props.ator = &bc->ator_tmp;
	bc->tmp_nodes.ator = &bc->ator_tmp;
	bc->tmp_elements.ator = &bc->ator_tmp;
	bc->tmp_props.ator = &bc->ator_tmp;
	bc->tmp_bake_stack.ator = &bc->ator_tmp;

	bc->anim = anim;
	if (anim->time_begin < anim->time_end) {
		bc->time_begin = anim->time_begin;
		bc->time_end = anim->time_end;
	}
	bc->time_min = UFBX_INFINITY;
	bc->time_max = -UFBX_INFINITY;

	bc->imp = ufbxi_push(&bc->result, ufbxi_baked_anim_imp, 1);
	ufbxi_check_err(&bc->error, bc->imp);

	ufbxi_check_err(&bc->error, ufbxi_bake_anim(bc));

	ufbxi_init_ref(&bc->imp->refcount, UFBXI_BAKED_ANIM_IMP_MAGIC, NULL);

	bc->bake.metadata.result_memory_used = bc->ator_result.current_size;
	bc->bake.metadata.temp_memory_used = bc->ator_tmp.current_size;
	bc->bake.metadata.result_allocs = bc->ator_result.num_allocs;
	bc->bake.metadata.temp_allocs = bc->ator_tmp.num_allocs;

	bc->imp->magic = UFBXI_BAKED_ANIM_IMP_MAGIC;
	bc->imp->bake = bc->bake;
	bc->imp->refcount.ator = bc->ator_result;
	bc->imp->refcount.buf = bc->result;

	return 1;
}

#endif

// -- NURBS

static ufbxi_forceinline ufbx_real ufbxi_nurbs_weight(const ufbx_real_list *knots, size_t knot, size_t degree, ufbx_real u)
{
	if (knot >= knots->count) return 0.0f;
	if (knots->count - knot < degree) return 0.0f;
	ufbx_real prev_u = knots->data[knot], next_u = knots->data[knot + degree];
	if (prev_u >= next_u) return 0.0f;
	if (u <= prev_u) return 0.0f;
	if (u >= next_u) return 1.0f;
	return (u - prev_u) / (next_u - prev_u);
}

static ufbxi_forceinline ufbx_real ufbxi_nurbs_deriv(const ufbx_real_list *knots, size_t knot, size_t degree)
{
	if (knot >= knots->count) return 0.0f;
	if (knots->count - knot < degree) return 0.0f;
	ufbx_real prev_u = knots->data[knot], next_u = knots->data[knot + degree];
	if (prev_u >= next_u) return 0.0f;
	return (ufbx_real)degree / (next_u - prev_u);
}

typedef struct {
	ufbxi_refcount refcount;
	ufbx_line_curve curve;
	uint32_t magic;
} ufbxi_line_curve_imp;

ufbx_static_assert(line_curve_imp_offset, offsetof(ufbxi_line_curve_imp, curve) == sizeof(ufbxi_refcount));

#if UFBXI_FEATURE_TESSELLATION

typedef struct {
	ufbx_error error;

	ufbx_tessellate_curve_opts opts;

	const ufbx_nurbs_curve *curve;

	ufbxi_allocator ator_tmp;
	ufbxi_allocator ator_result;

	ufbxi_buf result;

	ufbx_line_curve line;

	ufbxi_line_curve_imp *imp;

} ufbxi_tessellate_curve_context;

typedef struct {
	ufbx_error error;

	ufbx_tessellate_surface_opts opts;

	const ufbx_nurbs_surface *surface;

	ufbxi_allocator ator_tmp;
	ufbxi_allocator ator_result;

	ufbxi_buf tmp;
	ufbxi_buf result;

	ufbxi_map position_map;

	ufbx_mesh mesh;

	ufbxi_mesh_imp *imp;

} ufbxi_tessellate_surface_context;

ufbxi_nodiscard static ufbxi_noinline int ufbxi_tessellate_nurbs_curve_imp(ufbxi_tessellate_curve_context *tc)
{
	if (tc->opts.span_subdivision <= 0) {
		tc->opts.span_subdivision = 4;
	}
	size_t num_sub = tc->opts.span_subdivision;

	const ufbx_nurbs_curve *curve = tc->curve;
	ufbx_line_curve *line = &tc->line;
	ufbxi_check_err_msg(&tc->error, curve->basis.valid && curve->control_points.count > 0, "Bad NURBS geometry");

	ufbxi_init_ator(&tc->error, &tc->ator_tmp, &tc->opts.temp_allocator, "temp");
	ufbxi_init_ator(&tc->error, &tc->ator_result, &tc->opts.result_allocator, "result");

	tc->result.unordered = true;
	tc->result.ator = &tc->ator_result;

	size_t num_spans = curve->basis.spans.count;

	// Check conservatively that we don't overflow anything
	{
		size_t over_spans = num_spans * 2 * sizeof(ufbx_real);
		size_t over = over_spans * num_sub;
		ufbxi_check_err(&tc->error, !ufbxi_does_overflow(over, over_spans, num_sub));
	}

	bool is_open = curve->basis.topology == UFBX_NURBS_TOPOLOGY_OPEN;

	size_t num_indices = num_spans + (num_spans - 1) * (num_sub - 1);
	size_t num_vertices = num_indices - (is_open ? 0u : 1u);
	ufbxi_check_err(&tc->error, num_indices <= INT32_MAX);

	uint32_t *indices = ufbxi_push(&tc->result, uint32_t, num_indices);
	ufbx_vec3 *vertices = ufbxi_push(&tc->result, ufbx_vec3, num_vertices);
	ufbx_line_segment *segments = ufbxi_push(&tc->result, ufbx_line_segment, 1);
	ufbxi_check_err(&tc->error, indices && vertices && segments);

	for (size_t span_ix = 0; span_ix < num_spans; span_ix++) {
		size_t num_splits = span_ix + 1 == num_spans ? 1 : num_sub;

		for (size_t sub_ix = 0; sub_ix < num_splits; sub_ix++) {
			size_t ix = span_ix * num_sub + sub_ix;

			if (ix < num_vertices) {
				ufbx_real u = curve->basis.spans.data[span_ix];
				if (sub_ix > 0) {
					ufbx_real t = (ufbx_real)sub_ix / (ufbx_real)num_sub;
					u = u * (1.0f - t) + t * curve->basis.spans.data[span_ix + 1];
				}

				ufbx_curve_point point = ufbx_evaluate_nurbs_curve(curve, u);
				vertices[ix] = point.position;
				indices[ix] = (uint32_t)ix;
			} else {
				indices[ix] = 0;
			}
		}
	}

	segments[0].index_begin = 0;
	segments[0].num_indices = (uint32_t)num_indices;

	line->element.name.data = ufbxi_empty_char;
	line->element.type = UFBX_ELEMENT_LINE_CURVE;
	line->element.typed_id = UINT32_MAX;
	line->element.element_id = UINT32_MAX;

	line->color.x = 1.0f;
	line->color.y = 1.0f;
	line->color.z = 1.0f;

	line->control_points.data = vertices;
	line->control_points.count = num_vertices;
	line->point_indices.data = indices;
	line->point_indices.count = num_indices;
	line->segments.data = segments;
	line->segments.count = 1;

	line->from_tessellated_nurbs = true;

	tc->imp = ufbxi_push(&tc->result, ufbxi_line_curve_imp, 1);
	ufbxi_check_err(&tc->error, tc->imp);

	ufbxi_init_ref(&tc->imp->refcount, UFBXI_LINE_CURVE_IMP_MAGIC, &(ufbxi_get_imp(ufbxi_scene_imp, curve->element.scene))->refcount);

	tc->imp->magic = UFBXI_LINE_CURVE_IMP_MAGIC;
	tc->imp->curve = tc->line;
	tc->imp->refcount.ator = tc->ator_result;
	tc->imp->refcount.buf = tc->result;

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_tessellate_nurbs_surface_imp(ufbxi_tessellate_surface_context *tc)
{
	if (tc->opts.span_subdivision_u <= 0) {
		tc->opts.span_subdivision_u = 4;
	}
	if (tc->opts.span_subdivision_v <= 0) {
		tc->opts.span_subdivision_v = 4;
	}

	size_t sub_u = tc->opts.span_subdivision_u;
	size_t sub_v = tc->opts.span_subdivision_v;

	const ufbx_nurbs_surface *surface = tc->surface;
	ufbx_mesh *mesh = &tc->mesh;
	ufbxi_check_err_msg(&tc->error, surface->basis_u.valid && surface->basis_v.valid
		&& surface->num_control_points_u > 0 && surface->num_control_points_v > 0, "Bad NURBS geometry");

	ufbxi_init_ator(&tc->error, &tc->ator_tmp, &tc->opts.temp_allocator, "temp");
	ufbxi_init_ator(&tc->error, &tc->ator_result, &tc->opts.result_allocator, "result");

	tc->result.unordered = true;
	tc->tmp.unordered = true;

	tc->result.ator = &tc->ator_result;
	tc->tmp.ator = &tc->ator_tmp;

	bool open_u = surface->basis_u.topology == UFBX_NURBS_TOPOLOGY_OPEN;
	bool open_v = surface->basis_v.topology == UFBX_NURBS_TOPOLOGY_OPEN;

	size_t spans_u = surface->basis_u.spans.count;
	size_t spans_v = surface->basis_v.spans.count;

	// Check conservatively that we don't overflow anything
	{
		size_t over_spans_u = spans_u * 2 * sizeof(ufbx_real);
		size_t over_spans_v = spans_v * 2 * sizeof(ufbx_real);
		size_t over_u = over_spans_u * sub_u;
		size_t over_v = over_spans_v * sub_v;
		size_t over_uv = over_u * over_v;
		ufbxi_check_err(&tc->error, !ufbxi_does_overflow(over_u, over_spans_u, sub_u));
		ufbxi_check_err(&tc->error, !ufbxi_does_overflow(over_v, over_spans_v, sub_v));
		ufbxi_check_err(&tc->error, !ufbxi_does_overflow(over_uv, over_u, over_v));
	}

	size_t faces_u = (spans_u - 1) * sub_u;
	size_t faces_v = (spans_v - 1) * sub_v;

	size_t indices_u = spans_u + (spans_u - 1) * (sub_u - 1);
	size_t indices_v = spans_v + (spans_v - 1) * (sub_v - 1);

	size_t num_faces = faces_u * faces_v;
	size_t num_indices = indices_u * indices_v;
	ufbxi_check_err(&tc->error, num_indices <= INT32_MAX);

	uint32_t *position_ix = ufbxi_push(&tc->tmp, uint32_t, num_indices);
	ufbx_vec3 *positions = ufbxi_push(&tc->result, ufbx_vec3, num_indices + 1);
	ufbx_vec3 *normals = ufbxi_push(&tc->result, ufbx_vec3, num_indices + 1);
	ufbx_vec2 *uvs = ufbxi_push(&tc->result, ufbx_vec2, num_indices + 1);
	ufbx_vec3 *tangents = ufbxi_push(&tc->result, ufbx_vec3, num_indices + 1);
	ufbx_vec3 *bitangents = ufbxi_push(&tc->result, ufbx_vec3, num_indices + 1);
	ufbxi_check_err(&tc->error, position_ix && uvs && tangents && bitangents);

	*positions++ = ufbx_zero_vec3;
	*normals++ = ufbx_zero_vec3;
	*uvs++ = ufbx_zero_vec2;
	*tangents++ = ufbx_zero_vec3;
	*bitangents++ = ufbx_zero_vec3;

	uint32_t num_positions = 0;

	for (size_t span_v = 0; span_v < spans_v; span_v++) {
		size_t splits_v = span_v + 1 == spans_v ? 1 : sub_v;

		for (size_t split_v = 0; split_v < splits_v; split_v++) {
			size_t ix_v = span_v * sub_v + split_v;
			ufbx_assert(ix_v < indices_v);

			ufbx_real v = surface->basis_v.spans.data[span_v];
			if (split_v > 0) {
				ufbx_real t = (ufbx_real)split_v / (ufbx_real)splits_v;
				v = v * (1.0f - t) + t * surface->basis_v.spans.data[span_v + 1];
			}
			ufbx_real original_v = v;
			if (span_v + 1 == spans_v && !open_v) {
				v = surface->basis_v.spans.data[0];
			}

			for (size_t span_u = 0; span_u < spans_u; span_u++) {
				size_t splits_u = span_u + 1 == spans_u ? 1 : sub_u;
				for (size_t split_u = 0; split_u < splits_u; split_u++) {
					size_t ix_u = span_u * sub_u + split_u;
					ufbx_assert(ix_u < indices_u);

					ufbx_real u = surface->basis_u.spans.data[span_u];
					if (split_u > 0) {
						ufbx_real t = (ufbx_real)split_u / (ufbx_real)splits_u;
						u = u * (1.0f - t) + t * surface->basis_u.spans.data[span_u + 1];
					}
					ufbx_real original_u = u;
					if (span_u + 1 == spans_u && !open_u) {
						u = surface->basis_u.spans.data[0];
					}

					ufbx_surface_point point = ufbx_evaluate_nurbs_surface(surface, u, v);
					ufbx_vec3 pos = point.position;

					ufbx_vec3 tangent_u = ufbxi_slow_normalize3(&point.derivative_u);
					ufbx_vec3 tangent_v = ufbxi_slow_normalize3(&point.derivative_v);

					// Check if there's any wrapped positions that we could match
					size_t neighbors[5]; // ufbxi_uninit
					size_t num_neighbors = 0;

					if ((span_v == 0 && (span_u > 0 || split_u > 0)) || (span_u == 0 && (span_v > 0 || split_v > 0))) {
						// Top/left
						neighbors[num_neighbors++] = 0;
					}
					if (span_v + 1 == spans_v) {
						// Bottom
						neighbors[num_neighbors++] = ix_u;
						if (span_u > 0 || split_u > 0) {
							neighbors[num_neighbors++] = ix_v * indices_u;
						}
					}
					if (span_u + 1 == spans_u) {
						// Right
						neighbors[num_neighbors++] = ix_v * indices_u;
						if (span_v > 0 || split_v > 0) {
							neighbors[num_neighbors++] = indices_u - 1;
						}
					}

					size_t ix = ix_v * indices_u + ix_u;

					uint32_t pos_ix = num_positions;
					for (size_t i = 0; i < num_neighbors; i++) {
						size_t nb_ix = neighbors[i];
						ufbx_assert(nb_ix < ix);
						uint32_t nb_pos_ix = position_ix[nb_ix];
						ufbx_vec3 nb_pos = positions[nb_pos_ix];
						ufbx_real dx = nb_pos.x - pos.x;
						ufbx_real dy = nb_pos.y - pos.y;
						ufbx_real dz = nb_pos.z - pos.z;
						ufbx_real delta = dx*dx + dy*dy + dz*dz;
						if (delta < 0.0000001f) { // TODO: Configurable / something more rigorous
							pos_ix = nb_pos_ix;
							break;
						}
					}

					position_ix[ix] = pos_ix;
					if (pos_ix == num_positions) {
						positions[pos_ix] = pos;
						num_positions = pos_ix + 1;
					}
					uvs[ix].x = original_u;
					uvs[ix].y = original_v;
					tangents[ix] = tangent_u;
					bitangents[ix] = tangent_v;
				}
			}
		}
	}

	ufbx_face *faces = ufbxi_push(&tc->result, ufbx_face, num_faces);
	uint32_t *vertex_ix = ufbxi_push(&tc->result, uint32_t, num_faces * 4);
	uint32_t *attrib_ix = ufbxi_push(&tc->result, uint32_t, num_faces * 4);
	ufbxi_check_err(&tc->error, faces && vertex_ix && attrib_ix);

	size_t face_ix = 0;
	size_t dst_index = 0;

	size_t num_triangles = 0;

	for (size_t face_v = 0; face_v < faces_v; face_v++) {
		for (size_t face_u = 0; face_u < faces_u; face_u++) {

			attrib_ix[dst_index + 0] = (uint32_t)((face_v + 0) * indices_u + (face_u + 0));
			attrib_ix[dst_index + 1] = (uint32_t)((face_v + 0) * indices_u + (face_u + 1));
			attrib_ix[dst_index + 2] = (uint32_t)((face_v + 1) * indices_u + (face_u + 1));
			attrib_ix[dst_index + 3] = (uint32_t)((face_v + 1) * indices_u + (face_u + 0));

			vertex_ix[dst_index + 0] = position_ix[attrib_ix[dst_index + 0]];
			vertex_ix[dst_index + 1] = position_ix[attrib_ix[dst_index + 1]];
			vertex_ix[dst_index + 2] = position_ix[attrib_ix[dst_index + 2]];
			vertex_ix[dst_index + 3] = position_ix[attrib_ix[dst_index + 3]];

			bool is_triangle = false;
			for (size_t prev_ix = 0; prev_ix < 4; prev_ix++) {
				size_t next_ix = (prev_ix + 1) % 4;
				if (vertex_ix[dst_index + prev_ix] == vertex_ix[dst_index + next_ix]) {
					for (size_t i = next_ix; i < 3; i++) {
						attrib_ix[dst_index + i] = attrib_ix[dst_index + i + 1];
						vertex_ix[dst_index + i] = vertex_ix[dst_index + i + 1];
					}
					is_triangle = true;
					break;
				}
			}

			faces[face_ix].index_begin = (uint32_t)dst_index;
			faces[face_ix].num_indices = is_triangle ? 3 : 4;
			dst_index += is_triangle ? 3 : 4;
			num_triangles += is_triangle ? 1 : 2;
			face_ix++;
		}
	}

	ufbxi_check_err(&tc->error, positions && normals);

	mesh->element.name.data = ufbxi_empty_char;
	mesh->element.type = UFBX_ELEMENT_MESH;
	mesh->element.typed_id = UINT32_MAX;
	mesh->element.element_id = UINT32_MAX;

	mesh->vertices.data = positions;
	mesh->vertices.count = num_positions;
	mesh->num_vertices = num_positions;
	mesh->vertex_indices.data = vertex_ix;
	mesh->vertex_indices.count = dst_index;

	mesh->faces.data = faces;
	mesh->faces.count = num_faces;

	mesh->vertex_position.exists = true;
	mesh->vertex_position.values.data = positions;
	mesh->vertex_position.values.count = num_positions;
	mesh->vertex_position.indices.data = vertex_ix;
	mesh->vertex_position.indices.count = dst_index;
	mesh->vertex_position.unique_per_vertex = true;

	mesh->vertex_uv.exists = true;
	mesh->vertex_uv.values.data = uvs;
	mesh->vertex_uv.values.count = dst_index;
	mesh->vertex_uv.indices.data = attrib_ix;
	mesh->vertex_uv.indices.count = dst_index;

	mesh->vertex_normal.exists = true;
	mesh->vertex_normal.values.data = normals;
	mesh->vertex_normal.values.count = num_positions;
	mesh->vertex_normal.indices.data = vertex_ix;
	mesh->vertex_normal.indices.count = dst_index;

	mesh->vertex_tangent.exists = true;
	mesh->vertex_tangent.values.data = tangents;
	mesh->vertex_tangent.values.count = dst_index;
	mesh->vertex_tangent.indices.data = attrib_ix;
	mesh->vertex_tangent.indices.count = dst_index;

	mesh->vertex_bitangent.exists = true;
	mesh->vertex_bitangent.values.data = bitangents;
	mesh->vertex_bitangent.values.count = dst_index;
	mesh->vertex_bitangent.indices.data = attrib_ix;
	mesh->vertex_bitangent.indices.count = dst_index;

	mesh->num_faces = num_faces;
	mesh->num_triangles = num_triangles;
	mesh->num_indices = dst_index;
	mesh->max_face_triangles = 2;

	if (surface->material) {
		mesh->face_material.data = ufbxi_push_zero(&tc->result, uint32_t, num_faces);
		ufbxi_check_err(&tc->error, mesh->face_material.data);

		ufbx_material **mat = ufbxi_push_zero(&tc->result, ufbx_material*, 1);
		ufbxi_check_err(&tc->error, mat);

		*mat = surface->material;
		mesh->materials.data = mat;
		mesh->materials.count = 1;
	}

	if (!tc->opts.skip_mesh_parts) {
		mesh->material_parts.count = 1;
		mesh->material_parts.data = ufbxi_push_zero(&tc->result, ufbx_mesh_part, 1);
		ufbxi_check_err(&tc->error, mesh->material_parts.data);
	}

	ufbxi_check_err(&tc->error, ufbxi_finalize_mesh_material(&tc->result, &tc->error, mesh));
	ufbxi_check_err(&tc->error, ufbxi_finalize_mesh(&tc->result, &tc->error, mesh));

	mesh->generated_normals = true;
	ufbx_compute_normals(mesh, &mesh->vertex_position,
		mesh->vertex_normal.indices.data, mesh->vertex_normal.indices.count,
		mesh->vertex_normal.values.data, mesh->vertex_normal.values.count);

	if (surface->flip_normals) {
		ufbxi_nounroll ufbxi_for_list(ufbx_vec3, normal, mesh->vertex_normal.values) {
			normal->x *= -1.0f;
			normal->y *= -1.0f;
			normal->z *= -1.0f;
		}
	}

	tc->imp = ufbxi_push(&tc->result, ufbxi_mesh_imp, 1);
	ufbxi_check_err(&tc->error, tc->imp);

	ufbxi_init_ref(&tc->imp->refcount, UFBXI_MESH_IMP_MAGIC, &(ufbxi_get_imp(ufbxi_scene_imp, surface->element.scene))->refcount);

	tc->imp->magic = UFBXI_MESH_IMP_MAGIC;
	tc->imp->mesh = tc->mesh;
	tc->imp->refcount.ator = tc->ator_result;
	tc->imp->refcount.buf = tc->result;
	tc->imp->mesh.subdivision_evaluated = true;

	return 1;
}

#endif

// -- Topology

#if UFBXI_FEATURE_KD

typedef struct {
	ufbx_real split;
	uint32_t index_plus_one; // 0 for empty
	uint32_t slow_left;
	uint32_t slow_right;
	uint32_t slow_end;
} ufbxi_kd_node;

typedef struct {
	ufbx_face face;
	ufbx_vertex_vec3 positions;
	ufbx_vec3 axes[3];
	ufbxi_kd_node kd_nodes[1 << (UFBXI_KD_FAST_DEPTH + 1)];
	uint32_t *kd_indices;

	// Temporary
	ufbx_vec3 cur_axis_dir;
	ufbx_face cur_face;
} ufbxi_ngon_context;

typedef struct {
	ufbx_real min_t[2];
	ufbx_real max_t[2];
	ufbx_vec2 points[3];
	uint32_t indices[3];
} ufbxi_kd_triangle;

ufbxi_noinline static ufbx_vec2 ufbxi_ngon_project(ufbxi_ngon_context *nc, uint32_t index)
{
	ufbx_vec3 point = nc->positions.values.data[nc->positions.indices.data[nc->face.index_begin + index]];

	ufbx_vec2 p;
	p.x = ufbxi_dot3(nc->axes[0], point);
	p.y = ufbxi_dot3(nc->axes[1], point);
	return p;
}

ufbxi_forceinline static ufbx_real ufbxi_orient2d(ufbx_vec2 a, ufbx_vec2 b, ufbx_vec2 c)
{
	return (b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x);
}

ufbxi_noinline static bool ufbxi_kd_check_point(ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t index)
{
	if (index == tri->indices[0] || index == tri->indices[1] || index == tri->indices[2]) return false;
	ufbx_vec2 p = ufbxi_ngon_project(nc, index);

	ufbx_real u = ufbxi_orient2d(p, tri->points[0], tri->points[1]);
	ufbx_real v = ufbxi_orient2d(p, tri->points[1], tri->points[2]);
	ufbx_real w = ufbxi_orient2d(p, tri->points[2], tri->points[0]);

	if (u <= 0.0f && v <= 0.0f && w <= 0.0f) return true;
	if (u >= 0.0f && v >= 0.0f && w >= 0.0f) return true;
	return false;
}

// Recursion limited by 32-bit indices in input, minus halvings from `ufbxi_kd_check_fast()`
ufbxi_noinline static bool ufbxi_kd_check_slow(ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t begin, uint32_t count, uint32_t axis)
	ufbxi_recursive_function(bool, ufbxi_kd_check_slow, (nc, tri, begin, count, axis), 32 - UFBXI_KD_FAST_DEPTH,
		(ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t begin, uint32_t count, uint32_t axis))
{
	ufbx_vertex_vec3 pos = nc->positions;
	uint32_t *kd_indices = nc->kd_indices;

	while (count > 0) {
		uint32_t num_left = count / 2;
		uint32_t begin_right = begin + num_left + 1;
		uint32_t num_right = count - (num_left + 1);

		uint32_t index = kd_indices[begin + num_left];
		ufbx_vec3 point = pos.values.data[pos.indices.data[nc->face.index_begin + index]];
		ufbx_real split = ufbxi_dot3(point, nc->axes[axis]);
		bool hit_left = tri->min_t[axis] <= split;
		bool hit_right = tri->max_t[axis] >= split;

		if (hit_left && hit_right) {
			if (ufbxi_kd_check_point(nc, tri, index)) {
				return true;
			}

			if (ufbxi_kd_check_slow(nc, tri, begin_right, num_right, axis ^ 1)) {
				return true;
			}
		}

		axis ^= 1;
		if (hit_left) {
			count = num_left;
		} else {
			begin = begin_right;
			count = num_right;
		}
	}

	return false;
}

// Recursion limited by `UFBXI_KD_FAST_DEPTH`
ufbxi_noinline static bool ufbxi_kd_check_fast(ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t kd_index, uint32_t axis, uint32_t depth)
	ufbxi_recursive_function(bool, ufbxi_kd_check_fast, (nc, tri, kd_index, axis, depth), UFBXI_KD_FAST_DEPTH,
		(ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t kd_index, uint32_t axis, uint32_t depth))
{
	for (;;) {
		ufbxi_kd_node node = nc->kd_nodes[kd_index];
		if (node.index_plus_one == 0) return false;

		bool hit_left = tri->min_t[axis] <= node.split;
		bool hit_right = tri->max_t[axis] >= node.split;

		uint32_t side = hit_left ? 0 : 1;
		uint32_t child_kd_index = kd_index * 2 + 1 + side;
		if (hit_left && hit_right) {

			// Check for the point on the split plane
			uint32_t index = node.index_plus_one - 1;
			if (ufbxi_kd_check_point(nc, tri, index)) {
				return true;
			}

			// Recurse always to the right if we hit both sides
			if (depth + 1 == UFBXI_KD_FAST_DEPTH) {
				if (ufbxi_kd_check_slow(nc, tri, node.slow_right, node.slow_end - node.slow_right, axis ^ 1)) {
					return true;
				}
			} else {
				if (ufbxi_kd_check_fast(nc, tri, child_kd_index + 1, axis ^ 1, depth + 1)) {
					return true;
				}
			}
		}

		depth++;
		axis ^= 1;
		kd_index = child_kd_index;

		if (depth == UFBXI_KD_FAST_DEPTH) {
			if (hit_left) {
				return ufbxi_kd_check_slow(nc, tri, node.slow_left, node.slow_right - node.slow_left, axis);
			} else {
				return ufbxi_kd_check_slow(nc, tri, node.slow_right, node.slow_end - node.slow_right, axis);
			}
		}
	}
}

ufbxi_noinline static bool ufbxi_kd_check(ufbxi_ngon_context *nc, const ufbx_vec2 *points, const uint32_t *indices)
{
	ufbxi_kd_triangle tri; // ufbxi_uninit
	tri.points[0] = points[0];
	tri.points[1] = points[1];
	tri.points[2] = points[2];
	tri.indices[0] = indices[0];
	tri.indices[1] = indices[1];
	tri.indices[2] = indices[2];
	tri.min_t[0] = ufbxi_min_real(ufbxi_min_real(points[0].x, points[1].x), points[2].x);
	tri.min_t[1] = ufbxi_min_real(ufbxi_min_real(points[0].y, points[1].y), points[2].y);
	tri.max_t[0] = ufbxi_max_real(ufbxi_max_real(points[0].x, points[1].x), points[2].x);
	tri.max_t[1] = ufbxi_max_real(ufbxi_max_real(points[0].y, points[1].y), points[2].y);
	return ufbxi_kd_check_fast(nc, &tri, 0, 0, 0);
}

ufbxi_noinline static bool ufbxi_kd_index_less(void *user, const void *va, const void *vb)
{
	ufbxi_ngon_context *nc = (ufbxi_ngon_context*)user;
	ufbx_vertex_vec3 *pos = &nc->positions;
	const uint32_t a = *(const uint32_t*)va, b = *(const uint32_t*)vb;
	ufbx_real da = ufbxi_dot3(nc->cur_axis_dir, pos->values.data[pos->indices.data[nc->cur_face.index_begin + a]]);
	ufbx_real db = ufbxi_dot3(nc->cur_axis_dir, pos->values.data[pos->indices.data[nc->cur_face.index_begin + b]]);
	return da < db;
}

// Recursion limited by 32-bit indices in input
ufbxi_noinline static void ufbxi_kd_build(ufbxi_ngon_context *nc, uint32_t *indices, uint32_t *tmp, uint32_t num, uint32_t axis, uint32_t fast_index, uint32_t depth)
	ufbxi_recursive_function_void(ufbxi_kd_build, (nc, indices, tmp, num, axis, fast_index, depth), 32,
		(ufbxi_ngon_context *nc, uint32_t *indices, uint32_t *tmp, uint32_t num, uint32_t axis, uint32_t fast_index, uint32_t depth))
{
	if (num == 0) return;

	ufbx_vertex_vec3 pos = nc->positions;
	ufbx_vec3 axis_dir = nc->axes[axis];
	ufbx_face face = nc->face;

	nc->cur_axis_dir = axis_dir;
	nc->cur_face = face;

	// Sort the remaining indices based on the axis
	ufbxi_stable_sort(sizeof(uint32_t), 16, indices, tmp, num, &ufbxi_kd_index_less, nc);

	uint32_t num_left = num / 2;
	uint32_t begin_right = num_left + 1;
	uint32_t num_right = num - begin_right;
	uint32_t dst_right = num_left + 1;
	if (depth < UFBXI_KD_FAST_DEPTH) {
		uint32_t skip_left = 1u << (UFBXI_KD_FAST_DEPTH - depth - 1);
		dst_right = dst_right > skip_left ? dst_right - skip_left : 0;

		uint32_t index = indices[num_left];
		ufbxi_kd_node *kd = &nc->kd_nodes[fast_index];

		kd->split = ufbxi_dot3(axis_dir, pos.values.data[pos.indices.data[face.index_begin + index]]);
		kd->index_plus_one = index + 1;

		if (depth + 1 == UFBXI_KD_FAST_DEPTH) {
			kd->slow_left = (uint32_t)(indices - nc->kd_indices);
			kd->slow_right = kd->slow_left + num_left;
			kd->slow_end = kd->slow_right + num_right;
		} else {
			kd->slow_left = UINT32_MAX;
			kd->slow_right = UINT32_MAX;
			kd->slow_end = UINT32_MAX;
		}
	}

	uint32_t child_fast = fast_index * 2 + 1;
	ufbxi_kd_build(nc, indices, tmp, num_left, axis ^ 1, child_fast + 0, depth + 1);

	if (dst_right != begin_right) {
		memmove(indices + dst_right, indices + begin_right, num_right * sizeof(uint32_t));
	}

	ufbxi_kd_build(nc, indices + dst_right, tmp, num_right, axis ^ 1, child_fast + 1, depth + 1);
}

#endif

#if UFBXI_FEATURE_TRIANGULATION

ufbxi_noinline static ufbx_real ufbxi_ngon_tri_weight(const ufbx_vec2 *points)
{
	ufbx_vec2 p0 = points[0], p1 = points[1], p2 = points[2];
	ufbx_real orient = ufbxi_orient2d(p0, p1, p2);
	if (orient <= 0.0f) return -1.0f;

	ufbx_real a = ufbxi_distsq2(p0, p1);
	ufbx_real b = ufbxi_distsq2(p1, p2);
	ufbx_real c = ufbxi_distsq2(p2, p0);
	ufbx_real ab = (a + b - c) / (ufbx_real)ufbx_sqrt(4.0f * a * b);
	ufbx_real bc = (b + c - a) / (ufbx_real)ufbx_sqrt(4.0f * b * c);
	ufbx_real ca = (c + a - b) / (ufbx_real)ufbx_sqrt(4.0f * c * a);
	return (ufbx_real)ufbx_fmax(UFBX_EPSILON, 2.0f - ufbx_fmax(ufbx_fmax(ab, bc), ca));
}

ufbxi_noinline static uint32_t ufbxi_triangulate_ngon(ufbxi_ngon_context *nc, uint32_t *indices, uint32_t num_indices)
{
	ufbx_face face = nc->face;
	ufbx_assert(face.num_indices > 4);

	// Form an orthonormal basis to project the polygon into a 2D plane
	ufbx_vec3 normal = ufbx_get_weighted_face_normal(&nc->positions, face);
	ufbx_real len = ufbxi_length3(normal);
	if (len > UFBX_EPSILON) {
		normal = ufbxi_mul3(normal, 1.0f / len);
	} else {
		normal.x = 1.0f;
		normal.y = 0.0f;
		normal.z = 0.0f;
	}

	ufbx_vec3 axis; // ufbxi_uninit
	if (normal.x*normal.x < 0.5f) {
		axis.x = 1.0f;
		axis.y = 0.0f;
		axis.z = 0.0f;
	} else {
		axis.x = 0.0f;
		axis.y = 1.0f;
		axis.z = 0.0f;
	}
	nc->axes[0] = ufbxi_slow_normalized_cross3(&axis, &normal);
	nc->axes[1] = ufbxi_slow_normalized_cross3(&normal, &nc->axes[0]);
	nc->axes[2] = normal;

	uint32_t *kd_indices = indices;
	nc->kd_indices = kd_indices;

	uint32_t *kd_tmp = indices + face.num_indices;

	// Collect all the reflex corners for intersection testing.
	uint32_t num_kd_indices = 0;
	{
		ufbx_vec2 a = ufbxi_ngon_project(nc, face.num_indices - 1);
		ufbx_vec2 b = ufbxi_ngon_project(nc, 0);
		for (uint32_t i = 0; i < face.num_indices; i++) {
			uint32_t next = i + 1 < face.num_indices ? i + 1 : 0;
			ufbx_vec2 c = ufbxi_ngon_project(nc, next);

			if (ufbxi_orient2d(a, b, c) <= 0.0f) {
				kd_indices[num_kd_indices++] = i;
			}

			a = b;
			b = c;
		}
	}

	// Build a KD-tree of the vertices.
	uint32_t num_skip_indices = (1u << (UFBXI_KD_FAST_DEPTH + 1)) - 1;
	uint32_t kd_slow_indices = num_kd_indices > num_skip_indices ? num_kd_indices - num_skip_indices : 0;
	ufbxi_ignore(kd_slow_indices);
	ufbx_assert(kd_slow_indices + face.num_indices * 2 <= num_indices);
	ufbxi_kd_build(nc, kd_indices, kd_tmp, num_kd_indices, 0, 0, 0);

	uint32_t *edges = indices + num_indices - face.num_indices * 2;

	// Initialize `edges` to be a connectivity structure where:
	//  `edges[2*i + 0]` is the previous vertex of `i`
	//  `edges[2*i + 1]` is the next vertex of `i`
	// When clipped we mark indices with the high bit (0x80000000)
	for (uint32_t i = 0; i < face.num_indices; i++) {
		edges[i*2 + 0] = i > 0 ? i - 1 : face.num_indices - 1;
		edges[i*2 + 1] = i + 1 < face.num_indices ? i + 1 : 0;
	}

	// Core of the ear clipping algorithm.
	// Iterate through the polygon corners looking for potential ears satisfying:
	//   - Angle must be less than 180deg
	//   - The triangle formed by the two edges must be contained within the polygon
	// As these properties change only locally between modifications we only need
	// to iterate the polygon once if we move backwards one step every time we clip an ear.
	uint32_t indices_left = face.num_indices;
	{
		uint32_t point_indices[4] = { 0, 1, 2, 3 };
		ufbx_real weights[2]; // ufbxi_uninit
		ufbx_vec2 points[4]; // ufbxi_uninit

		uint32_t num_steps = 0;
		while (indices_left > 3) {
			points[0] = ufbxi_ngon_project(nc, point_indices[0]);
			points[1] = ufbxi_ngon_project(nc, point_indices[1]);
			points[2] = ufbxi_ngon_project(nc, point_indices[2]);
			points[3] = ufbxi_ngon_project(nc, point_indices[3]);

			weights[0] = ufbxi_ngon_tri_weight(points + 0);
			weights[1] = ufbxi_ngon_tri_weight(points + 1);

			uint32_t first_side = weights[1] > weights[0] ? 1 : 0;
			bool clipped = false;
			ufbxi_nounroll for (uint32_t side_ix = 0; side_ix < 2; side_ix++) {
				uint32_t side = side_ix ^ first_side;
				if (!(weights[side] >= 0.0f)) break;

				// If there is no reflex angle contained within the triangle formed
				// by `{ a, b, c }` connect the vertices `a - c` (prev, next) directly.
				if (!ufbxi_kd_check(nc, points + side, point_indices + side)) {
					uint32_t ia = point_indices[side + 0];
					uint32_t ib = point_indices[side + 1];
					uint32_t ic = point_indices[side + 2];

					// Mark as clipped
					edges[ib*2 + 0] |= 0x80000000;
					edges[ib*2 + 1] |= 0x80000000;

					edges[ic*2 + 0] = ia;
					edges[ia*2 + 1] = ic;

					indices_left -= 1;

					// TODO: This may cause O(n^2) behavior!
					num_steps = 0;

					if (side == 1) {
						point_indices[2] = point_indices[3];
						point_indices[3] = edges[point_indices[3]*2 + 1];
					} else {
						point_indices[1] = point_indices[0];
						point_indices[0] = edges[point_indices[0]*2 + 0];
					}

					clipped = true;
					break;
				}
			}
			if (clipped) continue;

			// Continue forward
			point_indices[0] = point_indices[1];
			point_indices[1] = point_indices[2];
			point_indices[2] = point_indices[3];
			point_indices[3] = edges[point_indices[3]*2 + 1];
			num_steps++;

			// If we have walked around the entire polygon it is irregular and
			// ear cutting won't find any more triangles.
			// TODO: This could be stricter?
			if (num_steps >= face.num_indices*2) break;
		}

		// Fallback: Cut non-ears until the polygon is completed.
		// TODO: Could do something better here..
		uint32_t ix = point_indices[1];
		while (indices_left > 3) {
			uint32_t prev = edges[ix*2 + 0];
			uint32_t next = edges[ix*2 + 1];

			// Mark as clipped
			edges[ix*2 + 0] |= 0x80000000;
			edges[ix*2 + 1] |= 0x80000000;

			edges[prev*2 + 1] = next;
			edges[next*2 + 0] = prev;

			indices_left -= 1;
			ix = next;
		}

		// Now we have a single triangle left at `ix`.
		edges[ix*2 + 0] |= 0x80000000;
		edges[ix*2 + 1] |= 0x80000000;
	}

	// Expand the adjacency information `edges` into proper triangles.
	// Care needs to be taken here as both refer to the same memory area:
	// The last 4 triangles may overlap in source and destination so we write
	// them to a stack buffer and copy them over in the end.
	uint32_t max_triangles = face.num_indices - 2;
	uint32_t num_triangles = 0, num_last_triangles = 0;
	uint32_t last_triangles[4*3]; // ufbxi_uninit

	uint32_t index_begin = face.index_begin;
	for (uint32_t ix = 0; ix < face.num_indices; ix++) {
		uint32_t prev = edges[ix*2 + 0];
		uint32_t next = edges[ix*2 + 1];
		if (!(prev & 0x80000000)) continue;

		uint32_t *dst = indices + num_triangles * 3;
		if (num_triangles + 4 >= max_triangles) {
			dst = last_triangles + num_last_triangles * 3;
			num_last_triangles++;
		}

		dst[0] = index_begin + (prev & 0x7fffffff);
		dst[1] = index_begin + ix;
		dst[2] = index_begin + (next & 0x7fffffff);
		num_triangles++;
	}

	// Copy over the last triangles
	ufbx_assert(num_triangles == max_triangles);
	memcpy(indices + (max_triangles - num_last_triangles) * 3, last_triangles, num_last_triangles * 3 * sizeof(uint32_t));

	return num_triangles;
}

#endif

static bool ufbxi_topo_less_index_prev_next(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_topo_edge *a = (const ufbx_topo_edge*)va, *b = (const ufbx_topo_edge*)vb;
	if ((int32_t)a->prev != (int32_t)b->prev) return (int32_t)a->prev < (int32_t)b->prev;
	return (int32_t)a->next < (int32_t)b->next;
}

static bool ufbxi_topo_less_index_index(void *user, const void *va, const void *vb)
{
	(void)user;
	const ufbx_topo_edge *a = (const ufbx_topo_edge*)va, *b = (const ufbx_topo_edge*)vb;
	return (int32_t)a->index < (int32_t)b->index;
}

ufbxi_noinline static void ufbxi_compute_topology(const ufbx_mesh *mesh, ufbx_topo_edge *topo)
{
	size_t num_indices = mesh->num_indices;

	// Temporarily use `prev` and `next` for vertices
	for (uint32_t fi = 0; fi < mesh->num_faces; fi++) {
		ufbx_face face = mesh->faces.data[fi];
		for (uint32_t pi = 0; pi < face.num_indices; pi++) {
			ufbx_topo_edge *te = &topo[face.index_begin + pi];
			uint32_t ni = (pi + 1) % face.num_indices;
			uint32_t va = mesh->vertex_indices.data[face.index_begin + pi];
			uint32_t vb = mesh->vertex_indices.data[face.index_begin + ni];

			if (vb < va) {
				uint32_t vt = va; va = vb; vb = vt;
			}
			te->index = face.index_begin + pi;
			te->twin = UFBX_NO_INDEX;
			te->edge = UFBX_NO_INDEX;
			te->prev = va;
			te->next = vb;
			te->face = fi;
			te->flags = (ufbx_topo_flags)0;
		}
	}

	ufbxi_unstable_sort(topo, num_indices, sizeof(ufbx_topo_edge), &ufbxi_topo_less_index_prev_next, NULL);

	if (mesh->edges.data) {
		for (uint32_t ei = 0; ei < mesh->num_edges; ei++) {
			ufbx_edge edge = mesh->edges.data[ei];
			uint32_t va = mesh->vertex_indices.data[edge.a];
			uint32_t vb = mesh->vertex_indices.data[edge.b];
			if (vb < va) {
				uint32_t vt = va; va = vb; vb = vt;
			}

			size_t ix = num_indices;
			ufbxi_macro_lower_bound_eq(ufbx_topo_edge, 32, &ix, topo, 0, num_indices,
				(a->prev == va ? a->next < vb : a->prev < va), (a->prev == va && a->next == vb));

			for (; ix < num_indices && topo[ix].prev == va && topo[ix].next == vb; ix++) {
				topo[ix].edge = ei;
			}
		}
	}

	// Connect paired edges
	for (size_t i0 = 0; i0 < num_indices; ) {
		size_t i1 = i0;

		uint32_t a = topo[i0].prev, b = topo[i0].next;
		while (i1 + 1 < num_indices && topo[i1 + 1].prev == a && topo[i1 + 1].next == b) i1++;

		if (i1 == i0 + 1) {
			topo[i0].twin = topo[i1].index;
			topo[i1].twin = topo[i0].index;
		} else if (i1 > i0 + 1) {
			for (size_t i = i0; i <= i1; i++) {
				topo[i].flags = (ufbx_topo_flags)(topo[i].flags | UFBX_TOPO_NON_MANIFOLD);
			}
		}

		i0 = i1 + 1;
	}

	ufbxi_unstable_sort(topo, num_indices, sizeof(ufbx_topo_edge), &ufbxi_topo_less_index_index, NULL);

	// Fix `prev` and `next` to the actual index values
	for (uint32_t fi = 0; fi < mesh->num_faces; fi++) {
		ufbx_face face = mesh->faces.data[fi];
		for (uint32_t i = 0; i < face.num_indices; i++) {
			ufbx_topo_edge *to = &topo[face.index_begin + i];
			to->prev = (uint32_t)(face.index_begin + (i + face.num_indices - 1) % face.num_indices);
			to->next = (uint32_t)(face.index_begin + (i + 1) % face.num_indices);
		}
	}
}

static bool ufbxi_is_edge_smooth(const ufbx_mesh *mesh, const ufbx_topo_edge *topo, size_t num_topo, uint32_t index, bool assume_smooth)
{
	ufbxi_ignore(num_topo);
	ufbx_assert((size_t)index < num_topo);
	if (mesh->edge_smoothing.data) {
		uint32_t edge = topo[index].edge;
		if (edge != UFBX_NO_INDEX && mesh->edge_smoothing.data[edge]) return true;
	}

	if (mesh->face_smoothing.data) {
		if (mesh->face_smoothing.data[topo[index].face]) return true;
		uint32_t twin = topo[index].twin;
		if (twin != UFBX_NO_INDEX) {
			if (mesh->face_smoothing.data[topo[twin].face]) return true;
		}
	}

	if (!mesh->edge_smoothing.data && !mesh->face_smoothing.data && mesh->vertex_normal.exists) {
		uint32_t twin = topo[index].twin;
		if (twin != UFBX_NO_INDEX && mesh->vertex_normal.exists) {
			ufbx_assert((size_t)twin < num_topo);
			ufbx_vec3 a0 = ufbx_get_vertex_vec3(&mesh->vertex_normal, index);
			ufbx_vec3 a1 = ufbx_get_vertex_vec3(&mesh->vertex_normal, topo[index].next);
			ufbx_vec3 b0 = ufbx_get_vertex_vec3(&mesh->vertex_normal, topo[twin].next);
			ufbx_vec3 b1 = ufbx_get_vertex_vec3(&mesh->vertex_normal, twin);
			if (a0.x == b0.x && a0.y == b0.y && a0.z == b0.z) return true;
			if (a1.x == b1.x && a1.y == b1.y && a1.z == b1.z) return true;
		}
	} else if (assume_smooth) {
		return true;
	}

	return false;
}

// -- Subdivision

#if UFBXI_FEATURE_SUBDIVISION

typedef struct {
	const void *data;
	ufbx_real weight;
} ufbxi_subdivide_input;

typedef int ufbxi_subdivide_sum_fn(void *user, void *output, const ufbxi_subdivide_input *inputs, size_t num_inputs);

typedef struct {
	ufbxi_subdivide_sum_fn *sum_fn;
	void *sum_user;

	const void *values;
	size_t stride;

	const uint32_t *indices;

	bool check_split_data;
	bool ignore_indices;

	ufbx_subdivision_boundary boundary;

} ufbxi_subdivide_layer_input;

typedef struct {
	void *values;
	size_t num_values;
	uint32_t *indices;
	size_t num_indices;
	bool unique_per_vertex;
} ufbxi_subdivide_layer_output;

typedef struct {
	ufbx_subdivision_weight *weights;
	size_t num_weights;
} ufbxi_subdivision_vertex_weights;

typedef struct {
	ufbxi_mesh_imp *imp;

	ufbx_error error;

	ufbx_mesh *src_mesh_ptr;
	ufbx_mesh src_mesh;
	ufbx_mesh dst_mesh;
	ufbx_topo_edge *topo;
	size_t num_topo;

	ufbx_subdivide_opts opts;

	ufbxi_allocator ator_result;
	ufbxi_allocator ator_tmp;

	ufbxi_buf result;
	ufbxi_buf tmp;
	ufbxi_buf source;

	ufbxi_subdivide_input *inputs;
	size_t inputs_cap;

	ufbx_real *tmp_vertex_weights;
	ufbx_subdivision_weight *tmp_weights;
	size_t total_weights;
	size_t max_vertex_weights;

} ufbxi_subdivide_context;

static int ufbxi_subdivide_sum_vec2(void *user, void *output, const ufbxi_subdivide_input *inputs, size_t num_inputs)
{
	(void)user;
	ufbx_vec2 dst = { 0 };
	ufbxi_nounroll for (size_t i = 0; i != num_inputs; i++) {
		const ufbx_vec2 *src = (const ufbx_vec2*)inputs[i].data;
		ufbx_real weight = inputs[i].weight;
		dst.x += src->x * weight;
		dst.y += src->y * weight;
	}
	*(ufbx_vec2*)output = dst;

	return 1;
}

static int ufbxi_subdivide_sum_vec3(void *user, void *output, const ufbxi_subdivide_input *inputs, size_t num_inputs)
{
	(void)user;
	ufbx_vec3 dst = { 0 };
	ufbxi_nounroll for (size_t i = 0; i != num_inputs; i++) {
		const ufbx_vec3 *src = (const ufbx_vec3*)inputs[i].data;
		ufbx_real weight = inputs[i].weight;
		dst.x += src->x * weight;
		dst.y += src->y * weight;
		dst.z += src->z * weight;
	}
	*(ufbx_vec3*)output = dst;

	return 1;
}

static int ufbxi_subdivide_sum_vec4(void *user, void *output, const ufbxi_subdivide_input *inputs, size_t num_inputs)
{
	(void)user;
	ufbx_vec4 dst = { 0 };
	ufbxi_nounroll for (size_t i = 0; i != num_inputs; i++) {
		const ufbx_vec4 *src = (const ufbx_vec4*)inputs[i].data;
		ufbx_real weight = inputs[i].weight;
		dst.x += src->x * weight;
		dst.y += src->y * weight;
		dst.z += src->z * weight;
		dst.w += src->w * weight;
	}
	*(ufbx_vec4*)output = dst;

	return 1;
}

static ufbxi_noinline bool ufbxi_subdivision_weight_less(void *user, const void *va, const void *vb)
{
	(void)user;
	ufbx_subdivision_weight a = *(const ufbx_subdivision_weight*)va, b = *(const ufbx_subdivision_weight*)vb;
	ufbxi_dev_assert(a.index != b.index);
	if (a.weight != b.weight) return a.weight > b.weight;
	return a.index < b.index;
}

static int ufbxi_subdivide_sum_vertex_weights(void *user, void *output, const ufbxi_subdivide_input *inputs, size_t num_inputs)
{
	ufbxi_subdivide_context *sc = (ufbxi_subdivide_context*)user;

	ufbx_real *vertex_weights = sc->tmp_vertex_weights;
	ufbx_subdivision_weight *tmp_weights = sc->tmp_weights;
	size_t num_weights = 0;

	ufbxi_nounroll for (size_t input_ix = 0; input_ix != num_inputs; input_ix++) {
		ufbxi_subdivision_vertex_weights src = *(const ufbxi_subdivision_vertex_weights*)inputs[input_ix].data;
		ufbx_real input_weight = inputs[input_ix].weight;

		for (size_t weight_ix = 0; weight_ix < src.num_weights; weight_ix++) {
			ufbx_real weight = input_weight * src.weights[weight_ix].weight;
			if (weight < 1.175494351e-38f) continue;

			uint32_t vx = src.weights[weight_ix].index;
			ufbxi_dev_assert(vx < sc->src_mesh.num_vertices);

			ufbx_real prev = vertex_weights[vx];
			vertex_weights[vx] = prev + weight;
			if (prev == 0.0f) {
				tmp_weights[num_weights++].index = vx;
			}
		}
	}

	ufbxi_nounroll for (size_t i = 0; i != num_weights; i++) {
		uint32_t vx = tmp_weights[i].index;
		tmp_weights[i].weight = vertex_weights[vx];
		vertex_weights[vx] = 0.0f;
	}

	ufbxi_unstable_sort(tmp_weights, num_weights, sizeof(ufbx_subdivision_weight), ufbxi_subdivision_weight_less, NULL);

	if (sc->max_vertex_weights != SIZE_MAX) {
		num_weights = ufbxi_min_sz(sc->max_vertex_weights, num_weights);

		// Normalize weights
		ufbx_real prefix_weight = 0.0f;
		ufbxi_nounroll for (size_t i = 0; i != num_weights; i++) {
			prefix_weight += tmp_weights[i].weight;
		}
		ufbxi_nounroll for (size_t i = 0; i != num_weights; i++) {
			tmp_weights[i].weight /= prefix_weight;
		}
	}

	sc->total_weights += num_weights;
	ufbx_subdivision_weight *weights = ufbxi_push_copy(&sc->tmp, ufbx_subdivision_weight, num_weights, tmp_weights);
	ufbxi_check_err(&sc->error, weights);

	ufbxi_subdivision_vertex_weights *dst = (ufbxi_subdivision_vertex_weights*)output;
	dst->weights = weights;
	dst->num_weights = num_weights;

	return 1;
}

static ufbxi_subdivide_sum_fn *const ufbxi_real_sum_fns[] = {
	NULL,
	&ufbxi_subdivide_sum_vec2,
	&ufbxi_subdivide_sum_vec3,
	&ufbxi_subdivide_sum_vec4,
};

ufbxi_noinline static bool ufbxi_is_edge_split(const ufbxi_subdivide_layer_input *input, const ufbx_topo_edge *topo, uint32_t index)
{
	uint32_t twin = topo[index].twin;
	if (twin != UFBX_NO_INDEX) {
		uint32_t a0 = input->indices[index];
		uint32_t a1 = input->indices[topo[index].next];
		uint32_t b0 = input->indices[topo[twin].next];
		uint32_t b1 = input->indices[twin];
		if (a0 == b0 && a1 == b1) return false;
		if (!input->check_split_data) return true;
		size_t stride = input->stride;
		char *da0 = (char*)input->values + a0 * stride;
		char *da1 = (char*)input->values + a1 * stride;
		char *db0 = (char*)input->values + b0 * stride;
		char *db1 = (char*)input->values + b1 * stride;
		if (!memcmp(da0, db0, stride) && !memcmp(da1, db1, stride)) return false;
		return true;
	}

	return false;
}

static ufbx_real ufbxi_edge_crease(const ufbx_mesh *mesh, bool split, const ufbx_topo_edge *topo, uint32_t index)
{
	if (topo[index].twin == UFBX_NO_INDEX) return 1.0f;
	if (split) return 1.0f;
	if (mesh->edge_crease.data && topo[index].edge != UFBX_NO_INDEX) return mesh->edge_crease.data[topo[index].edge] * (ufbx_real)10.0;
	return 0.0f;
}

static ufbxi_noinline int ufbxi_subdivide_layer(ufbxi_subdivide_context *sc, ufbxi_subdivide_layer_output *output, const ufbxi_subdivide_layer_input *input)
{
	ufbx_subdivision_boundary boundary = input->boundary;

	const ufbx_mesh *mesh = &sc->src_mesh;
	const ufbx_topo_edge *topo = sc->topo;
	size_t num_topo = sc->num_topo;

	uint32_t *edge_indices = ufbxi_push(&sc->result, uint32_t, mesh->num_indices);
	ufbxi_check_err(&sc->error, edge_indices);

	size_t num_edge_values = 0;
	for (uint32_t ix = 0; ix < (uint32_t)mesh->num_indices; ix++) {
		uint32_t twin = topo[ix].twin;
		if (twin < ix && !ufbxi_is_edge_split(input, topo, ix)) {
			edge_indices[ix] = edge_indices[twin];
		} else {
			edge_indices[ix] = (uint32_t)num_edge_values++;
		}
	}

	size_t stride = input->stride;
	size_t num_initial_values = (num_edge_values + mesh->num_faces + mesh->num_indices);
	char *values = (char*)ufbxi_push_size(&sc->tmp, stride, num_initial_values);
	ufbxi_check_err(&sc->error, values);

	char *face_values = values;
	char *edge_values = face_values + mesh->num_faces * stride;
	char *vertex_values = edge_values + num_edge_values * stride;

	size_t num_vertex_values = 0;

	uint32_t *vertex_indices = ufbxi_push(&sc->result, uint32_t, mesh->num_indices);
	ufbxi_check_err(&sc->error, vertex_indices);

	size_t min_inputs = ufbxi_max_sz(32, mesh->max_face_triangles + 2);
	ufbxi_check_err(&sc->error, ufbxi_grow_array(&sc->ator_tmp, &sc->inputs, &sc->inputs_cap, min_inputs));
	ufbxi_subdivide_input *inputs = sc->inputs;

	// Assume initially unique per vertex, remove if not the case
	output->unique_per_vertex = true;

	bool sharp_corners = false;
	bool sharp_splits = false;
	bool sharp_all = false;

	switch (boundary) {
	case UFBX_SUBDIVISION_BOUNDARY_DEFAULT:
	case UFBX_SUBDIVISION_BOUNDARY_SHARP_NONE:
	case UFBX_SUBDIVISION_BOUNDARY_LEGACY:
		// All smooth
		break;
	case UFBX_SUBDIVISION_BOUNDARY_SHARP_CORNERS:
		sharp_corners = true;
		break;
	case UFBX_SUBDIVISION_BOUNDARY_SHARP_BOUNDARY:
		sharp_corners = true;
		sharp_splits = true;
		break;
	case UFBX_SUBDIVISION_BOUNDARY_SHARP_INTERIOR:
		sharp_all = true;
		break;
	default:
		ufbxi_unreachable("Bad boundary mode");
	}

	ufbxi_subdivide_sum_fn *sum_fn = input->sum_fn;
	void *sum_user = input->sum_user;

	// Mark unused indices as `UFBX_NO_INDEX` so we can patch non-manifold
	ufbxi_nounroll for (size_t i = 0; i < mesh->num_indices; i++) {
		vertex_indices[i] = UFBX_NO_INDEX;
	}

	// Face points
	for (size_t fi = 0; fi < mesh->num_faces; fi++) {
		ufbx_face face = mesh->faces.data[fi];
		char *dst = face_values + fi * stride;

		ufbx_real weight = 1.0f / (ufbx_real)face.num_indices;
		for (uint32_t ci = 0; ci < face.num_indices; ci++) {
			uint32_t ix = face.index_begin + ci;
			inputs[ci].data = (const char*)input->values + input->indices[ix] * stride;
			inputs[ci].weight = weight;
		}

		ufbxi_check_err(&sc->error, sum_fn(sum_user, dst, inputs, face.num_indices));
	}

	// Edge points
	for (uint32_t ix = 0; ix < mesh->num_indices; ix++) {
		char *dst = edge_values + edge_indices[ix] * stride;

		uint32_t twin = topo[ix].twin;
		bool split = ufbxi_is_edge_split(input, topo, ix);

		if (split || (topo[ix].flags & UFBX_TOPO_NON_MANIFOLD) != 0) {
			output->unique_per_vertex = false;
		}

		ufbx_real crease = 0.0f;
		if (split || twin == UFBX_NO_INDEX) {
			crease = 1.0f;
		} else if (topo[ix].edge != UFBX_NO_INDEX && mesh->edge_crease.data) {
			crease = mesh->edge_crease.data[topo[ix].edge] * (ufbx_real)10.0;
		}
		if (sharp_all) crease = 1.0f;

		const char *v0 = (const char*)input->values + input->indices[ix] * stride;
		const char *v1 = (const char*)input->values + input->indices[topo[ix].next] * stride;

		// TODO: Unify
		if (twin < ix && !split) {
			// Already calculated
		} else if (crease <= 0.0f) {
			const char *f0 = face_values + topo[ix].face * stride;
			const char *f1 = face_values + topo[twin].face * stride;
			inputs[0].data = v0;
			inputs[0].weight = 0.25f;
			inputs[1].data = v1;
			inputs[1].weight = 0.25f;
			inputs[2].data = f0;
			inputs[2].weight = 0.25f;
			inputs[3].data = f1;
			inputs[3].weight = 0.25f;
			ufbxi_check_err(&sc->error, sum_fn(sum_user, dst, inputs, 4));
		} else if (crease >= 1.0f) {
			inputs[0].data = v0;
			inputs[0].weight = 0.5f;
			inputs[1].data = v1;
			inputs[1].weight = 0.5f;
			ufbxi_check_err(&sc->error, sum_fn(sum_user, dst, inputs, 2));
		} else if (crease < 1.0f) {
			const char *f0 = face_values + topo[ix].face * stride;
			const char *f1 = face_values + topo[twin].face * stride;
			ufbx_real w0 = 0.25f + 0.25f * crease;
			ufbx_real w1 = 0.25f - 0.25f * crease;

			inputs[0].data = v0;
			inputs[0].weight = w0;
			inputs[1].data = v1;
			inputs[1].weight = w0;
			inputs[2].data = f0;
			inputs[2].weight = w1;
			inputs[3].data = f1;
			inputs[3].weight = w1;
			ufbxi_check_err(&sc->error, sum_fn(sum_user, dst, inputs, 4));
		}
	}

	// Vertex points
	for (size_t vi = 0; vi < mesh->num_vertices; vi++) {
		uint32_t original_start = mesh->vertex_first_index.data[vi];
		if (original_start == UFBX_NO_INDEX) continue;

		// Find a topological boundary, or if not found a split edge
		uint32_t start = original_start;
		for (uint32_t cur = start;;) {
			uint32_t prev = ufbx_topo_prev_vertex_edge(topo, num_topo, cur);
			if (prev == UFBX_NO_INDEX) { start = cur; break; } // Topological boundary: Stop and use as start
			if (ufbxi_is_edge_split(input, topo, prev)) start = cur; // Split edge: Consider as start
			if (prev == original_start) break; // Loop: Stop, use original start or split if found
			cur = prev;
		}

		original_start = start;
		while (start != UFBX_NO_INDEX) {
			if (start != original_start) {
				output->unique_per_vertex = false;
			}

			uint32_t value_index = (uint32_t)num_vertex_values++;
			char *dst = vertex_values + value_index * stride;

			// We need to compute the average crease value and keep track of
			// two creased edges, if there's more we use the corner rule that
			// does not need the information.
			ufbx_real total_crease = 0.0f;
			size_t num_crease = 0;
			size_t num_split = 0;
			bool on_boundary = false;
			bool non_manifold = false;
			size_t crease_input_indices[2]; // ufbxi_uninit

			// At start we always have two edges and a single face
			uint32_t start_prev = topo[start].prev;
			uint32_t end_edge = topo[start_prev].twin;
			size_t valence = 2;

			non_manifold |= (topo[start].flags & UFBX_TOPO_NON_MANIFOLD) != 0;
			non_manifold |= (topo[start_prev].flags & UFBX_TOPO_NON_MANIFOLD) != 0;

			const char *v0 = (const char*)input->values + input->indices[start] * stride;

			size_t num_inputs = 4;

			{
				const char *e0 = (const char*)input->values + input->indices[topo[start].next] * stride;
				const char *e1 = (const char*)input->values + input->indices[start_prev] * stride;
				const char *f0 = face_values + topo[start].face * stride;
				inputs[0].data = v0;
				inputs[1].data = e0;
				inputs[2].data = e1;
				inputs[3].data = f0;
			}

			bool start_split = ufbxi_is_edge_split(input, topo, start);
			bool prev_split = end_edge != UFBX_NO_INDEX && ufbxi_is_edge_split(input, topo, end_edge);

			// Either of the first two edges may be creased
			ufbx_real start_crease = ufbxi_edge_crease(mesh, start_split, topo, start);
			if (start_crease > 0.0f) {
				total_crease += start_crease;
				crease_input_indices[num_crease++] = 1;
			}
			ufbx_real prev_crease = ufbxi_edge_crease(mesh, prev_split, topo, start_prev);
			if (prev_crease > 0.0f) {
				total_crease += prev_crease;
				crease_input_indices[num_crease++] = 2;
			}

			if (end_edge != UFBX_NO_INDEX) {
				if (prev_split) {
					num_split++;
				}
			} else {
				on_boundary = true;
			}

			ufbxi_check_err(&sc->error, vertex_indices[start] == UFBX_NO_INDEX);
			vertex_indices[start] = value_index;

			if (start_split) {
				// We need to special case if the first edge is split as we have
				// handled it already in the code above..
				start = ufbx_topo_next_vertex_edge(topo, num_topo, start);
				num_split++;
			} else {
				// Follow vertex edges until we either hit a topological/split boundary
				// or loop back to the left edge we accounted for in `start_prev`
				uint32_t cur = start;
				for (;;) {
					cur = ufbx_topo_next_vertex_edge(topo, num_topo, cur);

					// Topological boundary: Finished
					if (cur == UFBX_NO_INDEX) {
						on_boundary = true;
						start = UFBX_NO_INDEX;
						break;
					}

					non_manifold |= (topo[cur].flags & UFBX_TOPO_NON_MANIFOLD) != 0;
					ufbxi_check_err(&sc->error, vertex_indices[cur] == UFBX_NO_INDEX);
					vertex_indices[cur] = value_index;

					bool split = ufbxi_is_edge_split(input, topo, cur);

					// Looped: Add the face from the other side still if not split
					if (cur == end_edge && !split) {
						ufbxi_check_err(&sc->error, ufbxi_grow_array(&sc->ator_tmp, &sc->inputs, &sc->inputs_cap, num_inputs + 1));
						const char *f0 = face_values + topo[cur].face * stride;
						inputs[num_inputs].data = f0;
						start = UFBX_NO_INDEX;
						num_inputs += 1;
						break;
					}

					// Add the edge crease, this also handles boundaries as they
					// have an implicit crease of 1.0 using `ufbxi_edge_crease()`
					ufbx_real cur_crease = ufbxi_edge_crease(mesh, split, topo, cur);
					if (cur_crease > 0.0f) {
						total_crease += cur_crease;
						if (num_crease < 2) crease_input_indices[num_crease] = num_inputs;
						num_crease++;
					}

					// Add the new edge and face to the sum
					{
						ufbxi_check_err(&sc->error, ufbxi_grow_array(&sc->ator_tmp, &sc->inputs, &sc->inputs_cap, num_inputs + 2));
						inputs = sc->inputs;

						const char *e0 = (char*)input->values + input->indices[topo[cur].next] * stride;
						const char *f0 = face_values + topo[cur].face * stride;
						inputs[num_inputs + 0].data = e0;
						inputs[num_inputs + 1].data = f0;
						num_inputs += 2;
					}
					valence++;

					// If we landed at a split edge advance to the next one
					// and continue from there in the outer loop
					if (split) {
						start = ufbx_topo_next_vertex_edge(topo, num_topo, cur);
						num_split++;
						break;
					}
				}
			}

			if (start == original_start) start = UFBX_NO_INDEX;

			// Weights for various subdivision masks
			ufbx_real fe_weight = 1.0f / (ufbx_real)(valence*valence);
			ufbx_real v_weight = (ufbx_real)(valence - 2) / (ufbx_real)valence;

			// Select the right subdivision mask depending on valence and crease
			if (num_crease > 2
				|| (sharp_corners && valence == 2 && (num_split > 0 || on_boundary))
				|| (sharp_splits && (num_split > 0 || on_boundary))
				|| sharp_all
				|| non_manifold) {
				// Corner: Copy as-is
				inputs[0].data = v0;
				inputs[0].weight = 1.0f;
				num_inputs = 1;
			} else if (num_crease == 2) {
				// Boundary: Interpolate edge
				total_crease *= 0.5f;
				if (total_crease < 0.0f) total_crease = 0.0f;
				if (total_crease > 1.0f) total_crease = 1.0f;

				inputs[0].weight = v_weight * (1.0f - total_crease) + 0.75f * total_crease;
				ufbx_real few = fe_weight * (1.0f - total_crease);
				for (size_t i = 1; i < num_inputs; i++) {
					inputs[i].weight = few;
				}

				// Add weight to the creased edges
				inputs[crease_input_indices[0]].weight += 0.125f * total_crease;
				inputs[crease_input_indices[1]].weight += 0.125f * total_crease;
			} else {
				// Regular: Weighted sum with the accumulated edge/face points
				inputs[0].weight = v_weight;
				for (size_t i = 1; i < num_inputs; i++) {
					inputs[i].weight = fe_weight;
				}

			}

			if (mesh->vertex_crease.exists) {
				ufbx_real v = ufbx_get_vertex_real(&mesh->vertex_crease, original_start);
				v *= (ufbx_real)10.0;
				if (v > 0.0f) {
					if (v > 1.0) v = 1.0f;

					ufbx_real iv = 1.0f - v;
					inputs[0].weight = 1.0f * v + (inputs[0].weight) * iv;
					for (size_t i = 1; i < num_inputs; i++) {
						inputs[i].weight *= iv;
					}
				}
			}

#if defined(UFBX_REGRESSION)
			{
				ufbx_real total_weight = 0.0f;
				for (size_t i = 0; i < num_inputs; i++) {
					total_weight += inputs[i].weight;
				}
				ufbx_assert(ufbx_fabs(total_weight - 1.0f) < 0.001f);
			}
#endif

			ufbxi_check_err(&sc->error, sum_fn(sum_user, dst, inputs, num_inputs));
		}
	}

	// Copy non-manifold vertex values as-is
	for (size_t old_ix = 0; old_ix < mesh->num_indices; old_ix++) {
		uint32_t ix = vertex_indices[old_ix];
		if (ix == UFBX_NO_INDEX) {
			ix = (uint32_t)num_vertex_values++;
			vertex_indices[old_ix] = ix;
			const char *src = (const char*)input->values + input->indices[old_ix] * stride;
			char *dst = vertex_values + ix * stride;

			inputs[0].data = src;
			inputs[0].weight = 1.0f;
			ufbxi_check_err(&sc->error, sum_fn(sum_user, dst, inputs, 1));
		}
	}

	ufbx_assert(num_vertex_values <= mesh->num_indices);
	size_t num_values = num_edge_values + mesh->num_faces + num_vertex_values;
	char *new_values = (char*)ufbxi_push_size(&sc->result, stride, (num_values+1));
	ufbxi_check_err(&sc->error, new_values);

	memset(new_values, 0, stride);
	new_values += stride;

	memcpy(new_values, values, num_values * stride);

	output->values = new_values;
	output->num_values = num_values;

	if (!input->ignore_indices) {
		uint32_t *new_indices = ufbxi_push(&sc->result, uint32_t, mesh->num_indices * 4);
		ufbxi_check_err(&sc->error, new_indices);

		uint32_t face_start = 0;
		uint32_t edge_start = (uint32_t)(face_start + mesh->num_faces);
		uint32_t vert_start = (uint32_t)(edge_start + num_edge_values);
		uint32_t *p_ix = new_indices;
		for (size_t ix = 0; ix < mesh->num_indices; ix++) {
			p_ix[0] = vert_start + vertex_indices[ix];
			p_ix[1] = edge_start + edge_indices[ix];
			p_ix[2] = face_start + topo[ix].face;
			p_ix[3] = edge_start + edge_indices[topo[ix].prev];
			p_ix += 4;
		}
		output->indices = new_indices;
		output->num_indices = mesh->num_indices * 4;
	} else {
		output->indices = NULL;
		output->num_indices = 0;
	}

	return 1;
}

static ufbxi_noinline int ufbxi_subdivide_attrib(ufbxi_subdivide_context *sc, ufbx_vertex_attrib *attrib, ufbx_subdivision_boundary boundary, bool check_split_data)
{
	if (!attrib->exists) return 1;

	ufbx_assert(attrib->value_reals >= 2 && attrib->value_reals <= 4);

	ufbxi_subdivide_layer_input input; // ufbxi_uninit
	input.sum_fn = ufbxi_real_sum_fns[attrib->value_reals - 1];
	input.sum_user = NULL;
	input.values = attrib->values.data;
	input.indices = attrib->indices.data;
	input.stride = attrib->value_reals * sizeof(ufbx_real);
	input.boundary = boundary;
	input.check_split_data = check_split_data;
	input.ignore_indices = false;

	ufbxi_subdivide_layer_output output; // ufbxi_uninit
	ufbxi_check_err(&sc->error, ufbxi_subdivide_layer(sc, &output, &input));

	attrib->values.data = output.values;
	attrib->indices.data = output.indices;
	attrib->values.count = output.num_values;
	attrib->indices.count = output.num_indices;

	return 1;
}

static ufbxi_noinline ufbxi_subdivision_vertex_weights *ufbxi_subdivision_copy_weights(ufbxi_subdivide_context *sc, ufbx_subdivision_weight_range_list ranges, ufbx_subdivision_weight_list weights)
{
	ufbxi_subdivision_vertex_weights *dst = ufbxi_push(&sc->tmp, ufbxi_subdivision_vertex_weights, ranges.count);
	ufbxi_check_return_err(&sc->error, dst, NULL);

	ufbxi_nounroll for (size_t i = 0; i != ranges.count; i++) {
		ufbx_subdivision_weight_range range = ranges.data[i];
		dst[i].weights = weights.data + range.weight_begin;
		dst[i].num_weights = range.num_weights;
	}

	return dst;
}

static ufbxi_noinline ufbxi_subdivision_vertex_weights *ufbxi_init_source_vertex_weights(ufbxi_subdivide_context *sc, size_t num_vertices)
{
	ufbxi_subdivision_vertex_weights *dst = ufbxi_push(&sc->tmp, ufbxi_subdivision_vertex_weights, num_vertices);
	ufbx_subdivision_weight *weights = ufbxi_push(&sc->tmp, ufbx_subdivision_weight, num_vertices);
	ufbxi_check_return_err(&sc->error, dst && weights, NULL);

	ufbxi_nounroll for (size_t i = 0; i != num_vertices; i++) {
		dst[i].weights = weights + i;
		dst[i].num_weights = 1;
		weights[i].index = (uint32_t)i;
		weights[i].weight = 1.0f;
	}

	return dst;
}

static ufbxi_noinline ufbxi_subdivision_vertex_weights *ufbxi_init_skin_weights(ufbxi_subdivide_context *sc, size_t num_vertices, const ufbx_skin_deformer *skin)
{
	ufbxi_subdivision_vertex_weights *dst = ufbxi_push(&sc->tmp, ufbxi_subdivision_vertex_weights, num_vertices);
	ufbxi_check_return_err(&sc->error, dst, NULL);

	for (size_t i = 0; i < num_vertices; i++) {
		ufbxi_dev_assert(i < skin->vertices.count);
		ufbx_skin_vertex vertex = skin->vertices.data[i];
		size_t num_weights = ufbxi_min_sz(sc->max_vertex_weights, vertex.num_weights);

		ufbx_subdivision_weight *weights = ufbxi_push(&sc->tmp, ufbx_subdivision_weight, num_weights);
		ufbxi_check_err(&sc->error, weights);

		const ufbx_skin_weight *skin_weights = skin->weights.data + vertex.weight_begin;

		dst[i].weights = weights;
		dst[i].num_weights = num_weights;
		ufbxi_nounroll for (size_t wi = 0; wi != num_weights; wi++) {
			ufbxi_check_err(&sc->error, skin_weights[wi].cluster_index <= INT32_MAX);
			weights[wi].index = skin_weights[wi].cluster_index;
			weights[wi].weight = skin_weights[wi].weight;
		}
	}

	return dst;
}

static ufbxi_noinline int ufbxi_subdivide_weights(ufbxi_subdivide_context *sc, ufbx_subdivision_weight_range_list *ranges,
	ufbx_subdivision_weight_list *weights, const ufbxi_subdivision_vertex_weights *src)
{
	ufbxi_check_err(&sc->error, src);

	ufbxi_subdivide_layer_input input; // ufbxi_uninit
	input.sum_fn = ufbxi_subdivide_sum_vertex_weights;
	input.sum_user = sc;
	input.values = src;
	input.indices = sc->src_mesh.vertex_indices.data;
	input.stride = sizeof(ufbxi_subdivision_vertex_weights);
	input.boundary = sc->opts.boundary;
	input.check_split_data = false;
	input.ignore_indices = true;

	sc->total_weights = 0;

	ufbxi_subdivide_layer_output output; // ufbxi_uninit
	ufbxi_check_err(&sc->error, ufbxi_subdivide_layer(sc, &output, &input));

	size_t num_vertices = output.num_values;
	ufbx_assert(num_vertices == sc->dst_mesh.vertex_position.values.count);

	ufbx_subdivision_weight_range *dst_ranges = ufbxi_push(&sc->result, ufbx_subdivision_weight_range, num_vertices);
	ufbx_subdivision_weight *dst_weights = ufbxi_push(&sc->result, ufbx_subdivision_weight, sc->total_weights);
	ufbxi_check_err(&sc->error, ranges && weights);

	ufbxi_subdivision_vertex_weights *src_weights = (ufbxi_subdivision_vertex_weights*)output.values;

	size_t weight_offset = 0;
	for (size_t vi = 0; vi < num_vertices; vi++) {
		ufbxi_subdivision_vertex_weights ws = src_weights[vi];
		ufbxi_check_err(&sc->error, (size_t)UINT32_MAX - weight_offset >= ws.num_weights);

		dst_ranges[vi].weight_begin = (uint32_t)weight_offset;
		dst_ranges[vi].num_weights = (uint32_t)ws.num_weights;
		memcpy(dst_weights + weight_offset, ws.weights, ws.num_weights * sizeof(ufbx_subdivision_weight));
		weight_offset += ws.num_weights;
	}

	ranges->data = dst_ranges;
	ranges->count = num_vertices;
	weights->data = dst_weights;
	weights->count = sc->total_weights;

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_subdivide_vertex_crease(ufbxi_subdivide_context *sc, ufbx_vertex_real *ufbxi_restrict dst, const ufbx_vertex_real *ufbxi_restrict src)
{
	size_t src_indices = src->indices.count;
	size_t src_values = src->values.count;

	dst->values.count = src_values + 1;
	dst->values.data = ufbxi_push(&sc->result, ufbx_real, dst->values.count);
	ufbxi_check_err(&sc->error, dst->values.data);
	dst->values.data[src_values] = 0.0f;

	dst->indices.count = src_indices * 4;
	dst->indices.data = ufbxi_push(&sc->result, uint32_t, dst->indices.count);
	ufbxi_check_err(&sc->error, dst->indices.data);

	// Reduce the amount of vertex crease on each iteration
	ufbxi_nounroll for (size_t i = 0; i < src_values; i++) {
		ufbx_real crease = src->values.data[i];
		if (crease < 0.999f) crease -= 0.1f;
		if (crease < 0.0f) crease = 0.0f;
		dst->values.data[i] = crease;
	}

	// Write the crease at the vertex corner and zero (at `src_values`) on other ones
	uint32_t zero_index = (uint32_t)src_values;
	ufbxi_nounroll for (size_t i = 0; i < src_indices; i++) {
		uint32_t *quad = dst->indices.data + i * 4;
		quad[0] = src->indices.data[i];
		quad[1] = zero_index;
		quad[2] = zero_index;
		quad[3] = zero_index;
	}

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_subdivide_mesh_level(ufbxi_subdivide_context *sc)
{
	const ufbx_mesh *mesh = &sc->src_mesh;
	ufbx_mesh *result = &sc->dst_mesh;

	*result = *mesh;

	ufbx_topo_edge *topo = ufbxi_push(&sc->tmp, ufbx_topo_edge, mesh->num_indices);
	ufbxi_check_err(&sc->error, topo);
	ufbx_compute_topology(mesh, topo, mesh->num_indices);
	sc->topo = topo;
	sc->num_topo = mesh->num_indices;

	ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&result->vertex_position, sc->opts.boundary, false));

	memset(&result->vertex_uv, 0, sizeof(result->vertex_uv));
	memset(&result->vertex_tangent, 0, sizeof(result->vertex_tangent));
	memset(&result->vertex_bitangent, 0, sizeof(result->vertex_bitangent));
	memset(&result->vertex_color, 0, sizeof(result->vertex_color));

	result->uv_sets.data = ufbxi_push_copy(&sc->result, ufbx_uv_set, result->uv_sets.count, result->uv_sets.data);
	ufbxi_check_err(&sc->error, result->uv_sets.data);

	result->color_sets.data = ufbxi_push_copy(&sc->result, ufbx_color_set, result->color_sets.count, result->color_sets.data);
	ufbxi_check_err(&sc->error, result->color_sets.data);

	ufbxi_for_list(ufbx_uv_set, set, result->uv_sets) {
		ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&set->vertex_uv, sc->opts.uv_boundary, true));
		if (sc->opts.interpolate_tangents) {
			ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&set->vertex_tangent, sc->opts.uv_boundary, true));
			ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&set->vertex_bitangent, sc->opts.uv_boundary, true));
		} else {
			memset(&set->vertex_tangent, 0, sizeof(set->vertex_tangent));
			memset(&set->vertex_bitangent, 0, sizeof(set->vertex_bitangent));
		}
	}

	ufbxi_for_list(ufbx_color_set, set, result->color_sets) {
		ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&set->vertex_color, sc->opts.uv_boundary, true));
	}

	if (result->uv_sets.count > 0) {
		result->vertex_uv = result->uv_sets.data[0].vertex_uv;
		result->vertex_bitangent = result->uv_sets.data[0].vertex_bitangent;
		result->vertex_tangent = result->uv_sets.data[0].vertex_tangent;
	}
	if (result->color_sets.count > 0) {
		result->vertex_color = result->color_sets.data[0].vertex_color;
	}

	if (sc->opts.interpolate_normals && !sc->opts.ignore_normals) {
		ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&result->vertex_normal, sc->opts.boundary, true));
		ufbxi_for_list(ufbx_vec3, normal, result->vertex_normal.values) {
			*normal = ufbxi_slow_normalize3(normal);
		}
		if (mesh->skinned_normal.values.data == mesh->vertex_normal.values.data) {
			result->skinned_normal = result->vertex_normal;
		} else {
			ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&result->skinned_normal, sc->opts.boundary, true));
			ufbxi_for_list(ufbx_vec3, normal, result->skinned_normal.values) {
				*normal = ufbxi_slow_normalize3(normal);
			}
		}
	}

	if (result->vertex_crease.exists) {
		ufbxi_check_err(&sc->error, ufbxi_subdivide_vertex_crease(sc, &result->vertex_crease, &mesh->vertex_crease));
	}

	if (mesh->skinned_position.values.data == mesh->vertex_position.values.data) {
		result->skinned_position = result->vertex_position;
	} else {
		ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&result->skinned_position, sc->opts.boundary, false));
	}

	ufbx_subdivision_result *result_sub = ufbxi_push_zero(&sc->result, ufbx_subdivision_result, 1);
	ufbxi_check_err(&sc->error, result_sub);
	result->subdivision_result = result_sub;

	if (sc->opts.evaluate_source_vertices || sc->opts.evaluate_skin_weights) {
		ufbx_subdivision_result *mesh_sub = mesh->subdivision_result;

		ufbx_skin_deformer *skin = NULL;
		if (sc->opts.evaluate_skin_weights) {
			if (mesh->skin_deformers.count > 0) {
				ufbxi_check_err(&sc->error, sc->opts.skin_deformer_index < mesh->skin_deformers.count);
				skin = mesh->skin_deformers.data[sc->opts.skin_deformer_index];
			}
		}

		size_t max_weights = 0;
		if (sc->opts.evaluate_source_vertices) {
			max_weights = ufbxi_max_sz(max_weights, mesh->num_vertices);
		}
		if (skin) {
			max_weights = ufbxi_max_sz(max_weights, skin->clusters.count);
		}

		sc->tmp_vertex_weights = ufbxi_push_zero(&sc->tmp, ufbx_real, mesh->num_vertices);
		sc->tmp_weights = ufbxi_push(&sc->tmp, ufbx_subdivision_weight, max_weights);
		ufbxi_check_err(&sc->error, sc->tmp_vertex_weights && sc->tmp_weights);

		if (sc->opts.evaluate_source_vertices) {
			sc->max_vertex_weights = sc->opts.max_source_vertices ? sc->opts.max_source_vertices : SIZE_MAX;

			ufbxi_subdivision_vertex_weights *weights;
			if (mesh_sub && mesh_sub->source_vertex_ranges.count > 0) {
				weights = ufbxi_subdivision_copy_weights(sc, mesh_sub->source_vertex_ranges, mesh_sub->source_vertex_weights);
			} else {
				weights = ufbxi_init_source_vertex_weights(sc, mesh->num_vertices);
			}

			ufbxi_check_err(&sc->error, ufbxi_subdivide_weights(sc, &result_sub->source_vertex_ranges, &result_sub->source_vertex_weights, weights));
		}

		if (skin) {
			sc->max_vertex_weights = sc->opts.max_skin_weights ? sc->opts.max_skin_weights : SIZE_MAX;

			ufbxi_subdivision_vertex_weights *weights;
			if (mesh_sub && mesh_sub->source_vertex_ranges.count > 0) {
				weights = ufbxi_subdivision_copy_weights(sc, mesh_sub->skin_cluster_ranges, mesh_sub->skin_cluster_weights);
			} else {
				weights = ufbxi_init_skin_weights(sc, mesh->num_vertices, skin);
			}

			ufbxi_check_err(&sc->error, ufbxi_subdivide_weights(sc, &result_sub->skin_cluster_ranges, &result_sub->skin_cluster_weights, weights));
		}

	}

	result->num_vertices = result->vertex_position.values.count;
	result->num_indices = mesh->num_indices * 4;
	result->num_faces = mesh->num_indices;
	result->num_triangles = mesh->num_indices * 2;

	result->vertex_indices.data = result->vertex_position.indices.data;
	result->vertex_indices.count = result->num_indices;
	result->vertices.data = result->vertex_position.values.data;
	result->vertices.count = result->num_vertices;

	result->faces.count = result->num_faces;
	result->faces.data = ufbxi_push(&sc->result, ufbx_face, result->num_faces);
	ufbxi_check_err(&sc->error, result->faces.data);

	for (size_t i = 0; i < result->num_faces; i++) {
		result->faces.data[i].index_begin = (uint32_t)(i * 4);
		result->faces.data[i].num_indices = 4;
	}

	if (mesh->edges.data) {
		result->num_edges = mesh->num_edges*2 + result->num_faces;
		result->edges.count = result->num_edges;
		result->edges.data = ufbxi_push(&sc->result, ufbx_edge, result->num_edges);
		ufbxi_check_err(&sc->error, result->edges.data);

		if (mesh->edge_crease.data) {
			result->edge_crease.count = result->num_edges;
			result->edge_crease.data = ufbxi_push(&sc->result, ufbx_real, result->num_edges);
			ufbxi_check_err(&sc->error, result->edge_crease.data);
		}
		if (mesh->edge_smoothing.data) {
			result->edge_smoothing.count = result->num_edges;
			result->edge_smoothing.data = ufbxi_push(&sc->result, bool, result->num_edges);
			ufbxi_check_err(&sc->error, result->edge_smoothing.data);
		}
		if (mesh->edge_visibility.data) {
			result->edge_visibility.count = result->num_edges;
			result->edge_visibility.data = ufbxi_push(&sc->result, bool, result->num_edges);
			ufbxi_check_err(&sc->error, result->edge_visibility.data);
		}

		size_t di = 0;
		for (size_t i = 0; i < mesh->num_edges; i++) {
			ufbx_edge edge = mesh->edges.data[i];
			uint32_t face_ix = topo[edge.a].face;
			ufbx_face face = mesh->faces.data[face_ix];
			uint32_t offset = edge.a - face.index_begin;
			uint32_t next = (offset + 1) % (uint32_t)face.num_indices;

			uint32_t a = (face.index_begin + offset) * 4;
			uint32_t b = (face.index_begin + next) * 4;

			result->edges.data[di + 0].a = a;
			result->edges.data[di + 0].b = a + 1;
			result->edges.data[di + 1].a = b + 3;
			result->edges.data[di + 1].b = b;

			if (mesh->edge_crease.data) {
				ufbx_real crease = mesh->edge_crease.data[i];
				if (crease < 0.999f) crease -= (ufbx_real)0.1;
				if (crease < 0.0f) crease = 0.0f;
				result->edge_crease.data[di + 0] = crease;
				result->edge_crease.data[di + 1] = crease;
			}

			if (mesh->edge_smoothing.data) {
				result->edge_smoothing.data[di + 0] = mesh->edge_smoothing.data[i];
				result->edge_smoothing.data[di + 1] = mesh->edge_smoothing.data[i];
			}

			if (mesh->edge_visibility.data) {
				result->edge_visibility.data[di + 0] = mesh->edge_visibility.data[i];
				result->edge_visibility.data[di + 1] = mesh->edge_visibility.data[i];
			}

			di += 2;
		}

		for (size_t fi = 0; fi < result->num_faces; fi++) {
			result->edges.data[di].a = (uint32_t)(fi * 4 + 1);
			result->edges.data[di].b = (uint32_t)(fi * 4 + 2);

			if (result->edge_crease.data) {
				result->edge_crease.data[di] = 0.0f;
			}

			if (result->edge_smoothing.data) {
				result->edge_smoothing.data[di + 0] = true;
			}

			if (result->edge_visibility.data) {
				result->edge_visibility.data[di + 0] = false;
			}

			di++;
		}
	}

	if (mesh->face_material.data) {
		result->face_material.count = result->num_faces;
		result->face_material.data = ufbxi_push(&sc->result, uint32_t, result->num_faces);
		ufbxi_check_err(&sc->error, result->face_material.data);
	}
	if (mesh->face_smoothing.data) {
		result->face_smoothing.count = result->num_faces;
		result->face_smoothing.data = ufbxi_push(&sc->result, bool, result->num_faces);
		ufbxi_check_err(&sc->error, result->face_smoothing.data);
	}
	if (mesh->face_group.data) {
		result->face_group.count = result->num_faces;
		result->face_group.data = ufbxi_push(&sc->result, uint32_t, result->num_faces);
		ufbxi_check_err(&sc->error, result->face_group.data);
	}
	if (mesh->face_hole.data) {
		result->face_hole.count = result->num_faces;
		result->face_hole.data = ufbxi_push(&sc->result, bool, result->num_faces);
		ufbxi_check_err(&sc->error, result->face_hole.data);
	}

	if (result->material_parts.count > 0) {
		result->material_parts.data = ufbxi_push_zero(&sc->result, ufbx_mesh_part, result->material_parts.count);
		ufbxi_check_err(&sc->error, result->materials.data);
	}

	size_t index_offset = 0;
	for (size_t i = 0; i < mesh->num_faces; i++) {
		ufbx_face face = mesh->faces.data[i];

		uint32_t mat = 0;
		if (mesh->face_material.data) {
			mat = mesh->face_material.data[i];
			for (size_t ci = 0; ci < face.num_indices; ci++) {
				result->face_material.data[index_offset + ci] = mat;
			}
		}
		if (mesh->face_smoothing.data) {
			bool flag = mesh->face_smoothing.data[i];
			for (size_t ci = 0; ci < face.num_indices; ci++) {
				result->face_smoothing.data[index_offset + ci] = flag;
			}
		}
		if (mesh->face_group.data) {
			uint32_t group = mesh->face_group.data[i];
			for (size_t ci = 0; ci < face.num_indices; ci++) {
				result->face_group.data[index_offset + ci] = group;
			}
		}
		if (mesh->face_hole.data) {
			bool flag = mesh->face_hole.data[i];
			for (size_t ci = 0; ci < face.num_indices; ci++) {
				result->face_hole.data[index_offset + ci] = flag;
			}
		}
		index_offset += face.num_indices;
	}

	// Will be filled in by `ufbxi_finalize_mesh()`.
	result->vertex_first_index.count = 0;

	ufbxi_check_err(&sc->error, ufbxi_finalize_mesh_material(&sc->result, &sc->error, result));
	ufbxi_check_err(&sc->error, ufbxi_finalize_mesh(&sc->result, &sc->error, result));
	ufbxi_check_err(&sc->error, ufbxi_update_face_groups(&sc->result, &sc->error, result, true));

	return 1;
}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_subdivide_mesh_imp(ufbxi_subdivide_context *sc, size_t level)
{
	if (sc->opts.boundary == UFBX_SUBDIVISION_BOUNDARY_DEFAULT) {
		sc->opts.boundary = sc->src_mesh.subdivision_boundary;
	}

	if (sc->opts.uv_boundary == UFBX_SUBDIVISION_BOUNDARY_DEFAULT) {
		sc->opts.uv_boundary = sc->src_mesh.subdivision_uv_boundary;
	}

	ufbxi_init_ator(&sc->error, &sc->ator_tmp, &sc->opts.temp_allocator, "temp");
	ufbxi_init_ator(&sc->error, &sc->ator_result, &sc->opts.result_allocator, "result");

	sc->result.unordered = true;
	sc->source.unordered = true;
	sc->tmp.unordered = true;

	sc->source.ator = &sc->ator_tmp;
	sc->tmp.ator = &sc->ator_tmp;

	for (size_t i = 1; i < level; i++) {
		sc->result.ator = &sc->ator_tmp;

		ufbxi_check_err(&sc->error, ufbxi_subdivide_mesh_level(sc));

		sc->src_mesh = sc->dst_mesh;

		ufbxi_buf_free(&sc->source);
		ufbxi_buf_free(&sc->tmp);
		sc->source = sc->result;
		memset(&sc->result, 0, sizeof(sc->result));
	}

	sc->result.ator = &sc->ator_result;
	ufbxi_check_err(&sc->error, ufbxi_subdivide_mesh_level(sc));
	ufbxi_buf_free(&sc->tmp);

	ufbx_mesh *mesh = &sc->dst_mesh;

	// Subdivision always results in a mesh that consists only of quads
	mesh->max_face_triangles = 2;
	mesh->num_empty_faces = 0;
	mesh->num_point_faces = 0;
	mesh->num_line_faces = 0;

	if (!sc->opts.interpolate_normals) {
		memset(&mesh->vertex_normal, 0, sizeof(mesh->vertex_normal));
		memset(&mesh->skinned_normal, 0, sizeof(mesh->skinned_normal));
	}

	if (!sc->opts.interpolate_normals && !sc->opts.ignore_normals) {

		ufbx_topo_edge *topo = ufbxi_push(&sc->tmp, ufbx_topo_edge, mesh->num_indices);
		ufbxi_check_err(&sc->error, topo);
		ufbx_compute_topology(mesh, topo, mesh->num_indices);

		uint32_t *normal_indices = ufbxi_push(&sc->result, uint32_t, mesh->num_indices);
		ufbxi_check_err(&sc->error, normal_indices);

		size_t num_normals = ufbx_generate_normal_mapping(mesh, topo, mesh->num_indices, normal_indices, mesh->num_indices, true);
		if (num_normals == mesh->num_vertices) {
			mesh->skinned_normal.unique_per_vertex = true;
		}

		ufbx_vec3 *normal_data = ufbxi_push(&sc->result, ufbx_vec3, num_normals + 1);
		ufbxi_check_err(&sc->error, normal_data);
		normal_data[0] = ufbx_zero_vec3;
		normal_data++;

		ufbx_compute_normals(mesh, &mesh->skinned_position, normal_indices, mesh->num_indices, normal_data, num_normals);

		mesh->generated_normals = true;
		mesh->vertex_normal.exists = true;
		mesh->vertex_normal.values.data = normal_data;
		mesh->vertex_normal.values.count = num_normals;
		mesh->vertex_normal.indices.data = normal_indices;
		mesh->vertex_normal.indices.count = mesh->num_indices;

		mesh->skinned_normal = mesh->vertex_normal;
	}

	ufbxi_refcount *parent = NULL;
	if (sc->src_mesh_ptr->subdivision_evaluated && sc->src_mesh_ptr->from_tessellated_nurbs) {
		parent = &(ufbxi_get_imp(ufbxi_mesh_imp, sc->src_mesh_ptr))->refcount;
	} else {
		parent = &(ufbxi_get_imp(ufbxi_scene_imp, sc->src_mesh_ptr->element.scene))->refcount;
	}

	ufbxi_patch_mesh_reals(mesh);

	sc->imp = ufbxi_push(&sc->result, ufbxi_mesh_imp, 1);
	ufbxi_check_err(&sc->error, sc->imp);

	sc->dst_mesh.subdivision_result->result_memory_used = sc->ator_result.current_size;
	sc->dst_mesh.subdivision_result->temp_memory_used = sc->ator_tmp.current_size;
	sc->dst_mesh.subdivision_result->result_allocs = sc->ator_result.num_allocs;
	sc->dst_mesh.subdivision_result->temp_allocs = sc->ator_tmp.num_allocs;

	ufbxi_init_ref(&sc->imp->refcount, UFBXI_MESH_IMP_MAGIC, parent);

	sc->imp->magic = UFBXI_MESH_IMP_MAGIC;
	sc->imp->mesh = sc->dst_mesh;
	sc->imp->refcount.ator = sc->ator_result;
	sc->imp->refcount.buf = sc->result;
	sc->imp->mesh.subdivision_evaluated = true;

	return 1;
}

ufbxi_noinline static ufbx_mesh *ufbxi_subdivide_mesh(const ufbx_mesh *mesh, size_t level, const ufbx_subdivide_opts *user_opts, ufbx_error *p_error)
{
	ufbxi_subdivide_context sc = { 0 };
	if (user_opts) {
		sc.opts = *user_opts;
	}

	sc.src_mesh_ptr = (ufbx_mesh*)mesh;
	sc.src_mesh = *mesh;

	int ok = ufbxi_subdivide_mesh_imp(&sc, level);

	ufbxi_free(&sc.ator_tmp, ufbxi_subdivide_input, sc.inputs, sc.inputs_cap);
	ufbxi_buf_free(&sc.tmp);
	ufbxi_buf_free(&sc.source);

	if (ok) {
		ufbxi_free_ator(&sc.ator_tmp);
		if (p_error) {
			ufbxi_clear_error(p_error);
		}

		ufbxi_mesh_imp *imp = sc.imp;
		return &imp->mesh;
	} else {
		ufbxi_fix_error_type(&sc.error, "Failed to subdivide", p_error);
		ufbxi_buf_free(&sc.result);
		ufbxi_free_ator(&sc.ator_tmp);
		ufbxi_free_ator(&sc.ator_result);
		return NULL;
	}
}

#else

ufbxi_noinline static ufbx_mesh *ufbxi_subdivide_mesh(const ufbx_mesh *mesh, size_t level, const ufbx_subdivide_opts *user_opts, ufbx_error *p_error)
{
	if (p_error) {
		memset(p_error, 0, sizeof(ufbx_error));
		ufbxi_fmt_err_info(p_error, "UFBX_ENABLE_SUBDIVISION");
		ufbxi_report_err_msg(p_error, "UFBXI_FEATURE_SUBDIVISION", "Feature disabled");
	}
	return NULL;
}

#endif

// -- Utility

#if UFBXI_FEATURE_INDEX_GENERATION

static int ufbxi_map_cmp_vertex(void *user, const void *va, const void *vb)
{
	size_t size = *(size_t*)user;
#if defined(UFBX_REGRESSION)
	ufbx_assert(size % 8 == 0);
#endif
	for (size_t i = 0; i < size; i += 8) {
		uint64_t a = *(const uint64_t*)((const char*)va + i);
		uint64_t b = *(const uint64_t*)((const char*)vb + i);
		if (a != b) return a < b ? -1 : +1;
	}
	return 0;
}

typedef struct {
	char *begin, *ptr;
	size_t vertex_size;
	size_t packed_offset;
} ufbxi_vertex_stream;

static ufbxi_noinline size_t ufbxi_generate_indices(const ufbx_vertex_stream *user_streams, size_t num_streams, uint32_t *indices, size_t num_indices, const ufbx_allocator_opts *allocator, ufbx_error *error)
{
	bool fail = false;

	ufbxi_allocator ator = { 0 };
	ufbxi_init_ator(error, &ator, allocator, "allocator");

	ufbxi_vertex_stream local_streams[16]; // ufbxi_uninit
	uint64_t local_packed_vertex[64]; // ufbxi_uninit

	ufbxi_vertex_stream *streams = NULL;
	if (num_streams > ufbxi_arraycount(local_streams)) {
		streams = ufbxi_alloc(&ator, ufbxi_vertex_stream, num_streams);
		if (!streams) fail = true;
	} else {
		streams = local_streams;
	}

	size_t packed_size = 0;
	if (!fail) {
		for (size_t i = 0; i < num_streams; i++) {
			if (user_streams[i].vertex_count < num_indices) {
				ufbxi_fmt_err_info(error, "%zu", i);
				ufbxi_report_err_msg(error, "user_streams[i].vertex_count < num_indices", "Truncated vertex stream");
				fail = true;
				break;
			}

			size_t vertex_size = user_streams[i].vertex_size;
			size_t align = ufbxi_size_align_mask(vertex_size);
			packed_size = ufbxi_align_to_mask(packed_size, align);
			streams[i].ptr = streams[i].begin = (char*)user_streams[i].data;
			streams[i].vertex_size = vertex_size;
			streams[i].packed_offset = packed_size;
			packed_size += vertex_size;
		}
		packed_size = ufbxi_align_to_mask(packed_size, 7);
	}

	if (!fail && packed_size == 0) {
		ufbxi_report_err_msg(error, "packed_size != 0", "Zero vertex size");
		fail = true;
	}

	char *packed_vertex = NULL;
	if (!fail) {
		if (packed_size > sizeof(local_packed_vertex)) {
			ufbx_assert(packed_size % 8 == 0);
			packed_vertex = (char*)ufbxi_alloc(&ator, uint64_t, packed_size / 8);
			if (!packed_vertex) fail = true;
		} else {
			packed_vertex = (char*)local_packed_vertex;
		}
	}

	ufbxi_map map = { 0 };
	ufbxi_map_init(&map, &ator, &ufbxi_map_cmp_vertex, &packed_size);

	if (num_indices > 0 && !ufbxi_map_grow_size(&map, packed_size, num_indices)) {
		fail = true;
	}

	if (!fail) {
		ufbx_assert(packed_vertex != NULL);
		memset(packed_vertex, 0, packed_size);

		for (size_t i = 0; i < num_indices; i++) {
			for (size_t si = 0; si < num_streams; si++) {
				size_t size = streams[si].vertex_size, offset = streams[si].packed_offset;
				char *ptr = streams[si].ptr;
				memcpy(packed_vertex + offset, ptr, size);
				streams[si].ptr = ptr + size;
			}

			uint32_t hash = ufbxi_hash_string(packed_vertex, packed_size);
			void *entry = ufbxi_map_find_size(&map, packed_size, hash, packed_vertex);
			if (!entry) {
				entry = ufbxi_map_insert_size(&map, packed_size, hash, packed_vertex);
				if (!entry) {
					fail = true;
					break;
				}
				memcpy(entry, packed_vertex, packed_size);
			}
			uint32_t index = (uint32_t)(ufbxi_to_size((char*)entry - (char*)map.items) / packed_size);
			indices[i] = index;
		}
	}

	size_t result_vertices = 0;
	if (!fail) {
		result_vertices = map.size;

		for (size_t si = 0; si < num_streams; si++) {
			size_t vertex_size = streams[si].vertex_size;
			char *dst = streams[si].begin;
			char *src = ufbxi_add_ptr((char*)map.items, streams[si].packed_offset);
			for (size_t i = 0; i < result_vertices; i++) {
				memcpy(dst, src, vertex_size);
				dst += vertex_size;
				src += packed_size;
			}
		}

		ufbxi_clear_error(error);
	} else {
		ufbxi_fix_error_type(error, "Failed to generate indices", NULL);
	}

	if (streams && streams != local_streams) {
		ufbxi_free(&ator, ufbxi_vertex_stream, streams, num_streams);
	}
	if (packed_vertex && packed_vertex != (char*)local_packed_vertex) {
		ufbxi_free(&ator, uint64_t, packed_vertex, packed_size / 8);
	}

	ufbxi_map_free(&map);
	ufbxi_free_ator(&ator);

	return result_vertices;
}

#else

static ufbxi_noinline size_t ufbxi_generate_indices(const ufbx_vertex_stream *user_streams, size_t num_streams, uint32_t *indices, size_t num_indices, const ufbx_allocator_opts *allocator, ufbx_error *error)
{
	if (error) {
		memset(error, 0, sizeof(ufbx_error));
		ufbxi_fmt_err_info(error, "UFBX_ENABLE_INDEX_GENERATION");
		ufbxi_report_err_msg(error, "UFBXI_FEATURE_INDEX_GENERATION", "Feature disabled");
	}
	return 0;
}

#endif

static ufbxi_noinline void ufbxi_free_scene_imp(ufbxi_scene_imp *imp)
{
	ufbx_assert(imp->magic == UFBXI_SCENE_IMP_MAGIC);
	ufbxi_buf_free(&imp->string_buf);
}

static ufbxi_noinline void ufbxi_init_ref(ufbxi_refcount *refcount, uint32_t magic, ufbxi_refcount *parent)
{
	if (parent) {
		ufbxi_retain_ref(parent);
	}

	ufbxi_atomic_counter_init(&refcount->refcount);
	refcount->self_magic = UFBXI_REFCOUNT_IMP_MAGIC;
	refcount->type_magic = magic;
	refcount->parent = parent;
}

static ufbxi_noinline void ufbxi_retain_ref(ufbxi_refcount *refcount)
{
	ufbx_assert(refcount->self_magic == UFBXI_REFCOUNT_IMP_MAGIC);
	size_t count = ufbxi_atomic_counter_inc(&refcount->refcount);
	ufbxi_ignore(count);
	ufbx_assert(count < SIZE_MAX / 2);
}

static ufbxi_noinline void ufbxi_release_ref(ufbxi_refcount *refcount)
{
	while (refcount) {
		ufbx_assert(refcount->self_magic == UFBXI_REFCOUNT_IMP_MAGIC);
		if (ufbxi_atomic_counter_dec(&refcount->refcount) > 0) return;
		ufbxi_atomic_counter_free(&refcount->refcount);

		ufbxi_refcount *parent = refcount->parent;
		uint32_t type_magic = refcount->type_magic;

		refcount->self_magic = 0;
		refcount->type_magic = 0;

		// Type-specific cleanup
		switch (type_magic) {
		case UFBXI_SCENE_IMP_MAGIC: ufbxi_free_scene_imp((ufbxi_scene_imp*)refcount); break;
		case UFBXI_CACHE_IMP_MAGIC: ufbxi_free_geometry_cache_imp((ufbxi_geometry_cache_imp*)refcount); break;
		default: break;
		}

		// We need to free `data_buf` last and be careful to copy it to
		// the stack since the `ufbxi_refcount` that contains it is allocated
		// from the same result buffer!
		ufbxi_allocator ator = refcount->ator;
		ufbxi_buf buf = refcount->buf;
		buf.ator = &ator;
		ufbxi_buf_free(&buf);
		ufbxi_free_ator(&ator);

		refcount = parent;
	}
}

static ufbxi_noinline void *ufbxi_uninitialized_options(ufbx_error *p_error)
{
	if (p_error) {
		memset(p_error, 0, sizeof(ufbx_error));
		p_error->type = UFBX_ERROR_UNINITIALIZED_OPTIONS;
		p_error->description.data = "Uninitialized options";
		p_error->description.length = strlen("Uninitialized options");
	}
	return NULL;
}

#define ufbxi_check_opts_ptr(m_type, m_opts, m_error) do { if (m_opts) { \
		uint32_t opts_cleared_to_zero = m_opts->_begin_zero | m_opts->_end_zero; \
		ufbx_assert(opts_cleared_to_zero == 0); \
		if (opts_cleared_to_zero != 0) return (m_type*)ufbxi_uninitialized_options(m_error); \
	} } while (0)

#define ufbxi_check_opts_return(m_value, m_opts, m_error) do { if (m_opts) { \
		uint32_t opts_cleared_to_zero = m_opts->_begin_zero | m_opts->_end_zero; \
		ufbx_assert(opts_cleared_to_zero == 0); \
		if (opts_cleared_to_zero != 0) { \
			ufbxi_uninitialized_options(m_error); \
			return m_value; \
		} \
	} } while (0)

#define ufbxi_check_opts_return_no_error(m_value, m_opts) do { if (m_opts) { \
		uint32_t opts_cleared_to_zero = m_opts->_begin_zero | m_opts->_end_zero; \
		if (opts_cleared_to_zero != 0) return m_value; \
	} } while (0)

// -- API

#ifdef __cplusplus
extern "C" {
#endif

ufbx_abi_data_def const ufbx_string ufbx_empty_string = { ufbxi_empty_char, 0 };
ufbx_abi_data_def const ufbx_blob ufbx_empty_blob = { NULL, 0 };
ufbx_abi_data_def const ufbx_matrix ufbx_identity_matrix = { 1,0,0, 0,1,0, 0,0,1, 0,0,0 };
ufbx_abi_data_def const ufbx_transform ufbx_identity_transform = { {0,0,0}, {0,0,0,1}, {1,1,1} };
ufbx_abi_data_def const ufbx_vec2 ufbx_zero_vec2 = { 0,0 };
ufbx_abi_data_def const ufbx_vec3 ufbx_zero_vec3 = { 0,0,0 };
ufbx_abi_data_def const ufbx_vec4 ufbx_zero_vec4 = { 0,0,0,0 };
ufbx_abi_data_def const ufbx_quat ufbx_identity_quat = { 0,0,0,1 };

ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_right_handed_y_up = {
	UFBX_COORDINATE_AXIS_POSITIVE_X, UFBX_COORDINATE_AXIS_POSITIVE_Y, UFBX_COORDINATE_AXIS_POSITIVE_Z,
};
ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_right_handed_z_up = {
	UFBX_COORDINATE_AXIS_POSITIVE_X, UFBX_COORDINATE_AXIS_POSITIVE_Z, UFBX_COORDINATE_AXIS_NEGATIVE_Y,
};
ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_left_handed_y_up = {
	UFBX_COORDINATE_AXIS_POSITIVE_X, UFBX_COORDINATE_AXIS_POSITIVE_Y, UFBX_COORDINATE_AXIS_NEGATIVE_Z,
};
ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_left_handed_z_up = {
	UFBX_COORDINATE_AXIS_POSITIVE_X, UFBX_COORDINATE_AXIS_POSITIVE_Z, UFBX_COORDINATE_AXIS_POSITIVE_Y,
};

ufbx_abi_data_def const size_t ufbx_element_type_size[UFBX_ELEMENT_TYPE_COUNT] = {
	sizeof(ufbx_unknown),
	sizeof(ufbx_node),
	sizeof(ufbx_mesh),
	sizeof(ufbx_light),
	sizeof(ufbx_camera),
	sizeof(ufbx_bone),
	sizeof(ufbx_empty),
	sizeof(ufbx_line_curve),
	sizeof(ufbx_nurbs_curve),
	sizeof(ufbx_nurbs_surface),
	sizeof(ufbx_nurbs_trim_surface),
	sizeof(ufbx_nurbs_trim_boundary),
	sizeof(ufbx_procedural_geometry),
	sizeof(ufbx_stereo_camera),
	sizeof(ufbx_camera_switcher),
	sizeof(ufbx_marker),
	sizeof(ufbx_lod_group),
	sizeof(ufbx_skin_deformer),
	sizeof(ufbx_skin_cluster),
	sizeof(ufbx_blend_deformer),
	sizeof(ufbx_blend_channel),
	sizeof(ufbx_blend_shape),
	sizeof(ufbx_cache_deformer),
	sizeof(ufbx_cache_file),
	sizeof(ufbx_material),
	sizeof(ufbx_texture),
	sizeof(ufbx_video),
	sizeof(ufbx_shader),
	sizeof(ufbx_shader_binding),
	sizeof(ufbx_anim_stack),
	sizeof(ufbx_anim_layer),
	sizeof(ufbx_anim_value),
	sizeof(ufbx_anim_curve),
	sizeof(ufbx_display_layer),
	sizeof(ufbx_selection_set),
	sizeof(ufbx_selection_node),
	sizeof(ufbx_character),
	sizeof(ufbx_constraint),
	sizeof(ufbx_audio_layer),
	sizeof(ufbx_audio_clip),
	sizeof(ufbx_pose),
	sizeof(ufbx_metadata_object),
};

ufbx_abi bool ufbx_default_open_file(void *user, ufbx_stream *stream, const char *path, size_t path_len, const ufbx_open_file_info *info)
{
	(void)user;
	return ufbx_open_file_ctx(stream, info->context, path, path_len, NULL, NULL);
}

ufbx_abi bool ufbx_open_file(ufbx_stream *stream, const char *path, size_t path_len, const ufbx_open_file_opts *opts, ufbx_error *error)
{
	return ufbx_open_file_ctx(stream, (ufbx_open_file_context)NULL, path, path_len, opts, error);
}

ufbx_abi bool ufbx_open_file_ctx(ufbx_stream *stream, ufbx_open_file_context ctx, const char *path, size_t path_len, const ufbx_open_file_opts *opts, ufbx_error *error)
{
	bool ok = false;
	ufbxi_file_context fc; // ufbxi_uninit
	ufbxi_begin_file_context(&fc, ctx, NULL);
	if (path_len == SIZE_MAX) path_len = strlen(path);
#if !defined(UFBX_NO_STDIO)
	ok = ufbxi_stdio_open(&fc, stream, path, path_len, opts ? opts->filename_null_terminated : false);
#else
	(void)stream;
	(void)path;
	(void)path_len;
	(void)opts;
	ufbxi_fmt_err_info(&fc.error, "UFBX_NO_STDIO");
	ufbxi_report_err_msg(&fc.error, "UFBX_NO_STDIO", "Feature disabled");
#endif
	ufbxi_end_file_context(&fc, error, ok);
	return ok;
}

ufbx_abi bool ufbx_open_memory(ufbx_stream *stream, const void *data, size_t data_size, const ufbx_open_memory_opts *opts, ufbx_error *error)
{
	return ufbx_open_memory_ctx(stream, (ufbx_open_file_context)NULL, data, data_size, opts, error);
}

ufbx_abi bool ufbx_open_memory_ctx(ufbx_stream *stream, ufbx_open_file_context ctx, const void *data, size_t data_size, const ufbx_open_memory_opts *opts, ufbx_error *error)
{
	ufbx_open_memory_opts local_opts; // ufbxi_uninit
	if (!opts) {
		memset(&local_opts, 0, sizeof(local_opts));
		opts = &local_opts;
	}
	ufbx_assert(opts->_begin_zero == 0 && opts->_end_zero == 0);

	ufbxi_file_context fc; // ufbxi_uninit
	ufbxi_begin_file_context(&fc, ctx, &opts->allocator);

	size_t copy_size = opts->no_copy ? 0 : data_size;

	// Align the allocation size to 8 bytes to make sure the header is aligned.
	size_t self_size = ufbxi_align_to_mask(sizeof(ufbxi_memory_stream) + copy_size, 7);

	void *memory = ufbxi_alloc(&fc.ator, char, self_size);
	if (!memory) {
		ufbxi_end_file_context(&fc, error, false);
		return false;
	}

	ufbxi_memory_stream *mem = (ufbxi_memory_stream*)memory;
	memset(mem, 0, sizeof(ufbxi_memory_stream));

	mem->size = data_size;
	mem->self_size = self_size;
	mem->close_cb = opts->close_cb;

	if (opts->no_copy) {
		mem->data = data;
	} else {
		memcpy(mem->data_copy, data, data_size);
		mem->data = mem->data_copy;
	}

	// Transplant the allocator in the result blob
	if (fc.parent_ator) {
		mem->parent_ator = fc.parent_ator;
	} else {
		fc.parent_ator = &mem->local_ator;
	}

	stream->read_fn = ufbxi_memory_read;
	stream->skip_fn = ufbxi_memory_skip;
	stream->size_fn = ufbxi_memory_size;
	stream->close_fn = ufbxi_memory_close;
	stream->user = mem;

	ufbxi_end_file_context(&fc, error, true);

	return true;
}

ufbx_abi bool ufbx_is_thread_safe(void)
{
	return UFBXI_THREAD_SAFE != 0;
}

ufbx_abi ufbx_scene *ufbx_load_memory(const void *data, size_t size, const ufbx_load_opts *opts, ufbx_error *error)
{
	ufbxi_check_opts_ptr(ufbx_scene, opts, error);
	ufbxi_context uc; // ufbxi_uninit
	memset(&uc, 0, sizeof(ufbxi_context));
	uc.data_begin = uc.data = (const char *)data;
	uc.data_size = size;
	uc.progress_bytes_total = size;
	return ufbxi_load(&uc, opts, error);
}

ufbx_abi ufbx_scene *ufbx_load_file(const char *filename, const ufbx_load_opts *opts, ufbx_error *error)
{
	return ufbx_load_file_len(filename, SIZE_MAX, opts, error);
}

ufbx_abi ufbx_scene *ufbx_load_file_len(const char *filename, size_t filename_len, const ufbx_load_opts *opts, ufbx_error *error)
{
	ufbxi_check_opts_ptr(ufbx_scene, opts, error);
	ufbxi_context uc; // ufbxi_uninit
	memset(&uc, 0, sizeof(ufbxi_context));
	uc.deferred_load = true;
	uc.load_filename = filename;
	uc.load_filename_len = filename_len;
	return ufbxi_load(&uc, opts, error);
}

ufbx_abi ufbx_scene *ufbx_load_stdio(void *file_void, const ufbx_load_opts *opts, ufbx_error *error)
{
	return ufbx_load_stdio_prefix(file_void, NULL, 0, opts, error);
}

ufbx_abi ufbx_scene *ufbx_load_stdio_prefix(void *file_void, const void *prefix, size_t prefix_size, const ufbx_load_opts *opts, ufbx_error *error)
{
#if !defined(UFBX_NO_STDIO)
	if (!file_void) return NULL;
	ufbx_stream stream = { 0 };
	ufbxi_stdio_init(&stream, file_void, false);
	return ufbx_load_stream_prefix(&stream, prefix, prefix_size, opts, error);
#else
	(void)file_void;
	(void)prefix;
	(void)prefix_size;
	(void)opts;

	ufbxi_context uc; // ufbxi_uninit
	memset(&uc, 0, sizeof(ufbxi_context));
	ufbxi_fmt_err_info(&uc.error, "UFBX_NO_STDIO");
	ufbxi_report_err_msg(&uc.error, "UFBX_NO_STDIO", "Feature disabled");
	uc.deferred_failure = true;
	return ufbxi_load(&uc, NULL, error);
#endif
}

ufbx_abi ufbx_scene *ufbx_load_stream(const ufbx_stream *stream, const ufbx_load_opts *opts, ufbx_error *error)
{
	return ufbx_load_stream_prefix(stream, NULL, 0, opts, error);
}

ufbx_abi ufbx_scene *ufbx_load_stream_prefix(const ufbx_stream *stream, const void *prefix, size_t prefix_size, const ufbx_load_opts *opts, ufbx_error *error)
{
	ufbxi_check_opts_ptr(ufbx_scene, opts, error);
	ufbxi_context uc; // ufbxi_uninit
	memset(&uc, 0, sizeof(ufbxi_context));
	uc.data_begin = uc.data = (const char *)prefix;
	uc.data_size = prefix_size;
	uc.read_fn = stream->read_fn;
	uc.skip_fn = stream->skip_fn;
	uc.size_fn = stream->size_fn;
	uc.close_fn = stream->close_fn;
	uc.read_user = stream->user;

	ufbx_scene *scene = ufbxi_load(&uc, opts, error);
	return scene;
}

ufbx_abi void ufbx_free_scene(ufbx_scene *scene)
{
	if (!scene) return;

	ufbxi_scene_imp *imp = ufbxi_get_imp(ufbxi_scene_imp, scene);
	ufbx_assert(imp->magic == UFBXI_SCENE_IMP_MAGIC);
	if (imp->magic != UFBXI_SCENE_IMP_MAGIC) return;
	ufbxi_release_ref(&imp->refcount);
}

ufbx_abi void ufbx_retain_scene(ufbx_scene *scene)
{
	if (!scene) return;

	ufbxi_scene_imp *imp = ufbxi_get_imp(ufbxi_scene_imp, scene);
	ufbx_assert(imp->magic == UFBXI_SCENE_IMP_MAGIC);
	if (imp->magic != UFBXI_SCENE_IMP_MAGIC) return;
	ufbxi_retain_ref(&imp->refcount);
}

ufbx_abi ufbxi_noinline size_t ufbx_format_error(char *dst, size_t dst_size, const ufbx_error *error)
{
	if (!dst || !dst_size) return 0;
	if (!error) {
		*dst = '\0';
		return 0;
	}

	size_t offset = 0;

	{
		int num;
		if (error->info_length > 0 && error->info_length < UFBX_ERROR_INFO_LENGTH) {
			num = ufbxi_snprintf(dst + offset, dst_size - offset, "ufbx v%u.%u.%u error: %s (%.*s)\n",
				UFBX_SOURCE_VERSION/1000000, UFBX_SOURCE_VERSION/1000%1000, UFBX_SOURCE_VERSION%1000,
				error->description.data ? error->description.data : "Unknown error",
				(int)error->info_length, error->info);
		} else {
			num = ufbxi_snprintf(dst + offset, dst_size - offset, "ufbx v%u.%u.%u error: %s\n",
				UFBX_SOURCE_VERSION/1000000, UFBX_SOURCE_VERSION/1000%1000, UFBX_SOURCE_VERSION%1000,
				error->description.data ? error->description.data : "Unknown error");
		}

		if (num > 0) offset = ufbxi_min_sz(offset + (size_t)num, dst_size - 1);
	}

	size_t stack_size = ufbxi_min_sz(error->stack_size, UFBX_ERROR_STACK_MAX_DEPTH);
	int line_width = 6;
	for (size_t i = 0; i < stack_size; i++) {
		const ufbx_error_frame *frame = &error->stack[i];
		int num = ufbxi_snprintf(dst + offset, dst_size - offset, "%*u:%s: %s\n", line_width, frame->source_line, frame->function.data, frame->description.data);
		if (num > 0) offset = ufbxi_min_sz(offset + (size_t)num, dst_size - 1);
	}

	return offset;
}

ufbx_abi ufbx_prop *ufbx_find_prop_len(const ufbx_props *props, const char *name, size_t name_len)
{
	uint32_t key = ufbxi_get_name_key(name, name_len);
	ufbx_string name_str = ufbxi_safe_string(name, name_len);

	while (props) {
		size_t index = SIZE_MAX;
		ufbxi_macro_lower_bound_eq(ufbx_prop, 4, &index, props->props.data, 0, props->props.count,
			( ufbxi_cmp_prop_less_ref(a, name_str, key) ), ( a->_internal_key == key && ufbxi_str_equal(a->name, name_str) ));
		if (index != SIZE_MAX) return &props->props.data[index];

		props = props->defaults;
	}

	return NULL;
}

ufbx_abi ufbx_real ufbx_find_real_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_real def)
{
	ufbx_prop *prop = ufbx_find_prop_len(props, name, name_len);
	if (prop) {
		return prop->value_real;
	} else {
		return def;
	}
}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_find_vec3_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_vec3 def)
{
	ufbx_prop *prop = ufbx_find_prop_len(props, name, name_len);
	if (prop) {
		return prop->value_vec3;
	} else {
		return def;
	}
}

ufbx_abi ufbxi_noinline int64_t ufbx_find_int_len(const ufbx_props *props, const char *name, size_t name_len, int64_t def)
{
	ufbx_prop *prop = ufbx_find_prop_len(props, name, name_len);
	if (prop) {
		return prop->value_int;
	} else {
		return def;
	}
}

ufbx_abi bool ufbx_find_bool_len(const ufbx_props *props, const char *name, size_t name_len, bool def)
{
	ufbx_prop *prop = ufbx_find_prop_len(props, name, name_len);
	if (prop) {
		return prop->value_int != 0;
	} else {
		return def;
	}
}

ufbx_abi ufbxi_noinline ufbx_string ufbx_find_string_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_string def)
{
	ufbx_prop *prop = ufbx_find_prop_len(props, name, name_len);
	if (prop) {
		return prop->value_str;
	} else {
		return def;
	}
}

ufbx_abi ufbx_blob ufbx_find_blob_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_blob def)
{
	ufbx_prop *prop = ufbx_find_prop_len(props, name, name_len);
	if (prop) {
		return prop->value_blob;
	} else {
		return def;
	}
}

ufbx_abi ufbx_prop *ufbx_find_prop_concat(const ufbx_props *props, const ufbx_string *parts, size_t num_parts)
{
	uint32_t key = ufbxi_get_concat_key(parts, num_parts);

	while (props) {
		size_t index = SIZE_MAX;

		ufbxi_macro_lower_bound_eq(ufbx_prop, 2, &index, props->props.data, 0, props->props.count,
			( ufbxi_cmp_prop_less_concat(a, parts, num_parts, key) ),
			( a->_internal_key == key && ufbxi_concat_str_cmp(&a->name, parts, num_parts) == 0 ));
		if (index != SIZE_MAX) return &props->props.data[index];

		props = props->defaults;
	}

	return NULL;
}

ufbx_abi ufbx_element *ufbx_find_element_len(const ufbx_scene *scene, ufbx_element_type type, const char *name, size_t name_len)
{
	if (!scene) return NULL;
	ufbx_string name_str = ufbxi_safe_string(name, name_len);
	uint32_t key = ufbxi_get_name_key(name, name_len);

	size_t index = SIZE_MAX;
	ufbxi_macro_lower_bound_eq(ufbx_name_element, 16, &index, scene->elements_by_name.data, 0, scene->elements_by_name.count,
		( ufbxi_cmp_name_element_less_ref(a, name_str, type, key) ), ( ufbxi_str_equal(a->name, name_str) && a->type == type ));

	return index < SIZE_MAX ? scene->elements_by_name.data[index].element : NULL;
}

ufbx_abi ufbx_element *ufbx_get_prop_element(const ufbx_element *element, const ufbx_prop *prop, ufbx_element_type type)
{
	ufbx_assert(element && prop);
	if (!element || !prop) return NULL;
	return ufbxi_fetch_dst_element((ufbx_element*)element, false, prop->name.data, type);
}

ufbx_abi ufbx_element *ufbx_find_prop_element_len(const ufbx_element *element, const char *name, size_t name_len, ufbx_element_type type)
{
	const ufbx_prop *prop = ufbx_find_prop_len(&element->props, name, name_len);
	if (prop) {
		return ufbx_get_prop_element(element, prop, type);
	} else {
		return NULL;
	}
}

ufbx_abi ufbx_node *ufbx_find_node_len(const ufbx_scene *scene, const char *name, size_t name_len)
{
	return (ufbx_node*)ufbx_find_element_len(scene, UFBX_ELEMENT_NODE, name, name_len);
}

ufbx_abi ufbx_anim_stack *ufbx_find_anim_stack_len(const ufbx_scene *scene, const char *name, size_t name_len)
{
	return (ufbx_anim_stack*)ufbx_find_element_len(scene, UFBX_ELEMENT_ANIM_STACK, name, name_len);
}

ufbx_abi ufbx_material *ufbx_find_material_len(const ufbx_scene *scene, const char *name, size_t name_len)
{
	return (ufbx_material*)ufbx_find_element_len(scene, UFBX_ELEMENT_MATERIAL, name, name_len);
}

ufbx_abi ufbx_anim_prop *ufbx_find_anim_prop_len(const ufbx_anim_layer *layer, const ufbx_element *element, const char *prop, size_t prop_len)
{
	ufbx_assert(layer);
	ufbx_assert(element);
	if (!layer || !element) return NULL;

	ufbx_string prop_str = ufbxi_safe_string(prop, prop_len);

	size_t index = SIZE_MAX;
	ufbxi_macro_lower_bound_eq(ufbx_anim_prop, 16, &index, layer->anim_props.data, 0, layer->anim_props.count,
		( a->element != element ? a->element < element : ufbxi_str_less(a->prop_name, prop_str) ),
		( a->element == element && ufbxi_str_equal(a->prop_name, prop_str) ));

	if (index == SIZE_MAX) return NULL;
	return &layer->anim_props.data[index];
}

ufbx_abi ufbxi_noinline ufbx_anim_prop_list ufbx_find_anim_props(const ufbx_anim_layer *layer, const ufbx_element *element)
{
	ufbx_anim_prop_list result = { 0 };
	ufbx_assert(layer);
	ufbx_assert(element);
	if (!layer || !element) return result;

	size_t begin = layer->anim_props.count, end = begin;
	ufbxi_macro_lower_bound_eq(ufbx_anim_prop, 16, &begin, layer->anim_props.data, 0, layer->anim_props.count,
		( a->element < element ), ( a->element == element ));

	ufbxi_macro_upper_bound_eq(ufbx_anim_prop, 16, &end, layer->anim_props.data, begin, layer->anim_props.count,
		( a->element == element ));

	if (begin != end) {
		result.data = layer->anim_props.data + begin;
		result.count = end - begin;
	}

	return result;
}

ufbx_abi ufbxi_noinline ufbx_matrix ufbx_get_compatible_matrix_for_normals(const ufbx_node *node)
{
	if (!node) return ufbx_identity_matrix;

	ufbx_transform geom_rot = ufbx_identity_transform;
	geom_rot.rotation = node->geometry_transform.rotation;
	ufbx_matrix geom_rot_mat = ufbx_transform_to_matrix(&geom_rot);

	ufbx_matrix norm_mat = ufbx_matrix_mul(&node->node_to_world, &geom_rot_mat);
	norm_mat = ufbx_matrix_for_normals(&norm_mat);
	return norm_mat;
}

ufbx_abi ufbx_real ufbx_evaluate_curve(const ufbx_anim_curve *curve, double time, ufbx_real default_value)
{
	if (!curve) return default_value;
	if (curve->keyframes.count <= 1) {
		if (curve->keyframes.count == 1) {
			return curve->keyframes.data[0].value;
		} else {
			return default_value;
		}
	}

	size_t begin = 0;
	size_t end = curve->keyframes.count;
	const ufbx_keyframe *keys = curve->keyframes.data;
	while (end - begin >= 8) {
		size_t mid = (begin + end) >> 1;
		if (keys[mid].time <= time) {
			begin = mid + 1;
		} else {
			end = mid;
		}
	}

	end = curve->keyframes.count;
	for (; begin < end; begin++) {
		const ufbx_keyframe *next = &keys[begin];
		if (next->time <= time) continue;

		// First keyframe
		if (begin == 0) return next->value;

		const ufbx_keyframe *prev = next - 1;

		// Exact keyframe
		if (prev->time == time) return prev->value;

		double rcp_delta = 1.0 / (next->time - prev->time);
		double t = (time - prev->time) * rcp_delta;

		switch (prev->interpolation) {

		case UFBX_INTERPOLATION_CONSTANT_PREV:
			return prev->value;

		case UFBX_INTERPOLATION_CONSTANT_NEXT:
			return next->value;

		case UFBX_INTERPOLATION_LINEAR:
			return (ufbx_real)(prev->value*(1.0 - t) + next->value*t);

		case UFBX_INTERPOLATION_CUBIC:
		{
			double x1 = prev->right.dx * rcp_delta;
			double x2 = 1.0 - next->left.dx * rcp_delta;
			t = ufbxi_find_cubic_bezier_t(x1, x2, t);

			double t2 = t*t, t3 = t2*t;
			double u = 1.0 - t, u2 = u*u, u3 = u2*u;

			double y0 = prev->value;
			double y3 = next->value;
			double y1 = y0 + prev->right.dy;
			double y2 = y3 - next->left.dy;

			return (ufbx_real)(u3*y0 + 3.0 * (u2*t*y1 + u*t2*y2) + t3*y3);
		}

		default:
			ufbxi_unreachable("Bad interpolation mode");
			return 0.0f;

		}
	}

	// Last keyframe
	return curve->keyframes.data[curve->keyframes.count - 1].value;
}

ufbx_abi ufbxi_noinline ufbx_real ufbx_evaluate_anim_value_real(const ufbx_anim_value *anim_value, double time)
{
	if (!anim_value) {
		return 0.0f;
	}

	ufbx_real res = anim_value->default_value.x;
	if (anim_value->curves[0]) res = ufbx_evaluate_curve(anim_value->curves[0], time, res);
	return res;
}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_evaluate_anim_value_vec3(const ufbx_anim_value *anim_value, double time)
{
	if (!anim_value) {
		ufbx_vec3 zero = { 0.0f };
		return zero;
	}

	ufbx_vec3 res = anim_value->default_value;
	if (anim_value->curves[0]) res.x = ufbx_evaluate_curve(anim_value->curves[0], time, res.x);
	if (anim_value->curves[1]) res.y = ufbx_evaluate_curve(anim_value->curves[1], time, res.y);
	if (anim_value->curves[2]) res.z = ufbx_evaluate_curve(anim_value->curves[2], time, res.z);
	return res;
}

ufbx_abi ufbxi_noinline ufbx_prop ufbx_evaluate_prop_len(const ufbx_anim *anim, const ufbx_element *element, const char *name, size_t name_len, double time)
{
	ufbx_prop result;

	ufbx_prop *prop = ufbx_find_prop_len(&element->props, name, name_len);
	if (prop) {
		result = *prop;
	} else {
		memset(&result, 0, sizeof(result));
		result.name.data = name;
		result.name.length = name_len;
		result._internal_key = ufbxi_get_name_key(name, name_len);
		result.flags = UFBX_PROP_FLAG_NOT_FOUND;
		result.value_str.data = ufbxi_empty_char;
		result.value_str.length = 0;
		result.value_blob.data = NULL;
		result.value_blob.size = 0;
	}

	if (anim->prop_overrides.count > 0) {
		ufbxi_find_prop_override(&anim->prop_overrides, element->element_id, &result);
		return result;
	}

	if ((result.flags & (UFBX_PROP_FLAG_ANIMATED|UFBX_PROP_FLAG_CONNECTED)) == 0) return result;

	if ((prop->flags & UFBX_PROP_FLAG_CONNECTED) != 0 && !anim->ignore_connections) {
		ufbxi_evaluate_connected_prop(&result, anim, element, prop->name.data, time);
	}

	ufbxi_evaluate_props(anim, element, time, &result, 1);

	return result;
}

ufbx_abi ufbxi_noinline ufbx_props ufbx_evaluate_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *buffer, size_t buffer_size)
{
	ufbx_props ret = { NULL };
	if (!element) return ret;

	size_t num_anim = 0;
	ufbxi_prop_iter iter; // ufbxi_uninit
	ufbxi_init_prop_iter(&iter, anim, element);
	const ufbx_prop *prop = NULL;
	while ((prop = ufbxi_next_prop(&iter)) != NULL) {
		if (!(prop->flags & (UFBX_PROP_FLAG_ANIMATED|UFBX_PROP_FLAG_OVERRIDDEN|UFBX_PROP_FLAG_CONNECTED))) continue;
		if (num_anim >= buffer_size) break;

		ufbx_prop *dst = &buffer[num_anim++];
		*dst = *prop;

		if ((prop->flags & UFBX_PROP_FLAG_CONNECTED) != 0 && !anim->ignore_connections) {
			ufbxi_evaluate_connected_prop(dst, anim, element, prop->name.data, time);
		}
	}

	ufbxi_evaluate_props(anim, element, time, buffer, num_anim);

	ret.props.data = buffer;
	ret.props.count = ret.num_animated = num_anim;
	ret.defaults = (ufbx_props*)&element->props;
	return ret;
}

ufbx_abi ufbxi_noinline ufbx_transform ufbx_evaluate_transform(const ufbx_anim *anim, const ufbx_node *node, double time)
{
	return ufbx_evaluate_transform_flags(anim, node, time, 0);
}

static const char *const ufbxi_transform_props_all[] = {
	ufbxi_Lcl_Rotation,
	ufbxi_Lcl_Scaling,
	ufbxi_Lcl_Translation,
	ufbxi_PostRotation,
	ufbxi_PreRotation,
	ufbxi_RotationOffset,
	ufbxi_RotationOrder,
	ufbxi_RotationPivot,
	ufbxi_ScalingOffset,
	ufbxi_ScalingPivot,
};

static const char *const ufbxi_transform_props_rotation[] = {
	ufbxi_Lcl_Rotation,
	ufbxi_PostRotation,
	ufbxi_PreRotation,
	ufbxi_RotationOrder,
};

static const char *const ufbxi_transform_props_scale[] = {
	ufbxi_Lcl_Scaling,
};

static const char *const ufbxi_transform_props_rotation_scale[] = {
	ufbxi_Lcl_Rotation,
	ufbxi_Lcl_Scaling,
	ufbxi_PostRotation,
	ufbxi_PreRotation,
	ufbxi_RotationOrder,
};

ufbx_abi ufbxi_noinline ufbx_transform ufbx_evaluate_transform_flags(const ufbx_anim *anim, const ufbx_node *node, double time, uint32_t flags)
{
	ufbx_assert(anim);
	ufbx_assert(node);
	if (!node) return ufbx_identity_transform;
	if (!anim) return node->local_transform;
	if (node->is_root) return node->local_transform;

	if ((flags & UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES) == 0) {
		flags |= UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION|UFBX_TRANSFORM_FLAG_INCLUDE_SCALE|UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION;
	}

	const char *const *prop_names = ufbxi_transform_props_all;
	size_t num_prop_names = ufbxi_arraycount(ufbxi_transform_props_all);
	uint32_t components = flags & (UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION|UFBX_TRANSFORM_FLAG_INCLUDE_SCALE|UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION);
	if (components == (UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION|UFBX_TRANSFORM_FLAG_INCLUDE_SCALE)) {
		prop_names = ufbxi_transform_props_rotation_scale;
		num_prop_names = ufbxi_arraycount(ufbxi_transform_props_rotation_scale);
	} else if (components == UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION) {
		prop_names = ufbxi_transform_props_rotation;
		num_prop_names = ufbxi_arraycount(ufbxi_transform_props_rotation);
	} else if (components == UFBX_TRANSFORM_FLAG_INCLUDE_SCALE) {
		prop_names = ufbxi_transform_props_scale;
		num_prop_names = ufbxi_arraycount(ufbxi_transform_props_scale);
	} else if (components == 0) {
		return ufbx_identity_transform;
	}

	const ufbx_vec3 *translation_scale = NULL;
	ufbx_prop helper_scale; // ufbxi_uninit
	ufbx_vec3 scale_factor = ufbxi_one_vec3;
	bool use_scale_factor = false;

	if (node->parent && (flags & (UFBX_TRANSFORM_FLAG_INCLUDE_SCALE|UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION)) != 0) {
		ufbx_node *parent = node->parent;

		if ((flags & UFBX_TRANSFORM_FLAG_IGNORE_COMPONENTWISE_SCALE) == 0 && parent->inherit_scale_node) {
			ufbx_node *p = parent->inherit_scale_node;

			if (node->is_scale_helper) {
				use_scale_factor = true;
			}

			while (p && p->scale_helper) {
				ufbx_prop scale = ufbx_evaluate_prop(anim, &p->scale_helper->element, ufbxi_Lcl_Scaling, time);
				scale_factor.x *= scale.value_vec3.x;
				scale_factor.y *= scale.value_vec3.y;
				scale_factor.z *= scale.value_vec3.z;
				p = p->inherit_scale_node;
			}
		}

		if (parent->scale_helper && (flags & UFBX_TRANSFORM_FLAG_IGNORE_SCALE_HELPER) == 0) {
			helper_scale = ufbx_evaluate_prop(anim, &parent->scale_helper->element, ufbxi_Lcl_Scaling, time);
			if (helper_scale.flags & UFBX_PROP_FLAG_NOT_FOUND) {
				helper_scale.value_vec3.x = 1.0f;
				helper_scale.value_vec3.y = 1.0f;
				helper_scale.value_vec3.z = 1.0f;
			}
			helper_scale.value_vec3.x *= scale_factor.x;
			helper_scale.value_vec3.y *= scale_factor.y;
			helper_scale.value_vec3.z *= scale_factor.z;
			translation_scale = &helper_scale.value_vec3;
		}
	}

	ufbx_prop buf[ufbxi_arraycount(ufbxi_transform_props_all)]; // ufbxi_uninit
	ufbx_props props = ufbxi_evaluate_selected_props(anim, &node->element, time, buf, prop_names, num_prop_names);
	ufbx_rotation_order order = (ufbx_rotation_order)ufbxi_find_enum(&props, ufbxi_RotationOrder, UFBX_ROTATION_ORDER_XYZ, UFBX_ROTATION_ORDER_SPHERIC);

	ufbx_transform transform; // ufbxi_uninit
	if ((components & UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION) != 0) {
		transform = ufbxi_get_transform(&props, order, node, translation_scale);
	} else {
		transform.translation = ufbx_zero_vec3;
		if ((components & UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION) != 0) {
			transform.rotation = ufbxi_get_rotation(&props, order, node);
		} else {
			transform.rotation = ufbx_identity_quat;
		}
		if ((components & UFBX_TRANSFORM_FLAG_INCLUDE_SCALE) != 0) {
			transform.scale = ufbxi_get_scale(&props, node);
		} else {
			transform.scale = ufbxi_one_vec3;
		}
	}

	if (use_scale_factor) {
		transform.scale.x *= scale_factor.x;
		transform.scale.y *= scale_factor.y;
		transform.scale.z *= scale_factor.z;
	}
	return transform;
}

ufbx_abi ufbx_real ufbx_evaluate_blend_weight(const ufbx_anim *anim, const ufbx_blend_channel *channel, double time)
{
	const char *prop_names[] = {
		ufbxi_DeformPercent,
	};

	ufbx_prop buf[ufbxi_arraycount(prop_names)]; // ufbxi_uninit
	ufbx_props props = ufbxi_evaluate_selected_props(anim, &channel->element, time, buf, prop_names, ufbxi_arraycount(prop_names));
	return ufbxi_find_real(&props, ufbxi_DeformPercent, channel->weight * (ufbx_real)100.0) * (ufbx_real)0.01;
}

ufbx_abi ufbx_scene *ufbx_evaluate_scene(const ufbx_scene *scene, const ufbx_anim *anim, double time, const ufbx_evaluate_opts *opts, ufbx_error *error)
{
	ufbxi_check_opts_ptr(ufbx_scene, opts, error);
#if UFBXI_FEATURE_SCENE_EVALUATION
	ufbxi_eval_context ec = { 0 };
	return ufbxi_evaluate_scene(&ec, (ufbx_scene*)scene, anim, time, opts, error);
#else
	if (error) {
		memset(error, 0, sizeof(ufbx_error));
		ufbxi_fmt_err_info(error, "UFBX_ENABLE_SCENE_EVALUATION");
		ufbxi_report_err_msg(error, "UFBXI_FEATURE_SCENE_EVALUATION", "Feature disabled");
	}
	return NULL;
#endif
}

ufbx_abi ufbx_anim *ufbx_create_anim(const ufbx_scene *scene, const ufbx_anim_opts *opts, ufbx_error *error)
{
	ufbxi_check_opts_ptr(ufbx_anim, opts, error);
	ufbx_assert(scene);

	ufbxi_create_anim_context ac = { UFBX_ERROR_NONE };
	if (opts) {
		ac.opts = *opts;
	}

	ac.scene = scene;

	int ok = ufbxi_create_anim_imp(&ac);

	if (ok) {
		ufbxi_clear_error(error);
		ufbxi_anim_imp *imp = ac.imp;
		return &imp->anim;
	} else {
		ufbxi_fix_error_type(&ac.error, "Failed to create anim", error);
		ufbxi_buf_free(&ac.result);
		ufbxi_free_ator(&ac.ator_result);
		return NULL;
	}
}

ufbx_abi void ufbx_free_anim(ufbx_anim *anim)
{
	if (!anim) return;
	if (!anim->custom) return;

	ufbxi_anim_imp *imp = ufbxi_get_imp(ufbxi_anim_imp, anim);
	ufbx_assert(imp->magic == UFBXI_ANIM_IMP_MAGIC);
	if (imp->magic != UFBXI_ANIM_IMP_MAGIC) return;
	ufbxi_release_ref(&imp->refcount);
}

ufbx_abi void ufbx_retain_anim(ufbx_anim *anim)
{
	if (!anim) return;
	if (!anim->custom) return;

	ufbxi_anim_imp *imp = ufbxi_get_imp(ufbxi_anim_imp, anim);
	ufbx_assert(imp->magic == UFBXI_ANIM_IMP_MAGIC);
	if (imp->magic != UFBXI_ANIM_IMP_MAGIC) return;
	ufbxi_retain_ref(&imp->refcount);
}

ufbx_abi ufbx_baked_anim *ufbx_bake_anim(const ufbx_scene *scene, const ufbx_anim *anim, const ufbx_bake_opts *opts, ufbx_error *error)
{
	ufbx_assert(scene);
#if UFBXI_FEATURE_ANIMATION_BAKING
	ufbxi_check_opts_ptr(ufbx_baked_anim, opts, error);
	if (!anim) {
		anim = scene->anim;
	}

	ufbxi_bake_context bc = { UFBX_ERROR_NONE };
	if (opts) {
		bc.opts = *opts;
	}

	bc.scene = scene;

	int ok = ufbxi_bake_anim_imp(&bc, anim);

	ufbxi_buf_free(&bc.tmp);
	ufbxi_buf_free(&bc.tmp_prop);
	ufbxi_buf_free(&bc.tmp_times);
	ufbxi_buf_free(&bc.tmp_bake_props);
	ufbxi_buf_free(&bc.tmp_nodes);
	ufbxi_buf_free(&bc.tmp_elements);
	ufbxi_buf_free(&bc.tmp_props);
	ufbxi_buf_free(&bc.tmp_bake_stack);
	ufbxi_free(&bc.ator_tmp, char, bc.tmp_arr, bc.tmp_arr_size);
	ufbxi_free_ator(&bc.ator_tmp);

	if (ok) {
		ufbxi_clear_error(error);
		ufbxi_baked_anim_imp *imp = bc.imp;
		return &imp->bake;
	} else {
		ufbxi_fix_error_type(&bc.error, "Failed to bake anim", error);
		ufbxi_buf_free(&bc.result);
		ufbxi_free_ator(&bc.ator_result);
		return NULL;
	}
#else
	if (error) {
		memset(error, 0, sizeof(ufbx_error));
		ufbxi_fmt_err_info(error, "UFBX_ENABLE_ANIMATION_BAKING");
		ufbxi_report_err_msg(error, "UFBXI_FEATURE_ANIMATION_BAKING", "Feature disabled");
	}
	return NULL;
#endif
}

ufbx_abi void ufbx_retain_baked_anim(ufbx_baked_anim *bake)
{
	if (!bake) return;

	ufbxi_baked_anim_imp *imp = ufbxi_get_imp(ufbxi_baked_anim_imp, bake);
	ufbx_assert(imp->magic == UFBXI_BAKED_ANIM_IMP_MAGIC);
	if (imp->magic != UFBXI_BAKED_ANIM_IMP_MAGIC) return;
	ufbxi_retain_ref(&imp->refcount);
}

ufbx_abi void ufbx_free_baked_anim(ufbx_baked_anim *bake)
{
	if (!bake) return;

	ufbxi_baked_anim_imp *imp = ufbxi_get_imp(ufbxi_baked_anim_imp, bake);
	ufbx_assert(imp->magic == UFBXI_BAKED_ANIM_IMP_MAGIC);
	if (imp->magic != UFBXI_BAKED_ANIM_IMP_MAGIC) return;
	ufbxi_release_ref(&imp->refcount);
}


ufbx_abi ufbx_baked_node *ufbx_find_baked_node_by_typed_id(ufbx_baked_anim *bake, uint32_t typed_id)
{
	size_t index = SIZE_MAX;
	ufbxi_macro_lower_bound_eq(ufbx_baked_node, 8, &index, bake->nodes.data, 0, bake->nodes.count,
		( a->typed_id < typed_id ), ( a->typed_id == typed_id) );
	return index < SIZE_MAX ? &bake->nodes.data[index] : NULL;
}

ufbx_abi ufbx_baked_node *ufbx_find_baked_node(ufbx_baked_anim *bake, ufbx_node *node)
{
	if (!bake || !node) return NULL;
	return ufbx_find_baked_node_by_typed_id(bake, node->typed_id);
}

ufbx_abi ufbx_baked_element *ufbx_find_baked_element_by_element_id(ufbx_baked_anim *bake, uint32_t element_id)
{
	size_t index = SIZE_MAX;
	ufbxi_macro_lower_bound_eq(ufbx_baked_element, 8, &index, bake->elements.data, 0, bake->elements.count,
		( a->element_id < element_id ), ( a->element_id == element_id) );
	return index < SIZE_MAX ? &bake->elements.data[index] : NULL;
}

ufbx_abi ufbx_baked_element *ufbx_find_baked_element(ufbx_baked_anim *bake, ufbx_element *element)
{
	if (!bake || !element) return NULL;
	return ufbx_find_baked_element_by_element_id(bake, element->element_id);
}

ufbx_abi ufbx_vec3 ufbx_evaluate_baked_vec3(ufbx_baked_vec3_list keyframes, double time)
{
	size_t begin = 0;
	size_t end = keyframes.count;
	const ufbx_baked_vec3 *keys = keyframes.data;
	while (end - begin >= 8) {
		size_t mid = (begin + end) >> 1;
		if (keys[mid].time <= time) {
			begin = mid + 1;
		} else {
			end = mid;
		}
	}

	end = keyframes.count;
	for (; begin < end; begin++) {
		const ufbx_baked_vec3 *next = &keys[begin];
		if (next->time <= time) continue;
		if (begin == 0) return next->value;

		const ufbx_baked_vec3 *prev = next - 1;
		if (prev > keys && (prev->flags & UFBX_BAKED_KEY_STEP_RIGHT) != 0 && prev[-1].time == time) prev--;
		if (time == prev->time) return prev->value;
		double t = (time - prev->time) / (next->time - prev->time);
		if (prev->flags & UFBX_BAKED_KEY_STEP_LEFT) t = 0.0;
		if (next->flags & UFBX_BAKED_KEY_STEP_RIGHT) t = 1.0;
		return ufbxi_lerp3(prev->value, next->value, (ufbx_real)t);
	}

	return keyframes.data[keyframes.count - 1].value;
}

ufbx_abi ufbx_quat ufbx_evaluate_baked_quat(ufbx_baked_quat_list keyframes, double time)
{
	size_t begin = 0;
	size_t end = keyframes.count;
	const ufbx_baked_quat *keys = keyframes.data;
	while (end - begin >= 8) {
		size_t mid = (begin + end) >> 1;
		if (keys[mid].time <= time) {
			begin = mid + 1;
		} else {
			end = mid;
		}
	}

	end = keyframes.count;
	for (; begin < end; begin++) {
		const ufbx_baked_quat *next = &keys[begin];
		if (next->time <= time) continue;
		if (begin == 0) return next->value;

		const ufbx_baked_quat *prev = next - 1;
		if (prev > keys && prev[-1].time == time) prev--;
		if (time == prev->time) return prev->value;
		double t = (time - prev->time) / (next->time - prev->time);
		if (prev > keys && (prev->flags & UFBX_BAKED_KEY_STEP_RIGHT) != 0 && prev[-1].time == time) prev--;
		if (prev->flags & UFBX_BAKED_KEY_STEP_LEFT) t = 0.0;
		if (next->flags & UFBX_BAKED_KEY_STEP_RIGHT) t = 1.0;
		return ufbx_quat_slerp(prev->value, next->value, (ufbx_real)t);
	}

	return keyframes.data[keyframes.count - 1].value;
}

ufbx_abi ufbx_bone_pose *ufbx_get_bone_pose(const ufbx_pose *pose, const ufbx_node *node)
{
	if (!pose || !node) return NULL;
	size_t index = SIZE_MAX;
	ufbxi_macro_lower_bound_eq(ufbx_bone_pose, 8, &index, pose->bone_poses.data, 0, pose->bone_poses.count,
		( a->bone_node->typed_id < node->typed_id ), ( a->bone_node == node ));
	return index < SIZE_MAX ? &pose->bone_poses.data[index] : NULL;
}

ufbx_abi ufbx_texture *ufbx_find_prop_texture_len(const ufbx_material *material, const char *name, size_t name_len)
{
	ufbx_string name_str = ufbxi_safe_string(name, name_len);
	if (!material) return NULL;

	size_t index = SIZE_MAX;
	ufbxi_macro_lower_bound_eq(ufbx_material_texture, 4, &index, material->textures.data, 0, material->textures.count,
		( ufbxi_str_less(a->material_prop, name_str) ), ( ufbxi_str_equal(a->material_prop, name_str) ));
	return index < SIZE_MAX ? material->textures.data[index].texture : NULL;
}

ufbx_abi ufbx_string ufbx_find_shader_prop_len(const ufbx_shader *shader, const char *name, size_t name_len)
{
	ufbx_shader_prop_binding_list bindings = ufbx_find_shader_prop_bindings_len(shader, name, name_len);
	if (bindings.count > 0) {
		return bindings.data[0].material_prop;
	}
	return ufbx_empty_string;
}

ufbx_abi ufbx_shader_prop_binding_list ufbx_find_shader_prop_bindings_len(const ufbx_shader *shader, const char *name, size_t name_len)
{
	ufbx_shader_prop_binding_list bindings = { NULL, 0 };

	ufbx_string name_str = ufbxi_safe_string(name, name_len);
	if (!shader) return bindings;

	ufbxi_for_ptr_list(ufbx_shader_binding, p_bind, shader->bindings) {
		ufbx_shader_binding *bind = *p_bind;

		size_t begin = SIZE_MAX;
		ufbxi_macro_lower_bound_eq(ufbx_shader_prop_binding, 4, &begin, bind->prop_bindings.data, 0, bind->prop_bindings.count,
			( ufbxi_str_less(a->shader_prop, name_str) ), ( ufbxi_str_equal(a->shader_prop, name_str) ));

		if (begin != SIZE_MAX) {

			size_t end = begin;
			ufbxi_macro_upper_bound_eq(ufbx_shader_prop_binding, 4, &end, bind->prop_bindings.data, begin, bind->prop_bindings.count,
				( ufbxi_str_equal(a->shader_prop, name_str) ));

			bindings.data = bind->prop_bindings.data + begin;
			bindings.count = end - begin;
			break;
		}
	}

	return bindings;
}

ufbx_abi ufbx_shader_texture_input *ufbx_find_shader_texture_input_len(const ufbx_shader_texture *shader, const char *name, size_t name_len)
{
	ufbx_string name_str = ufbxi_safe_string(name, name_len);

	size_t index = SIZE_MAX;
	ufbxi_macro_lower_bound_eq(ufbx_shader_texture_input, 4, &index, shader->inputs.data, 0, shader->inputs.count,
		( ufbxi_str_less(a->name, name_str) ), ( ufbxi_str_equal(a->name, name_str) ));

	if (index != SIZE_MAX) {
		return &shader->inputs.data[index];
	}

	return NULL;
}

ufbx_abi bool ufbx_coordinate_axes_valid(ufbx_coordinate_axes axes)
{
	if (axes.right < UFBX_COORDINATE_AXIS_POSITIVE_X || axes.right > UFBX_COORDINATE_AXIS_NEGATIVE_Z) return false;
	if (axes.up < UFBX_COORDINATE_AXIS_POSITIVE_X || axes.up > UFBX_COORDINATE_AXIS_NEGATIVE_Z) return false;
	if (axes.front < UFBX_COORDINATE_AXIS_POSITIVE_X || axes.front > UFBX_COORDINATE_AXIS_NEGATIVE_Z) return false;

	// Check that all the positive/negative axes are used
	uint32_t mask = 0;
	mask |= 1u << ((uint32_t)axes.right >> 1);
	mask |= 1u << ((uint32_t)axes.up >> 1);
	mask |= 1u << ((uint32_t)axes.front >> 1);
	return (mask & 0x7u) == 0x7u;
}

ufbx_abi ufbx_quat ufbx_quat_mul(ufbx_quat a, ufbx_quat b)
{
	return ufbxi_mul_quat(a, b);
}

ufbx_abi ufbx_vec3 ufbx_vec3_normalize(ufbx_vec3 v)
{
	return ufbxi_normalize3(v);
}

ufbx_abi ufbxi_noinline ufbx_real ufbx_quat_dot(ufbx_quat a, ufbx_quat b)
{
	return a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w;
}

ufbx_abi ufbxi_noinline ufbx_quat ufbx_quat_normalize(ufbx_quat q)
{
	ufbx_real norm = ufbx_quat_dot(q, q);
	if (norm == 0.0) return ufbx_identity_quat;
	norm = (ufbx_real)ufbx_sqrt(norm);
	q.x /= norm;
	q.y /= norm;
	q.z /= norm;
	q.w /= norm;
	return q;
}

ufbx_abi ufbxi_noinline ufbx_quat ufbx_quat_fix_antipodal(ufbx_quat q, ufbx_quat reference)
{
	if (ufbx_quat_dot(q, reference) < 0.0f) {
		q.x = -q.x; q.y = -q.y; q.z = -q.z; q.w = -q.w;
	}
	return q;
}

ufbx_abi ufbxi_noinline ufbx_quat ufbx_quat_slerp(ufbx_quat a, ufbx_quat b, ufbx_real t)
{
	double dot = a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w;
	if (dot < 0.0) {
		dot = -dot;
		b.x = -b.x; b.y = -b.y; b.z = -b.z; b.w = -b.w;
	}
	double omega = ufbx_acos(ufbx_fmin(ufbx_fmax(dot, 0.0), 1.0));
	if (omega <= 1.175494351e-38f) return a;
	double rcp_so = 1.0 / ufbx_sin(omega);
	double af = ufbx_sin((1.0 - t) * omega) * rcp_so;
	double bf = ufbx_sin(t * omega) * rcp_so;

	double x = af*a.x + bf*b.x;
	double y = af*a.y + bf*b.y;
	double z = af*a.z + bf*b.z;
	double w = af*a.w + bf*b.w;
	double rcp_len = 1.0 / ufbx_sqrt(x*x + y*y + z*z + w*w);

	ufbx_quat ret;
	ret.x = (ufbx_real)(x * rcp_len);
	ret.y = (ufbx_real)(y * rcp_len);
	ret.z = (ufbx_real)(z * rcp_len);
	ret.w = (ufbx_real)(w * rcp_len);
	return ret;
}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_quat_rotate_vec3(ufbx_quat q, ufbx_vec3 v)
{
	ufbx_real xy = q.x*v.y - q.y*v.x;
	ufbx_real xz = q.x*v.z - q.z*v.x;
	ufbx_real yz = q.y*v.z - q.z*v.y;
	ufbx_vec3 r;
	r.x = 2.0f * (+ q.w*yz + q.y*xy + q.z*xz) + v.x;
	r.y = 2.0f * (- q.x*xy - q.w*xz + q.z*yz) + v.y;
	r.z = 2.0f * (- q.x*xz - q.y*yz + q.w*xy) + v.z;
	return r;
}

ufbx_abi ufbxi_noinline ufbx_quat ufbx_euler_to_quat(ufbx_vec3 v, ufbx_rotation_order order)
{
	double vx = v.x * (UFBXI_DEG_TO_RAD_DOUBLE * 0.5);
	double vy = v.y * (UFBXI_DEG_TO_RAD_DOUBLE * 0.5);
	double vz = v.z * (UFBXI_DEG_TO_RAD_DOUBLE * 0.5);
	double cx = ufbx_cos(vx), sx = ufbx_sin(vx);
	double cy = ufbx_cos(vy), sy = ufbx_sin(vy);
	double cz = ufbx_cos(vz), sz = ufbx_sin(vz);
	ufbx_quat q;

	// Generated by `misc/gen_rotation_order.py`
	switch (order) {
	case UFBX_ROTATION_ORDER_XYZ:
		q.x = (ufbx_real)(-cx*sy*sz + cy*cz*sx);
		q.y = (ufbx_real)(cx*cz*sy + cy*sx*sz);
		q.z = (ufbx_real)(cx*cy*sz - cz*sx*sy);
		q.w = (ufbx_real)(cx*cy*cz + sx*sy*sz);
		break;
	case UFBX_ROTATION_ORDER_XZY:
		q.x = (ufbx_real)(cx*sy*sz + cy*cz*sx);
		q.y = (ufbx_real)(cx*cz*sy + cy*sx*sz);
		q.z = (ufbx_real)(cx*cy*sz - cz*sx*sy);
		q.w = (ufbx_real)(cx*cy*cz - sx*sy*sz);
		break;
	case UFBX_ROTATION_ORDER_YZX:
		q.x = (ufbx_real)(-cx*sy*sz + cy*cz*sx);
		q.y = (ufbx_real)(cx*cz*sy - cy*sx*sz);
		q.z = (ufbx_real)(cx*cy*sz + cz*sx*sy);
		q.w = (ufbx_real)(cx*cy*cz + sx*sy*sz);
		break;
	case UFBX_ROTATION_ORDER_YXZ:
		q.x = (ufbx_real)(-cx*sy*sz + cy*cz*sx);
		q.y = (ufbx_real)(cx*cz*sy + cy*sx*sz);
		q.z = (ufbx_real)(cx*cy*sz + cz*sx*sy);
		q.w = (ufbx_real)(cx*cy*cz - sx*sy*sz);
		break;
	case UFBX_ROTATION_ORDER_ZXY:
		q.x = (ufbx_real)(cx*sy*sz + cy*cz*sx);
		q.y = (ufbx_real)(cx*cz*sy - cy*sx*sz);
		q.z = (ufbx_real)(cx*cy*sz - cz*sx*sy);
		q.w = (ufbx_real)(cx*cy*cz + sx*sy*sz);
		break;
	case UFBX_ROTATION_ORDER_ZYX:
		q.x = (ufbx_real)(cx*sy*sz + cy*cz*sx);
		q.y = (ufbx_real)(cx*cz*sy - cy*sx*sz);
		q.z = (ufbx_real)(cx*cy*sz + cz*sx*sy);
		q.w = (ufbx_real)(cx*cy*cz - sx*sy*sz);
		break;
	default:
		q.x = q.y = q.z = 0.0f; q.w = 1.0f;
		break;
	}

	return q;
}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_quat_to_euler(ufbx_quat q, ufbx_rotation_order order)
{
	// TODO: Derive these rigorously
	#if defined(UFBX_REAL_IS_FLOAT)
		const double eps = 0.9999999;
	#else
		const double eps = 0.999999999;
	#endif

	double vx, vy, vz;
	double t;

	double qx = q.x, qy = q.y, qz = q.z, qw = q.w;

	// Generated by `misc/gen_quat_to_euler.py`
	switch (order) {
	case UFBX_ROTATION_ORDER_XYZ:
		t = 2.0f*(qw*qy - qx*qz);
		if (ufbx_fabs(t) < eps) {
			vy = ufbx_asin(t);
			vz = ufbx_atan2(2.0f*(qw*qz + qx*qy), 2.0f*(qw*qw + qx*qx) - 1.0f);
			vx = -ufbx_atan2(-2.0f*(qw*qx + qy*qz), 2.0f*(qw*qw + qz*qz) - 1.0f);
		} else {
			vy = ufbx_copysign(UFBXI_DPI*0.5, t);
			vz = ufbx_atan2(-2.0f*t*(qw*qx - qy*qz), t*(2.0f*qw*qy + 2.0f*qx*qz));
			vx = 0.0f;
		}
		break;
	case UFBX_ROTATION_ORDER_XZY:
		t = 2.0f*(qw*qz + qx*qy);
		if (ufbx_fabs(t) < eps) {
			vz = ufbx_asin(t);
			vy = ufbx_atan2(2.0f*(qw*qy - qx*qz), 2.0f*(qw*qw + qx*qx) - 1.0f);
			vx = -ufbx_atan2(-2.0f*(qw*qx - qy*qz), 2.0f*(qw*qw + qy*qy) - 1.0f);
		} else {
			vz = ufbx_copysign(UFBXI_DPI*0.5, t);
			vy = ufbx_atan2(2.0f*t*(qw*qx + qy*qz), -t*(2.0f*qx*qy - 2.0f*qw*qz));
			vx = 0.0f;
		}
		break;
	case UFBX_ROTATION_ORDER_YZX:
		t = 2.0f*(qw*qz - qx*qy);
		if (ufbx_fabs(t) < eps) {
			vz = ufbx_asin(t);
			vx = ufbx_atan2(2.0f*(qw*qx + qy*qz), 2.0f*(qw*qw + qy*qy) - 1.0f);
			vy = -ufbx_atan2(-2.0f*(qw*qy + qx*qz), 2.0f*(qw*qw + qx*qx) - 1.0f);
		} else {
			vz = ufbx_copysign(UFBXI_DPI*0.5, t);
			vx = ufbx_atan2(-2.0f*t*(qw*qy - qx*qz), t*(2.0f*qw*qz + 2.0f*qx*qy));
			vy = 0.0f;
		}
		break;
	case UFBX_ROTATION_ORDER_YXZ:
		t = 2.0f*(qw*qx + qy*qz);
		if (ufbx_fabs(t) < eps) {
			vx = ufbx_asin(t);
			vz = ufbx_atan2(2.0f*(qw*qz - qx*qy), 2.0f*(qw*qw + qy*qy) - 1.0f);
			vy = -ufbx_atan2(-2.0f*(qw*qy - qx*qz), 2.0f*(qw*qw + qz*qz) - 1.0f);
		} else {
			vx = ufbx_copysign(UFBXI_DPI*0.5, t);
			vz = ufbx_atan2(2.0f*t*(qw*qy + qx*qz), -t*(2.0f*qy*qz - 2.0f*qw*qx));
			vy = 0.0f;
		}
		break;
	case UFBX_ROTATION_ORDER_ZXY:
		t = 2.0f*(qw*qx - qy*qz);
		if (ufbx_fabs(t) < eps) {
			vx = ufbx_asin(t);
			vy = ufbx_atan2(2.0f*(qw*qy + qx*qz), 2.0f*(qw*qw + qz*qz) - 1.0f);
			vz = -ufbx_atan2(-2.0f*(qw*qz + qx*qy), 2.0f*(qw*qw + qy*qy) - 1.0f);
		} else {
			vx = ufbx_copysign(UFBXI_DPI*0.5, t);
			vy = ufbx_atan2(-2.0f*t*(qw*qz - qx*qy), t*(2.0f*qw*qx + 2.0f*qy*qz));
			vz = 0.0f;
		}
		break;
	case UFBX_ROTATION_ORDER_ZYX:
		t = 2.0f*(qw*qy + qx*qz);
		if (ufbx_fabs(t) < eps) {
			vy = ufbx_asin(t);
			vx = ufbx_atan2(2.0f*(qw*qx - qy*qz), 2.0f*(qw*qw + qz*qz) - 1.0f);
			vz = -ufbx_atan2(-2.0f*(qw*qz - qx*qy), 2.0f*(qw*qw + qx*qx) - 1.0f);
		} else {
			vy = ufbx_copysign(UFBXI_DPI*0.5, t);
			vx = ufbx_atan2(2.0f*t*(qw*qz + qx*qy), -t*(2.0f*qx*qz - 2.0f*qw*qy));
			vz = 0.0f;
		}
		break;
	default:
		vx = vy = vz = 0.0;
		break;
	}

	vx *= UFBXI_RAD_TO_DEG_DOUBLE;
	vy *= UFBXI_RAD_TO_DEG_DOUBLE;
	vz *= UFBXI_RAD_TO_DEG_DOUBLE;

	ufbx_vec3 v = { (ufbx_real)vx, (ufbx_real)vy, (ufbx_real)vz };
	return v;
}

ufbx_abi ufbxi_noinline ufbx_matrix ufbx_matrix_mul(const ufbx_matrix *a, const ufbx_matrix *b)
{
	ufbx_assert(a && b);
	if (!a || !b) return ufbx_identity_matrix;

	ufbx_matrix dst;

	dst.m03 = a->m00*b->m03 + a->m01*b->m13 + a->m02*b->m23 + a->m03;
	dst.m13 = a->m10*b->m03 + a->m11*b->m13 + a->m12*b->m23 + a->m13;
	dst.m23 = a->m20*b->m03 + a->m21*b->m13 + a->m22*b->m23 + a->m23;

	dst.m00 = a->m00*b->m00 + a->m01*b->m10 + a->m02*b->m20;
	dst.m10 = a->m10*b->m00 + a->m11*b->m10 + a->m12*b->m20;
	dst.m20 = a->m20*b->m00 + a->m21*b->m10 + a->m22*b->m20;

	dst.m01 = a->m00*b->m01 + a->m01*b->m11 + a->m02*b->m21;
	dst.m11 = a->m10*b->m01 + a->m11*b->m11 + a->m12*b->m21;
	dst.m21 = a->m20*b->m01 + a->m21*b->m11 + a->m22*b->m21;

	dst.m02 = a->m00*b->m02 + a->m01*b->m12 + a->m02*b->m22;
	dst.m12 = a->m10*b->m02 + a->m11*b->m12 + a->m12*b->m22;
	dst.m22 = a->m20*b->m02 + a->m21*b->m12 + a->m22*b->m22;

	return dst;
}

ufbx_abi ufbx_real ufbx_matrix_determinant(const ufbx_matrix *m)
{
	return
		- m->m02*m->m11*m->m20 + m->m01*m->m12*m->m20 + m->m02*m->m10*m->m21
		- m->m00*m->m12*m->m21 - m->m01*m->m10*m->m22 + m->m00*m->m11*m->m22;
}

ufbx_abi ufbx_matrix ufbx_matrix_invert(const ufbx_matrix *m)
{
	ufbx_real det = ufbx_matrix_determinant(m);

	ufbx_matrix r;
	if (ufbx_fabs(det) <= UFBX_EPSILON) {
		memset(&r, 0, sizeof(r));
		return r;
	}

	ufbx_real rcp_det = 1.0f / det;

	r.m00 = ( - m->m12*m->m21 + m->m11*m->m22) * rcp_det;
	r.m10 = ( + m->m12*m->m20 - m->m10*m->m22) * rcp_det;
	r.m20 = ( - m->m11*m->m20 + m->m10*m->m21) * rcp_det;
	r.m01 = ( + m->m02*m->m21 - m->m01*m->m22) * rcp_det;
	r.m11 = ( - m->m02*m->m20 + m->m00*m->m22) * rcp_det;
	r.m21 = ( + m->m01*m->m20 - m->m00*m->m21) * rcp_det;
	r.m02 = ( - m->m02*m->m11 + m->m01*m->m12) * rcp_det;
	r.m12 = ( + m->m02*m->m10 - m->m00*m->m12) * rcp_det;
	r.m22 = ( - m->m01*m->m10 + m->m00*m->m11) * rcp_det;
	r.m03 = (m->m03*m->m12*m->m21 - m->m02*m->m13*m->m21 - m->m03*m->m11*m->m22 + m->m01*m->m13*m->m22 + m->m02*m->m11*m->m23 - m->m01*m->m12*m->m23) * rcp_det;
	r.m13 = (m->m02*m->m13*m->m20 - m->m03*m->m12*m->m20 + m->m03*m->m10*m->m22 - m->m00*m->m13*m->m22 - m->m02*m->m10*m->m23 + m->m00*m->m12*m->m23) * rcp_det;
	r.m23 = (m->m03*m->m11*m->m20 - m->m01*m->m13*m->m20 - m->m03*m->m10*m->m21 + m->m00*m->m13*m->m21 + m->m01*m->m10*m->m23 - m->m00*m->m11*m->m23) * rcp_det;

	return r;
}

ufbx_abi ufbxi_noinline ufbx_matrix ufbx_matrix_for_normals(const ufbx_matrix *m)
{
	ufbx_real det = ufbx_matrix_determinant(m);
	ufbx_real det_sign = det >= 0.0f ? 1.0f : -1.0f;

	ufbx_matrix r;
	r.m00 = ( - m->m12*m->m21 + m->m11*m->m22) * det_sign;
	r.m01 = ( + m->m12*m->m20 - m->m10*m->m22) * det_sign;
	r.m02 = ( - m->m11*m->m20 + m->m10*m->m21) * det_sign;
	r.m10 = ( + m->m02*m->m21 - m->m01*m->m22) * det_sign;
	r.m11 = ( - m->m02*m->m20 + m->m00*m->m22) * det_sign;
	r.m12 = ( + m->m01*m->m20 - m->m00*m->m21) * det_sign;
	r.m20 = ( - m->m02*m->m11 + m->m01*m->m12) * det_sign;
	r.m21 = ( + m->m02*m->m10 - m->m00*m->m12) * det_sign;
	r.m22 = ( - m->m01*m->m10 + m->m00*m->m11) * det_sign;
	r.m03 = r.m13 = r.m23 = 0.0f;

	return r;
}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_transform_position(const ufbx_matrix *m, ufbx_vec3 v)
{
	ufbx_assert(m);
	if (!m) return ufbx_zero_vec3;

	ufbx_vec3 r;
	r.x = m->m00*v.x + m->m01*v.y + m->m02*v.z + m->m03;
	r.y = m->m10*v.x + m->m11*v.y + m->m12*v.z + m->m13;
	r.z = m->m20*v.x + m->m21*v.y + m->m22*v.z + m->m23;
	return r;
}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_transform_direction(const ufbx_matrix *m, ufbx_vec3 v)
{
	ufbx_assert(m);
	if (!m) return ufbx_zero_vec3;

	ufbx_vec3 r;
	r.x = m->m00*v.x + m->m01*v.y + m->m02*v.z;
	r.y = m->m10*v.x + m->m11*v.y + m->m12*v.z;
	r.z = m->m20*v.x + m->m21*v.y + m->m22*v.z;
	return r;
}

ufbx_abi ufbxi_noinline ufbx_matrix ufbx_transform_to_matrix(const ufbx_transform *t)
{
	ufbx_assert(t);
	if (!t) return ufbx_identity_matrix;

	ufbx_quat q = t->rotation;
	ufbx_real sx = 2.0f * t->scale.x, sy = 2.0f * t->scale.y, sz = 2.0f * t->scale.z;
	ufbx_real xx = q.x*q.x, xy = q.x*q.y, xz = q.x*q.z, xw = q.x*q.w;
	ufbx_real yy = q.y*q.y, yz = q.y*q.z, yw = q.y*q.w;
	ufbx_real zz = q.z*q.z, zw = q.z*q.w;
	ufbx_matrix m;
	m.m00 = sx * (- yy - zz + 0.5f);
	m.m10 = sx * (+ xy + zw);
	m.m20 = sx * (- yw + xz);
	m.m01 = sy * (- zw + xy);
	m.m11 = sy * (- xx - zz + 0.5f);
	m.m21 = sy * (+ xw + yz);
	m.m02 = sz * (+ xz + yw);
	m.m12 = sz * (- xw + yz);
	m.m22 = sz * (- xx - yy + 0.5f);
	m.m03 = t->translation.x;
	m.m13 = t->translation.y;
	m.m23 = t->translation.z;
	return m;
}

ufbx_abi ufbxi_noinline ufbx_transform ufbx_matrix_to_transform(const ufbx_matrix *m)
{
	ufbx_assert(m);
	if (!m) return ufbx_identity_transform;

	ufbx_real det = ufbx_matrix_determinant(m);

	ufbx_transform t;
	t.translation = m->cols[3];
	t.scale.x = ufbxi_length3(m->cols[0]);
	t.scale.y = ufbxi_length3(m->cols[1]);
	t.scale.z = ufbxi_length3(m->cols[2]);

	// Flip a single non-zero axis if negative determinant
	ufbx_real sign_x = 1.0f;
	ufbx_real sign_y = 1.0f;
	ufbx_real sign_z = 1.0f;
	if (det < 0.0f) {
		if (t.scale.x > 0.0f) sign_x = -1.0f;
		else if (t.scale.y > 0.0f) sign_y = -1.0f;
		else if (t.scale.z > 0.0f) sign_z = -1.0f;
	}

	ufbx_vec3 x = ufbxi_mul3(m->cols[0], t.scale.x > 0.0f ? sign_x / t.scale.x : 0.0f);
	ufbx_vec3 y = ufbxi_mul3(m->cols[1], t.scale.y > 0.0f ? sign_y / t.scale.y : 0.0f);
	ufbx_vec3 z = ufbxi_mul3(m->cols[2], t.scale.z > 0.0f ? sign_z / t.scale.z : 0.0f);
	ufbx_real trace = x.x + y.y + z.z;
	if (trace > 0.0f) {
		ufbx_real a = (ufbx_real)ufbx_sqrt(ufbx_fmax(0.0, trace + 1.0)), b = (a != 0.0f) ? 0.5f / a : 0.0f;
		t.rotation.x = (y.z - z.y) * b;
		t.rotation.y = (z.x - x.z) * b;
		t.rotation.z = (x.y - y.x) * b;
		t.rotation.w = 0.5f * a;
	} else if (x.x > y.y && x.x > z.z) {
		ufbx_real a = (ufbx_real)ufbx_sqrt(ufbx_fmax(0.0, 1.0 + x.x - y.y - z.z)), b = (a != 0.0f) ? 0.5f / a : 0.0f;
		t.rotation.x = 0.5f * a;
		t.rotation.y = (y.x + x.y) * b;
		t.rotation.z = (z.x + x.z) * b;
		t.rotation.w = (y.z - z.y) * b;
	}
	else if (y.y > z.z) {
		ufbx_real a = (ufbx_real)ufbx_sqrt(ufbx_fmax(0.0, 1.0 - x.x + y.y - z.z)), b = (a != 0.0f) ? 0.5f / a : 0.0f;
		t.rotation.x = (y.x + x.y) * b;
		t.rotation.y = 0.5f * a;
		t.rotation.z = (z.y + y.z) * b;
		t.rotation.w = (z.x - x.z) * b;
	}
	else {
		ufbx_real a = (ufbx_real)ufbx_sqrt(ufbx_fmax(0.0, 1.0 - x.x - y.y + z.z)), b = (a != 0.0f) ? 0.5f / a : 0.0f;
		t.rotation.x = (z.x + x.z) * b;
		t.rotation.y = (z.y + y.z) * b;
		t.rotation.z = 0.5f * a;
		t.rotation.w = (x.y - y.x) * b;
	}

	ufbx_real len = t.rotation.x*t.rotation.x + t.rotation.y*t.rotation.y + t.rotation.z*t.rotation.z + t.rotation.w*t.rotation.w;
	if (ufbx_fabs(len - 1.0f) > UFBX_EPSILON) {
		if (ufbx_fabs(len) <= UFBX_EPSILON) {
			t.rotation = ufbx_identity_quat;
		} else {
			t.rotation.x /= len;
			t.rotation.y /= len;
			t.rotation.z /= len;
			t.rotation.w /= len;
		}
	}

	t.scale.x *= sign_x;
	t.scale.y *= sign_y;
	t.scale.z *= sign_z;

	return t;
}

ufbx_abi ufbxi_noinline ufbx_matrix ufbx_catch_get_skin_vertex_matrix(ufbx_panic *panic, const ufbx_skin_deformer *skin, size_t vertex, const ufbx_matrix *fallback)
{
	ufbx_assert(skin);
	if (ufbxi_panicf(panic, vertex < skin->vertices.count, "vertex (%zu) out of bounds (%zu)", vertex, skin->vertices.count)) return ufbx_identity_matrix;

	if (!skin || vertex >= skin->vertices.count) return ufbx_identity_matrix;
	ufbx_skin_vertex skin_vertex = skin->vertices.data[vertex];

	ufbx_matrix mat = { 0.0f };
	ufbx_quat q0 = { 0.0f }, qe = { 0.0f };
	ufbx_quat first_q0 = { 0.0f };
	ufbx_vec3 qs = { 0.0f, 0.0f, 0.0f };
	ufbx_real total_weight = 0.0f;

	for (uint32_t i = 0; i < skin_vertex.num_weights; i++) {
		ufbx_skin_weight weight = skin->weights.data[skin_vertex.weight_begin + i];
		ufbx_skin_cluster *cluster = skin->clusters.data[weight.cluster_index];
		const ufbx_node *node = cluster->bone_node;
		if (!node) continue;

		total_weight += weight.weight;
		if (skin_vertex.dq_weight > 0.0f) {
			ufbx_transform t = cluster->geometry_to_world_transform;
			ufbx_quat vq0 = t.rotation;
			if (i == 0) first_q0 = vq0;

			if (ufbx_quat_dot(first_q0, vq0) < 0.0f) {
				vq0.x = -vq0.x;
				vq0.y = -vq0.y;
				vq0.z = -vq0.z;
				vq0.w = -vq0.w;
			}

			ufbx_quat vqt = { 0.5f * t.translation.x, 0.5f * t.translation.y, 0.5f * t.translation.z };
			ufbx_quat vqe = ufbxi_mul_quat(vqt, vq0);
			ufbxi_add_weighted_quat(&q0, vq0, weight.weight);
			ufbxi_add_weighted_quat(&qe, vqe, weight.weight);
			ufbxi_add_weighted_vec3(&qs, t.scale, weight.weight);
		}

		if (skin_vertex.dq_weight < 1.0f) {
			ufbxi_add_weighted_mat(&mat, &cluster->geometry_to_world, (1.0f-skin_vertex.dq_weight) * weight.weight);
		}
	}

	if (total_weight <= 0.0f) {
		if (fallback) {
			return *fallback;
		} else {
			return ufbx_identity_matrix;
		}
	}

	if (ufbx_fabs(total_weight - 1.0f) > UFBX_EPSILON) {
		ufbx_real rcp_weight = ufbx_fabs(total_weight) > UFBX_EPSILON ? 1.0f / total_weight : 0.0f;
		if (skin_vertex.dq_weight > 0.0f) {
			q0.x *= rcp_weight; q0.y *= rcp_weight; q0.z *= rcp_weight; q0.w *= rcp_weight;
			qe.x *= rcp_weight; qe.y *= rcp_weight; qe.z *= rcp_weight; qe.w *= rcp_weight;
			qs.x *= rcp_weight; qs.y *= rcp_weight; qs.z *= rcp_weight;
		}
		if (skin_vertex.dq_weight < 1.0f) {
			mat.m00 *= rcp_weight; mat.m01 *= rcp_weight; mat.m02 *= rcp_weight; mat.m03 *= rcp_weight;
			mat.m10 *= rcp_weight; mat.m11 *= rcp_weight; mat.m12 *= rcp_weight; mat.m13 *= rcp_weight;
			mat.m20 *= rcp_weight; mat.m21 *= rcp_weight; mat.m22 *= rcp_weight; mat.m23 *= rcp_weight;
		}
	}

	if (skin_vertex.dq_weight > 0.0f) {
		ufbx_transform dqt; // ufbxi_uninit
		ufbx_real rcp_len = (ufbx_real)(1.0 / ufbx_sqrt(q0.x*q0.x + q0.y*q0.y + q0.z*q0.z + q0.w*q0.w));
		ufbx_real rcp_len2x2 = 2.0f * rcp_len * rcp_len;
		dqt.rotation.x = q0.x * rcp_len;
		dqt.rotation.y = q0.y * rcp_len;
		dqt.rotation.z = q0.z * rcp_len;
		dqt.rotation.w = q0.w * rcp_len;
		dqt.scale.x = qs.x;
		dqt.scale.y = qs.y;
		dqt.scale.z = qs.z;
		dqt.translation.x = rcp_len2x2 * (- qe.w*q0.x + qe.x*q0.w - qe.y*q0.z + qe.z*q0.y);
		dqt.translation.y = rcp_len2x2 * (- qe.w*q0.y + qe.x*q0.z + qe.y*q0.w - qe.z*q0.x);
		dqt.translation.z = rcp_len2x2 * (- qe.w*q0.z - qe.x*q0.y + qe.y*q0.x + qe.z*q0.w);
		ufbx_matrix dqm = ufbx_transform_to_matrix(&dqt);
		if (skin_vertex.dq_weight < 1.0f) {
			ufbxi_add_weighted_mat(&mat, &dqm, skin_vertex.dq_weight);
		} else {
			mat = dqm;
		}
	}

	return mat;
}

ufbx_abi ufbxi_noinline uint32_t ufbx_get_blend_shape_offset_index(const ufbx_blend_shape *shape, size_t vertex)
{
	ufbx_assert(shape);
	if (!shape) return UFBX_NO_INDEX;

	size_t index = SIZE_MAX;
	uint32_t vertex_ix = (uint32_t)vertex;

	ufbxi_macro_lower_bound_eq(uint32_t, 16, &index, shape->offset_vertices.data, 0, shape->num_offsets,
		( *a < vertex_ix ), ( *a == vertex_ix ));
	if (index >= UINT32_MAX) return UFBX_NO_INDEX;

	return (uint32_t)index;
}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_get_blend_shape_vertex_offset(const ufbx_blend_shape *shape, size_t vertex)
{
	uint32_t index = ufbx_get_blend_shape_offset_index(shape, vertex);
	if (index == UFBX_NO_INDEX) return ufbx_zero_vec3;
	return shape->position_offsets.data[index];
}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_get_blend_vertex_offset(const ufbx_blend_deformer *blend, size_t vertex)
{
	ufbx_assert(blend);
	if (!blend) return ufbx_zero_vec3;

	ufbx_vec3 offset = ufbx_zero_vec3;

	ufbxi_for_ptr_list(ufbx_blend_channel, p_chan, blend->channels) {
		ufbx_blend_channel *chan = *p_chan;
		ufbxi_for_list(ufbx_blend_keyframe, key, chan->keyframes) {
			if (key->effective_weight == 0.0f) continue;

			ufbx_vec3 key_offset = ufbx_get_blend_shape_vertex_offset(key->shape, vertex);
			ufbxi_add_weighted_vec3(&offset, key_offset, key->effective_weight);
		}
	}

	return offset;
}

ufbx_abi void ufbx_add_blend_shape_vertex_offsets(const ufbx_blend_shape *shape, ufbx_vec3 *vertices, size_t num_vertices, ufbx_real weight)
{
	if (weight == 0.0f) return;
	if (!vertices) return;

	size_t num_offsets = shape->num_offsets;
	uint32_t *vertex_indices = shape->offset_vertices.data;
	ufbx_vec3 *offsets = shape->position_offsets.data;
	for (size_t i = 0; i < num_offsets; i++) {
		uint32_t index = vertex_indices[i];
		if (index < num_vertices) {
			ufbxi_add_weighted_vec3(&vertices[index], offsets[i], weight);
		}
	}
}

ufbx_abi void ufbx_add_blend_vertex_offsets(const ufbx_blend_deformer *blend, ufbx_vec3 *vertices, size_t num_vertices, ufbx_real weight)
{
	ufbx_assert(blend);
	if (!blend) return;

	ufbxi_for_ptr_list(ufbx_blend_channel, p_chan, blend->channels) {
		ufbx_blend_channel *chan = *p_chan;
		ufbxi_for_list(ufbx_blend_keyframe, key, chan->keyframes) {
			if (key->effective_weight == 0.0f) continue;
			ufbx_add_blend_shape_vertex_offsets(key->shape, vertices, num_vertices, weight * key->effective_weight);
		}
	}
}

ufbx_abi size_t ufbx_evaluate_nurbs_basis(const ufbx_nurbs_basis *basis, ufbx_real u, ufbx_real *weights, size_t num_weights, ufbx_real *derivatives, size_t num_derivatives)
{
	ufbx_assert(basis);
	if (!basis) return SIZE_MAX;
	if (basis->order == 0) return SIZE_MAX;
	if (!basis->valid) return SIZE_MAX;

	size_t degree = basis->order - 1;
	ufbx_assert(degree >= 1);

	// Binary search for the knot span `[min_u, max_u]` where `min_u <= u < max_u`
	ufbx_real_list knots = basis->knot_vector;
	size_t knot = SIZE_MAX;

	if (u <= basis->t_min) {
		knot = degree;
		u = basis->t_min;
	} else if (u >= basis->t_max) {
		knot = basis->knot_vector.count - degree - 2;
		u = basis->t_max;
	} else {
		ufbxi_macro_lower_bound_eq(ufbx_real, 8, &knot, knots.data, 0, knots.count - 1,
			( a[1] <= u ), ( a[0] <= u && u < a[1] ));
	}

	// The found effective control points are found left from `knot`, locally
	// we use `knot - ix` here as it's more convenient for the following algorithm
	// but we return it as `knot - degree` so that users can find the control points
	// at `points[knot], points[knot+1], ..., points[knot+degree]`
	if (knot < degree) return SIZE_MAX;

	if (num_derivatives == 0) derivatives = NULL;
	if (num_weights < basis->order) return knot - degree;
	if (!weights) return knot - degree;

	weights[0] = 1.0f;
	for (size_t p = 1; p <= degree; p++) {

		ufbx_real prev = 0.0f;
		ufbx_real g = 1.0f - ufbxi_nurbs_weight(&knots, knot - p + 1, p, u);
		ufbx_real dg = 0.0f;
		if (derivatives && p == degree) {
			dg = ufbxi_nurbs_deriv(&knots, knot - p + 1, p);
		}

		for (size_t i = p; i > 0; i--) {
			ufbx_real f = ufbxi_nurbs_weight(&knots, knot - p + i, p, u);
			ufbx_real weight = weights[i - 1];
			weights[i] = f*weight + g*prev;

			if (derivatives && p == degree) {
				ufbx_real df = ufbxi_nurbs_deriv(&knots, knot - p + i, p);
				if (i < num_derivatives) {
					derivatives[i] = df*weight - dg*prev;
				}
				dg = df;
			}

			prev = weight;
			g = 1.0f - f;
		}

		weights[0] = g*prev;
		if (derivatives && p == degree) {
			derivatives[0] = -dg*prev;
		}
	}

	return knot - degree;
}

ufbx_abi ufbxi_noinline ufbx_curve_point ufbx_evaluate_nurbs_curve(const ufbx_nurbs_curve *curve, ufbx_real u)
{
	ufbx_curve_point result = { false };

	ufbx_assert(curve);
	if (!curve) return result;

	ufbx_real weights[UFBXI_MAX_NURBS_ORDER]; // ufbxi_uninit
	ufbx_real derivs[UFBXI_MAX_NURBS_ORDER]; // ufbxi_uninit
	size_t base = ufbx_evaluate_nurbs_basis(&curve->basis, u, weights, UFBXI_MAX_NURBS_ORDER, derivs, UFBXI_MAX_NURBS_ORDER);
	if (base == SIZE_MAX) return result;

	ufbx_vec4 p = { 0 };
	ufbx_vec4 d = { 0 };

	size_t order = curve->basis.order;
	if (order > UFBXI_MAX_NURBS_ORDER) return result;
	if (curve->control_points.count == 0) return result;

	for (size_t i = 0; i < order; i++) {
		size_t ix = (base + i) % curve->control_points.count;
		ufbx_vec4 cp = curve->control_points.data[ix];
		ufbx_real weight = weights[i] * cp.w, deriv = derivs[i] * cp.w;

		p.x += cp.x * weight;
		p.y += cp.y * weight;
		p.z += cp.z * weight;
		p.w += weight;

		d.x += cp.x * deriv;
		d.y += cp.y * deriv;
		d.z += cp.z * deriv;
		d.w += deriv;
	}

	ufbx_real rcp_w = 1.0f / p.w;
	result.valid = true;
	result.position.x = p.x * rcp_w;
	result.position.y = p.y * rcp_w;
	result.position.z = p.z * rcp_w;
	result.derivative.x = (d.x - d.w*result.position.x) * rcp_w;
	result.derivative.y = (d.y - d.w*result.position.y) * rcp_w;
	result.derivative.z = (d.z - d.w*result.position.z) * rcp_w;
	return result;
}

ufbx_abi ufbxi_noinline ufbx_surface_point ufbx_evaluate_nurbs_surface(const ufbx_nurbs_surface *surface, ufbx_real u, ufbx_real v)
{
	ufbx_surface_point result = { false };

	ufbx_assert(surface);
	if (!surface) return result;

	ufbx_real weights_u[UFBXI_MAX_NURBS_ORDER], weights_v[UFBXI_MAX_NURBS_ORDER]; // ufbxi_uninit
	ufbx_real derivs_u[UFBXI_MAX_NURBS_ORDER], derivs_v[UFBXI_MAX_NURBS_ORDER]; // ufbxi_uninit
	size_t base_u = ufbx_evaluate_nurbs_basis(&surface->basis_u, u, weights_u, UFBXI_MAX_NURBS_ORDER, derivs_u, UFBXI_MAX_NURBS_ORDER);
	size_t base_v = ufbx_evaluate_nurbs_basis(&surface->basis_v, v, weights_v, UFBXI_MAX_NURBS_ORDER, derivs_v, UFBXI_MAX_NURBS_ORDER);
	if (base_u == SIZE_MAX || base_v == SIZE_MAX) return result;

	ufbx_vec4 p = { 0 };
	ufbx_vec4 du = { 0 };
	ufbx_vec4 dv = { 0 };

	size_t num_u = surface->num_control_points_u;
	size_t num_v = surface->num_control_points_v;
	size_t order_u = surface->basis_u.order;
	size_t order_v = surface->basis_v.order;
	if (order_u > UFBXI_MAX_NURBS_ORDER || order_v > UFBXI_MAX_NURBS_ORDER) return result;
	if (num_u == 0 || num_v == 0) return result;

	for (size_t vi = 0; vi < order_v; vi++) {
		size_t vix = (base_v + vi) % num_v;
		ufbx_real weight_v = weights_v[vi], deriv_v = derivs_v[vi];

		for (size_t ui = 0; ui < order_u; ui++) {
			size_t uix = (base_u + ui) % num_u;
			ufbx_real weight_u = weights_u[ui], deriv_u = derivs_u[ui];
			ufbx_vec4 cp = surface->control_points.data[vix * num_u + uix];

			ufbx_real weight = weight_u * weight_v * cp.w;
			ufbx_real wderiv_u = deriv_u * weight_v * cp.w;
			ufbx_real wderiv_v = deriv_v * weight_u * cp.w;

			p.x += cp.x * weight;
			p.y += cp.y * weight;
			p.z += cp.z * weight;
			p.w += weight;

			du.x += cp.x * wderiv_u;
			du.y += cp.y * wderiv_u;
			du.z += cp.z * wderiv_u;
			du.w += wderiv_u;

			dv.x += cp.x * wderiv_v;
			dv.y += cp.y * wderiv_v;
			dv.z += cp.z * wderiv_v;
			dv.w += wderiv_v;
		}
	}

	ufbx_real rcp_w = 1.0f / p.w;
	result.valid = true;
	result.position.x = p.x * rcp_w;
	result.position.y = p.y * rcp_w;
	result.position.z = p.z * rcp_w;
	result.derivative_u.x = (du.x - du.w*result.position.x) * rcp_w;
	result.derivative_u.y = (du.y - du.w*result.position.y) * rcp_w;
	result.derivative_u.z = (du.z - du.w*result.position.z) * rcp_w;
	result.derivative_v.x = (dv.x - dv.w*result.position.x) * rcp_w;
	result.derivative_v.y = (dv.y - dv.w*result.position.y) * rcp_w;
	result.derivative_v.z = (dv.z - dv.w*result.position.z) * rcp_w;
	return result;
}

ufbx_abi ufbx_line_curve *ufbx_tessellate_nurbs_curve(const ufbx_nurbs_curve *curve, const ufbx_tessellate_curve_opts *opts, ufbx_error *error)
{
#if UFBXI_FEATURE_TESSELLATION
	ufbxi_check_opts_ptr(ufbx_line_curve, opts, error);
	ufbx_assert(curve);
	if (!curve) return NULL;

	ufbxi_tessellate_curve_context tc = { UFBX_ERROR_NONE };
	if (opts) {
		tc.opts = *opts;
	}

	tc.curve = curve;

	int ok = ufbxi_tessellate_nurbs_curve_imp(&tc);

	ufbxi_free_ator(&tc.ator_tmp);

	if (ok) {
		ufbxi_clear_error(error);
		ufbxi_line_curve_imp *imp = tc.imp;
		return &imp->curve;
	} else {
		ufbxi_fix_error_type(&tc.error, "Failed to tessellate", error);
		ufbxi_buf_free(&tc.result);
		ufbxi_free_ator(&tc.ator_result);
		return NULL;
	}
#else
	if (error) {
		memset(error, 0, sizeof(ufbx_error));
		ufbxi_fmt_err_info(error, "UFBX_ENABLE_TESSELLATION");
		ufbxi_report_err_msg(error, "UFBXI_FEATURE_TESSELLATION", "Feature disabled");
	}
	return NULL;
#endif
}

ufbx_abi ufbx_mesh *ufbx_tessellate_nurbs_surface(const ufbx_nurbs_surface *surface, const ufbx_tessellate_surface_opts *opts, ufbx_error *error)
{
#if UFBXI_FEATURE_TESSELLATION
	ufbx_assert(surface);
	ufbxi_check_opts_ptr(ufbx_mesh, opts, error);
	if (!surface) return NULL;

	ufbxi_tessellate_surface_context tc = { UFBX_ERROR_NONE };
	if (opts) {
		tc.opts = *opts;
	}

	tc.surface = surface;

	int ok = ufbxi_tessellate_nurbs_surface_imp(&tc);

	ufbxi_buf_free(&tc.tmp);
	ufbxi_map_free(&tc.position_map);
	ufbxi_free_ator(&tc.ator_tmp);

	if (ok) {
		ufbxi_clear_error(error);
		ufbxi_mesh_imp *imp = tc.imp;
		return &imp->mesh;
	} else {
		ufbxi_fix_error_type(&tc.error, "Failed to tessellate", error);
		ufbxi_buf_free(&tc.result);
		ufbxi_free_ator(&tc.ator_result);
		return NULL;
	}
#else
	if (error) {
		memset(error, 0, sizeof(ufbx_error));
		ufbxi_report_err_msg(error, "UFBXI_FEATURE_TESSELLATION", "Feature disabled");
	}
	return NULL;
#endif
}

ufbx_abi void ufbx_free_line_curve(ufbx_line_curve *line_curve)
{
	if (!line_curve) return;
	if (!line_curve->from_tessellated_nurbs) return;

	ufbxi_line_curve_imp *imp = ufbxi_get_imp(ufbxi_line_curve_imp, line_curve);
	ufbx_assert(imp->magic == UFBXI_LINE_CURVE_IMP_MAGIC);
	if (imp->magic != UFBXI_LINE_CURVE_IMP_MAGIC) return;
	ufbxi_release_ref(&imp->refcount);
}

ufbx_abi void ufbx_retain_line_curve(ufbx_line_curve *line_curve)
{
	if (!line_curve) return;
	if (!line_curve->from_tessellated_nurbs) return;

	ufbxi_line_curve_imp *imp = ufbxi_get_imp(ufbxi_line_curve_imp, line_curve);
	ufbx_assert(imp->magic == UFBXI_LINE_CURVE_IMP_MAGIC);
	if (imp->magic != UFBXI_LINE_CURVE_IMP_MAGIC) return;
	ufbxi_retain_ref(&imp->refcount);
}

ufbx_abi uint32_t ufbx_find_face_index(ufbx_mesh *mesh, size_t index)
{
	if (!mesh || index > UINT32_MAX) return UFBX_NO_INDEX;
	uint32_t ix = (uint32_t)index;

	size_t face_ix = SIZE_MAX;
	ufbxi_macro_lower_bound_eq(ufbx_face, 4, &face_ix, mesh->faces.data, 0, mesh->faces.count,
		( a->index_begin + a->num_indices <= ix ), ( ix >= a->index_begin && ix < a->index_begin + a->num_indices ));
	return (uint32_t)face_ix;
}

ufbx_abi ufbxi_noinline uint32_t ufbx_catch_triangulate_face(ufbx_panic *panic, uint32_t *indices, size_t num_indices, const ufbx_mesh *mesh, ufbx_face face)
{
#if UFBXI_FEATURE_TRIANGULATION
	if (face.num_indices < 3) return 0;

	size_t required_indices = ((size_t)face.num_indices - 2) * 3;
	if (ufbxi_panicf(panic, num_indices >= required_indices, "Face needs at least %zu indices for triangles, got space for %zu", required_indices, num_indices)) return 0;
	if (ufbxi_panicf(panic, face.index_begin < mesh->num_indices, "Face index begin (%u) out of bounds (%zu)", face.index_begin, mesh->num_indices)) return 0;
	if (ufbxi_panicf(panic, mesh->num_indices - face.index_begin >= face.num_indices, "Face index end (%u + %u) out of bounds (%zu)", face.index_begin, face.num_indices, mesh->num_indices)) return 0;

	if (face.num_indices == 3) {
		// Fast case: Already a triangle
		indices[0] = face.index_begin + 0;
		indices[1] = face.index_begin + 1;
		indices[2] = face.index_begin + 2;
		return 1;
	} else if (face.num_indices == 4) {
		// Quad: Split along the shortest axis unless a vertex crosses the axis
		uint32_t i0 = face.index_begin + 0;
		uint32_t i1 = face.index_begin + 1;
		uint32_t i2 = face.index_begin + 2;
		uint32_t i3 = face.index_begin + 3;
		ufbx_vec3 v0 = mesh->vertex_position.values.data[mesh->vertex_position.indices.data[i0]];
		ufbx_vec3 v1 = mesh->vertex_position.values.data[mesh->vertex_position.indices.data[i1]];
		ufbx_vec3 v2 = mesh->vertex_position.values.data[mesh->vertex_position.indices.data[i2]];
		ufbx_vec3 v3 = mesh->vertex_position.values.data[mesh->vertex_position.indices.data[i3]];

		ufbx_vec3 a = ufbxi_sub3(v2, v0);
		ufbx_vec3 b = ufbxi_sub3(v3, v1);

		ufbx_vec3 na1 = ufbxi_normalize3(ufbxi_cross3(a, ufbxi_sub3(v1, v0)));
		ufbx_vec3 na3 = ufbxi_normalize3(ufbxi_cross3(a, ufbxi_sub3(v0, v3)));
		ufbx_vec3 nb0 = ufbxi_normalize3(ufbxi_cross3(b, ufbxi_sub3(v1, v0)));
		ufbx_vec3 nb2 = ufbxi_normalize3(ufbxi_cross3(b, ufbxi_sub3(v2, v1)));

		ufbx_real dot_aa = ufbxi_dot3(a, a);
		ufbx_real dot_bb = ufbxi_dot3(b, b);
		ufbx_real dot_na = ufbxi_dot3(na1, na3);
		ufbx_real dot_nb = ufbxi_dot3(nb0, nb2);

		bool split_a = dot_aa <= dot_bb;

		if (dot_na < 0.0f || dot_nb < 0.0f) {
			split_a = dot_na >= dot_nb;
		}

		if (split_a) {
			indices[0] = i0;
			indices[1] = i1;
			indices[2] = i2;
			indices[3] = i2;
			indices[4] = i3;
			indices[5] = i0;
		} else {
			indices[0] = i1;
			indices[1] = i2;
			indices[2] = i3;
			indices[3] = i3;
			indices[4] = i0;
			indices[5] = i1;
		}

		return 2;
	} else {
		ufbxi_ngon_context nc = { 0 };
		nc.positions = mesh->vertex_position;
		nc.face = face;

		uint32_t num_indices_u32 = num_indices < UINT32_MAX ? (uint32_t)num_indices : UINT32_MAX;

		uint32_t local_indices[12]; // ufbxi_uninit
		if (num_indices_u32 < 12) {
			uint32_t num_tris = ufbxi_triangulate_ngon(&nc, local_indices, 12);
			memcpy(indices, local_indices, num_tris * 3 * sizeof(uint32_t));
			return num_tris;
		} else {
			return ufbxi_triangulate_ngon(&nc, indices, num_indices_u32);
		}
	}
#else
	ufbxi_panicf_imp(panic, "Triangulation disabled");
	return 0;
#endif
}

ufbx_abi void ufbx_catch_compute_topology(ufbx_panic *panic, const ufbx_mesh *mesh, ufbx_topo_edge *indices, size_t num_indices)
{
	if (ufbxi_panicf(panic, num_indices >= mesh->num_indices, "Required mesh.num_indices (%zu) indices, got %zu", mesh->num_indices, num_indices)) return;

	ufbxi_compute_topology(mesh, indices);
}

ufbx_abi uint32_t ufbx_catch_topo_next_vertex_edge(ufbx_panic *panic, const ufbx_topo_edge *topo, size_t num_topo, uint32_t index)
{
	if (index == UFBX_NO_INDEX) return UFBX_NO_INDEX;
	if (ufbxi_panicf(panic, (size_t)index < num_topo, "index (%u) out of bounds (%zu)", index, num_topo)) return UFBX_NO_INDEX;
	uint32_t twin = topo[index].twin;
	if (twin == UFBX_NO_INDEX) return UFBX_NO_INDEX;
	if (ufbxi_panicf(panic, (size_t)twin < num_topo, "Corrupted topology structure")) return UFBX_NO_INDEX;
	return topo[twin].next;
}

ufbx_abi uint32_t ufbx_catch_topo_prev_vertex_edge(ufbx_panic *panic, const ufbx_topo_edge *topo, size_t num_topo, uint32_t index)
{
	if (index == UFBX_NO_INDEX) return UFBX_NO_INDEX;
	if (ufbxi_panicf(panic, (size_t)index < num_topo, "index (%u) out of bounds (%zu)", index, num_topo)) return UFBX_NO_INDEX;
	return topo[topo[index].prev].twin;
}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_catch_get_weighted_face_normal(ufbx_panic *panic, const ufbx_vertex_vec3 *positions, ufbx_face face)
{
	if (ufbxi_panicf(panic, face.index_begin <= positions->indices.count, "Face index begin (%u) out of bounds (%zu)", face.index_begin, positions->indices.count)) return ufbx_zero_vec3;
	if (ufbxi_panicf(panic, positions->indices.count - face.index_begin >= face.num_indices, "Face index end (%u + %u) out of bounds (%zu)", face.index_begin, face.num_indices, positions->indices.count)) return ufbx_zero_vec3;

	if (face.num_indices < 3) {
		return ufbx_zero_vec3;
	} else if (face.num_indices == 3) {
		ufbx_vec3 a = ufbx_get_vertex_vec3(positions, face.index_begin + 0);
		ufbx_vec3 b = ufbx_get_vertex_vec3(positions, face.index_begin + 1);
		ufbx_vec3 c = ufbx_get_vertex_vec3(positions, face.index_begin + 2);
		return ufbxi_cross3(ufbxi_sub3(b, a), ufbxi_sub3(c, a));
	} else if (face.num_indices == 4) {
		ufbx_vec3 a = ufbx_get_vertex_vec3(positions, face.index_begin + 0);
		ufbx_vec3 b = ufbx_get_vertex_vec3(positions, face.index_begin + 1);
		ufbx_vec3 c = ufbx_get_vertex_vec3(positions, face.index_begin + 2);
		ufbx_vec3 d = ufbx_get_vertex_vec3(positions, face.index_begin + 3);
		return ufbxi_cross3(ufbxi_sub3(c, a), ufbxi_sub3(d, b));
	} else {
		// Newell's Method
		ufbx_vec3 result = ufbx_zero_vec3;
		for (size_t i = 0; i < face.num_indices; i++) {
			size_t next = i + 1 < face.num_indices ? i + 1 : 0;
			ufbx_vec3 a = ufbx_get_vertex_vec3(positions, face.index_begin + i);
			ufbx_vec3 b = ufbx_get_vertex_vec3(positions, face.index_begin + next);
			result.x += (a.y - b.y) * (a.z + b.z);
			result.y += (a.z - b.z) * (a.x + b.x);
			result.z += (a.x - b.x) * (a.y + b.y);
		}
		return result;
	}
}

size_t ufbx_catch_generate_normal_mapping(ufbx_panic *panic, const ufbx_mesh *mesh, const ufbx_topo_edge *topo, size_t num_topo, uint32_t *normal_indices, size_t num_normal_indices, bool assume_smooth)
{
	uint32_t next_index = 0;
	if (ufbxi_panicf(panic, num_normal_indices >= mesh->num_indices, "Expected at least mesh.num_indices (%zu), got %zu", mesh->num_indices, num_normal_indices)) return 0;

	for (size_t i = 0; i < mesh->num_indices; i++) {
		normal_indices[i] = UFBX_NO_INDEX;
	}

	// Walk around vertices and merge around smooth edges
	for (size_t vi = 0; vi < mesh->num_vertices; vi++) {
		uint32_t original_start = mesh->vertex_first_index.data[vi];
		if (original_start == UFBX_NO_INDEX) continue;
		uint32_t start = original_start, cur = start;

		for (;;) {
			uint32_t prev = ufbx_topo_next_vertex_edge(topo, num_topo, cur);
			if (!ufbxi_is_edge_smooth(mesh, topo, num_topo, cur, assume_smooth)) start = cur;
			if (prev == UFBX_NO_INDEX) { start = cur; break; }
			if (prev == original_start) break;
			cur = prev;
		}

		normal_indices[start] = next_index++;
		uint32_t next = start;
		for (;;) {
			next = ufbx_topo_prev_vertex_edge(topo, num_topo, next);
			if (next == UFBX_NO_INDEX || next == start) break;

			if (!ufbxi_is_edge_smooth(mesh, topo, num_topo, next, assume_smooth)) {
				++next_index;
			}
			normal_indices[next] = next_index - 1;
		}
	}

	// Assign non-manifold indices
	for (size_t i = 0; i < mesh->num_indices; i++) {
		if (normal_indices[i] == UFBX_NO_INDEX) {
			normal_indices[i] = next_index++;
		}
	}

	return (size_t)next_index;
}

ufbx_abi size_t ufbx_generate_normal_mapping(const ufbx_mesh *mesh, const ufbx_topo_edge *topo, size_t num_topo, uint32_t *normal_indices, size_t num_normal_indices, bool assume_smooth)
{
	return ufbx_catch_generate_normal_mapping(NULL, mesh, topo, num_topo, normal_indices, num_normal_indices, assume_smooth);
}

ufbx_abi void ufbx_catch_compute_normals(ufbx_panic *panic, const ufbx_mesh *mesh, const ufbx_vertex_vec3 *positions, const uint32_t *normal_indices, size_t num_normal_indices, ufbx_vec3 *normals, size_t num_normals)
{
	if (ufbxi_panicf(panic, num_normal_indices >= mesh->num_indices, "Expected at least mesh.num_indices (%zu), got %zu", mesh->num_indices, num_normal_indices)) return;

	memset(normals, 0, sizeof(ufbx_vec3)*num_normals);

	for (size_t fi = 0; fi < mesh->num_faces; fi++) {
		ufbx_face face = mesh->faces.data[fi];
		ufbx_vec3 normal = ufbx_get_weighted_face_normal(positions, face);
		for (size_t ix = 0; ix < face.num_indices; ix++) {
			uint32_t index = normal_indices[face.index_begin + ix];

			if (ufbxi_panicf(panic, index < num_normals, "Normal index (%u) out of bounds (%zu) at %zu", index, num_normals, ix)) return;

			ufbx_vec3 *n = &normals[index];
			*n = ufbxi_add3(*n, normal);
		}
	}

	for (size_t i = 0; i < num_normals; i++) {
		ufbx_real len = ufbxi_length3(normals[i]);
		if (len > 0.0f) {
			normals[i].x /= len;
			normals[i].y /= len;
			normals[i].z /= len;
		}
	}
}

ufbx_abi void ufbx_compute_normals(const ufbx_mesh *mesh, const ufbx_vertex_vec3 *positions, const uint32_t *normal_indices, size_t num_normal_indices, ufbx_vec3 *normals, size_t num_normals)
{
	ufbx_catch_compute_normals(NULL, mesh, positions, normal_indices, num_normal_indices, normals, num_normals);
}

ufbx_abi ufbx_mesh *ufbx_subdivide_mesh(const ufbx_mesh *mesh, size_t level, const ufbx_subdivide_opts *opts, ufbx_error *error)
{
	ufbxi_check_opts_ptr(ufbx_mesh, opts, error);
	if (!mesh) return NULL;
	if (level == 0) return (ufbx_mesh*)mesh;
	return ufbxi_subdivide_mesh(mesh, level, opts, error);
}

ufbx_abi void ufbx_free_mesh(ufbx_mesh *mesh)
{
	if (!mesh) return;
	if (!mesh->subdivision_evaluated && !mesh->from_tessellated_nurbs) return;

	ufbxi_mesh_imp *imp = ufbxi_get_imp(ufbxi_mesh_imp, mesh);
	ufbx_assert(imp->magic == UFBXI_MESH_IMP_MAGIC);
	if (imp->magic != UFBXI_MESH_IMP_MAGIC) return;
	ufbxi_release_ref(&imp->refcount);
}

ufbx_abi void ufbx_retain_mesh(ufbx_mesh *mesh)
{
	if (!mesh) return;
	if (!mesh->subdivision_evaluated && !mesh->from_tessellated_nurbs) return;

	ufbxi_mesh_imp *imp = ufbxi_get_imp(ufbxi_mesh_imp, mesh);
	ufbx_assert(imp->magic == UFBXI_MESH_IMP_MAGIC);
	if (imp->magic != UFBXI_MESH_IMP_MAGIC) return;
	ufbxi_retain_ref(&imp->refcount);
}

ufbx_abi ufbx_geometry_cache *ufbx_load_geometry_cache(
	const char *filename,
	const ufbx_geometry_cache_opts *opts, ufbx_error *error)
{
	return ufbx_load_geometry_cache_len(filename, strlen(filename),
		opts, error);
}

ufbx_abi ufbx_geometry_cache *ufbx_load_geometry_cache_len(
	const char *filename, size_t filename_len,
	const ufbx_geometry_cache_opts *opts, ufbx_error *error)
{
	ufbxi_check_opts_ptr(ufbx_geometry_cache, opts, error);
	ufbx_string str = ufbxi_safe_string(filename, filename_len);
	return ufbxi_load_geometry_cache(str, opts, error);
}

ufbx_abi void ufbx_free_geometry_cache(ufbx_geometry_cache *cache)
{
	if (!cache) return;

	ufbxi_geometry_cache_imp *imp = ufbxi_get_imp(ufbxi_geometry_cache_imp, cache);
	ufbx_assert(imp->magic == UFBXI_CACHE_IMP_MAGIC);
	if (imp->magic != UFBXI_CACHE_IMP_MAGIC) return;
	if (imp->owned_by_scene) return;
	ufbxi_release_ref(&imp->refcount);
}

ufbx_abi void ufbx_retain_geometry_cache(ufbx_geometry_cache *cache)
{
	if (!cache) return;

	ufbxi_geometry_cache_imp *imp = ufbxi_get_imp(ufbxi_geometry_cache_imp, cache);
	ufbx_assert(imp->magic == UFBXI_CACHE_IMP_MAGIC);
	if (imp->magic != UFBXI_CACHE_IMP_MAGIC) return;
	if (imp->owned_by_scene) return;
	ufbxi_retain_ref(&imp->refcount);
}

typedef struct {
	union {
		double f64[UFBXI_GEOMETRY_CACHE_BUFFER_SIZE];
		float f32[UFBXI_GEOMETRY_CACHE_BUFFER_SIZE];
	} src;
	ufbx_real dst[UFBXI_GEOMETRY_CACHE_BUFFER_SIZE];
} ufbxi_geometry_cache_buffer;

ufbx_abi ufbxi_noinline size_t ufbx_read_geometry_cache_real(const ufbx_cache_frame *frame, ufbx_real *data, size_t count, const ufbx_geometry_cache_data_opts *user_opts)
{
#if UFBXI_FEATURE_GEOMETRY_CACHE
	ufbxi_check_opts_return_no_error(0, user_opts);
	if (!frame || count == 0) return 0;
	ufbx_assert(data);
	if (!data) return 0;

	ufbx_geometry_cache_data_opts opts; // ufbxi_uninit
	if (user_opts) {
		opts = *user_opts;
	} else {
		memset(&opts, 0, sizeof(opts));
	}

	if (!opts.open_file_cb.fn) {
		opts.open_file_cb.fn = ufbx_default_open_file;
	}

	bool use_double = false;

	size_t src_count = 0;

	switch (frame->data_format) {
	case UFBX_CACHE_DATA_FORMAT_UNKNOWN: src_count = 0; break;
	case UFBX_CACHE_DATA_FORMAT_REAL_FLOAT: src_count = frame->data_count; break;
	case UFBX_CACHE_DATA_FORMAT_VEC3_FLOAT: src_count = frame->data_count * 3; break;
	case UFBX_CACHE_DATA_FORMAT_REAL_DOUBLE: src_count = frame->data_count; use_double = true; break;
	case UFBX_CACHE_DATA_FORMAT_VEC3_DOUBLE: src_count = frame->data_count * 3; use_double = true; break;
	default: ufbxi_unreachable("Bad data_format"); break;
	}

	bool src_big_endian = false;
	switch (frame->data_encoding) {
	case UFBX_CACHE_DATA_ENCODING_UNKNOWN: return 0;
	case UFBX_CACHE_DATA_ENCODING_LITTLE_ENDIAN: src_big_endian = false; break;
	case UFBX_CACHE_DATA_ENCODING_BIG_ENDIAN: src_big_endian = true; break;
	default: ufbxi_unreachable("Bad data_encoding"); break;
	}

	// Test endianness
	bool dst_big_endian;
	{
		uint8_t buf[2];
		uint16_t val = 0xbbaa;
		memcpy(buf, &val, 2);
		dst_big_endian = buf[0] == 0xbb;
	}

	if (src_count == 0) return 0;
	src_count = ufbxi_min_sz(src_count, count);

	ufbx_stream stream = { 0 };
	if (!ufbxi_open_file(&opts.open_file_cb, &stream, frame->filename.data, frame->filename.length, NULL, NULL, UFBX_OPEN_FILE_GEOMETRY_CACHE)) {
		return 0;
	}

	// Skip to the correct point in the file
	uint64_t offset = frame->data_offset;
	if (stream.skip_fn) {
		while (offset > 0) {
			size_t to_skip = (size_t)ufbxi_min64(offset, UFBXI_MAX_SKIP_SIZE);
			if (!stream.skip_fn(stream.user, to_skip)) break;
			offset -= to_skip;
		}
	} else {
		char buffer[4096]; // ufbxi_uninit
		while (offset > 0) {
			size_t to_skip = (size_t)ufbxi_min64(offset, sizeof(buffer));
			size_t num_read = stream.read_fn(stream.user, buffer, to_skip);
			if (num_read != to_skip) break;
			offset -= to_skip;
		}
	}

	// Failed to skip all the way
	if (offset > 0) {
		if (stream.close_fn) {
			stream.close_fn(stream.user);
		}
		return 0;
	}

	ufbx_real *dst = data;
	size_t mirror_ix = (size_t)frame->mirror_axis - 1;
	ufbxi_geometry_cache_buffer buffer; // ufbxi_uninit
	while (src_count > 0) {
		size_t to_read = ufbxi_min_sz(src_count, UFBXI_GEOMETRY_CACHE_BUFFER_SIZE);
		src_count -= to_read;
		size_t num_read = 0;
		if (use_double) {
			size_t bytes_read = stream.read_fn(stream.user, buffer.src.f64, to_read * sizeof(double));
			if (bytes_read == SIZE_MAX) bytes_read = 0;
			num_read = bytes_read / sizeof(double);
			if (src_big_endian != dst_big_endian) {
				for (size_t i = 0; i < num_read; i++) {
					char t, *v = (char*)&buffer.src.f64[i];
					t = v[0]; v[0] = v[7]; v[7] = t;
					t = v[1]; v[1] = v[6]; v[6] = t;
					t = v[2]; v[2] = v[5]; v[5] = t;
					t = v[3]; v[3] = v[4]; v[4] = t;
				}
			}
			ufbxi_nounroll for (size_t i = 0; i < num_read; i++) {
				buffer.dst[i] = (ufbx_real)buffer.src.f64[i];
			}
		} else {
			size_t bytes_read = stream.read_fn(stream.user, buffer.src.f32, to_read * sizeof(float));
			if (bytes_read == SIZE_MAX) bytes_read = 0;
			num_read = bytes_read / sizeof(float);
			if (src_big_endian != dst_big_endian) {
				for (size_t i = 0; i < num_read; i++) {
					char t, *v = (char*)&buffer.src.f32[i];
					t = v[0]; v[0] = v[3]; v[3] = t;
					t = v[1]; v[1] = v[2]; v[2] = t;
				}
			}
			ufbxi_nounroll for (size_t i = 0; i < num_read; i++) {
				buffer.dst[i] = (ufbx_real)buffer.src.f32[i];
			}
		}

		if (!opts.ignore_transform) {
			ufbx_real scale = frame->scale_factor;
			if (scale != 1.0f) {
				for (size_t i = 0; i < num_read; i++) {
					buffer.dst[i] *= scale;
				}
			}
			if (frame->mirror_axis) {
				while (mirror_ix < num_read) {
					buffer.dst[mirror_ix] = -buffer.dst[mirror_ix];
					mirror_ix += 3;
				}
				mirror_ix -= num_read;
			}
		}

		if (dst) {
			ufbx_real weight = opts.use_weight ? opts.weight : 1.0f;
			if (opts.additive) {
				ufbxi_nounroll for (size_t i = 0; i < num_read; i++) {
					dst[i] += buffer.dst[i] * weight;
				}
			} else {
				ufbxi_nounroll for (size_t i = 0; i < num_read; i++) {
					dst[i] = buffer.dst[i] * weight;
				}
			}
			dst += num_read;
		}

		if (num_read != to_read) break;
	}

	if (stream.close_fn) {
		stream.close_fn(stream.user);
	}

	return ufbxi_to_size(dst - data);
#else
	return 0;
#endif
}

ufbx_abi ufbxi_noinline size_t ufbx_sample_geometry_cache_real(const ufbx_cache_channel *channel, double time, ufbx_real *data, size_t count, const ufbx_geometry_cache_data_opts *user_opts)
{
#if UFBXI_FEATURE_GEOMETRY_CACHE
	ufbxi_check_opts_return_no_error(0, user_opts);
	if (!channel || count == 0) return 0;
	ufbx_assert(data);
	if (!data) return 0;
	if (channel->frames.count == 0) return 0;

	ufbx_geometry_cache_data_opts opts;
	if (user_opts) {
		opts = *user_opts;
	} else {
		memset(&opts, 0, sizeof(opts));
	}

	size_t begin = 0;
	size_t end = channel->frames.count;
	const ufbx_cache_frame *frames = channel->frames.data;
	while (end - begin >= 8) {
		size_t mid = (begin + end) >> 1;
		if (frames[mid].time < time) {
			begin = mid + 1;
		} else {
			end = mid;
		}
	}

	const double eps = 0.00000001;

	end = channel->frames.count;
	for (; begin < end; begin++) {
		const ufbx_cache_frame *next = &frames[begin];
		if (next->time < time) continue;

		// First keyframe
		if (begin == 0) {
			return ufbx_read_geometry_cache_real(next, data, count, &opts);
		}

		const ufbx_cache_frame *prev = next - 1;

		// Snap to exact frames if near
		if (ufbx_fabs(next->time - time) < eps) {
			return ufbx_read_geometry_cache_real(next, data, count, &opts);
		}
		if (ufbx_fabs(prev->time - time) < eps) {
			return ufbx_read_geometry_cache_real(prev, data, count, &opts);
		}

		double rcp_delta = 1.0 / (next->time - prev->time);
		double t = (time - prev->time) * rcp_delta;

		ufbx_real original_weight = opts.use_weight ? opts.weight : 1.0f;

		opts.use_weight = true;
		opts.weight = (ufbx_real)(original_weight * (1.0 - t));
		size_t num_prev = ufbx_read_geometry_cache_real(prev, data, count, &opts);

		opts.additive = true;
		opts.weight = (ufbx_real)(original_weight * t);
		return ufbx_read_geometry_cache_real(next, data, num_prev, &opts);
	}

	// Last frame
	const ufbx_cache_frame *last = &frames[end - 1];
	return ufbx_read_geometry_cache_real(last, data, count, &opts);
#else
	return 0;
#endif
}

ufbx_abi ufbxi_noinline size_t ufbx_read_geometry_cache_vec3(const ufbx_cache_frame *frame, ufbx_vec3 *data, size_t count, const ufbx_geometry_cache_data_opts *opts)
{
#if UFBXI_FEATURE_GEOMETRY_CACHE
	if (!frame || count == 0) return 0;
	ufbx_assert(data);
	if (!data) return 0;
	return ufbx_read_geometry_cache_real(frame, (ufbx_real*)data, count * 3, opts) / 3;
#else
	return 0;
#endif
}

ufbx_abi ufbxi_noinline size_t ufbx_sample_geometry_cache_vec3(const ufbx_cache_channel *channel, double time, ufbx_vec3 *data, size_t count, const ufbx_geometry_cache_data_opts *opts)
{
#if UFBXI_FEATURE_GEOMETRY_CACHE
	if (!channel || count == 0) return 0;
	ufbx_assert(data);
	if (!data) return 0;
	return ufbx_sample_geometry_cache_real(channel, time, (ufbx_real*)data, count * 3, opts) / 3;
#else
	return 0;
#endif
}

ufbx_abi ufbx_dom_node *ufbx_dom_find_len(const ufbx_dom_node *parent, const char *name, size_t name_len)
{
	ufbx_string ref = ufbxi_safe_string(name, name_len);
	ufbxi_for_ptr_list(ufbx_dom_node, p_child, parent->children) {
		if (ufbxi_str_equal((*p_child)->name, ref)) return (ufbx_dom_node*)*p_child;
	}
	return NULL;
}

ufbx_abi size_t ufbx_generate_indices(const ufbx_vertex_stream *streams, size_t num_streams, uint32_t *indices, size_t num_indices, const ufbx_allocator_opts *allocator, ufbx_error *error)
{
	ufbx_error local_error; // ufbxi_uninit
	if (!error) {
		error = &local_error;
	}
	memset(error, 0, sizeof(ufbx_error));
	return ufbxi_generate_indices(streams, num_streams, indices, num_indices, allocator, error);
}

ufbx_abi void ufbx_thread_pool_run_task(ufbx_thread_pool_context ctx, uint32_t index)
{
	ufbxi_thread_pool_execute((ufbxi_thread_pool*)ctx, index);
}

ufbx_abi void ufbx_thread_pool_set_user_ptr(ufbx_thread_pool_context ctx, void *user)
{
	ufbxi_thread_pool *pool = (ufbxi_thread_pool*)ctx;
	pool->user_ptr = user;
}

ufbx_abi void *ufbx_thread_pool_get_user_ptr(ufbx_thread_pool_context ctx)
{
	ufbxi_thread_pool *pool = (ufbxi_thread_pool*)ctx;
	return pool->user_ptr;
}

ufbx_abi ufbxi_noinline ufbx_real ufbx_catch_get_vertex_real(ufbx_panic *panic, const ufbx_vertex_real *v, size_t index)
{
	if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return 0.0f;
	uint32_t ix = v->indices.data[index];
	if (ufbxi_panicf(panic, (size_t)ix < v->values.count || ix == UFBX_NO_INDEX, "Corrupted or missing vertex attribute (%u) at %zu", ix, index)) return 0.0f;
	return v->values.data[(int32_t)ix];
}

ufbx_abi ufbxi_noinline ufbx_vec2 ufbx_catch_get_vertex_vec2(ufbx_panic *panic, const ufbx_vertex_vec2 *v, size_t index)
{
	if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return ufbx_zero_vec2;
	uint32_t ix = v->indices.data[index];
	if (ufbxi_panicf(panic, (size_t)ix < v->values.count || ix == UFBX_NO_INDEX, "Corrupted or missing vertex attribute (%u) at %zu", ix, index)) return ufbx_zero_vec2;
	return v->values.data[(int32_t)ix];
}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_catch_get_vertex_vec3(ufbx_panic *panic, const ufbx_vertex_vec3 *v, size_t index)
{
	if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return ufbx_zero_vec3;
	uint32_t ix = v->indices.data[index];
	if (ufbxi_panicf(panic, (size_t)ix < v->values.count || ix == UFBX_NO_INDEX, "Corrupted or missing vertex attribute (%u) at %zu", ix, index)) return ufbx_zero_vec3;
	return v->values.data[(int32_t)ix];
}

ufbx_abi ufbxi_noinline ufbx_vec4 ufbx_catch_get_vertex_vec4(ufbx_panic *panic, const ufbx_vertex_vec4 *v, size_t index)
{
	if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return ufbx_zero_vec4;
	uint32_t ix = v->indices.data[index];
	if (ufbxi_panicf(panic, (size_t)ix < v->values.count || ix == UFBX_NO_INDEX, "Corrupted or missing vertex attribute (%u) at %zu", ix, index)) return ufbx_zero_vec4;
	return v->values.data[(int32_t)ix];
}

ufbx_abi ufbx_real ufbx_catch_get_vertex_w_vec3(ufbx_panic *panic, const ufbx_vertex_vec3 *v, size_t index)
{
	if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return 0.0f;
	if (v->values_w.count == 0) return 0.0f;
	uint32_t ix = v->indices.data[index];
	if (ufbxi_panicf(panic, (size_t)ix < v->values.count || ix == UFBX_NO_INDEX, "Corrupted or missing vertex attribute (%u) at %zu", ix, index)) return 0.0f;
	return v->values_w.data[(int32_t)ix];
}

ufbx_abi ufbx_unknown *ufbx_as_unknown(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_UNKNOWN ? (ufbx_unknown*)element : NULL; }
ufbx_abi ufbx_node *ufbx_as_node(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_NODE ? (ufbx_node*)element : NULL; }
ufbx_abi ufbx_mesh *ufbx_as_mesh(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_MESH ? (ufbx_mesh*)element : NULL; }
ufbx_abi ufbx_light *ufbx_as_light(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_LIGHT ? (ufbx_light*)element : NULL; }
ufbx_abi ufbx_camera *ufbx_as_camera(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CAMERA ? (ufbx_camera*)element : NULL; }
ufbx_abi ufbx_bone *ufbx_as_bone(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_BONE ? (ufbx_bone*)element : NULL; }
ufbx_abi ufbx_empty *ufbx_as_empty(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_EMPTY ? (ufbx_empty*)element : NULL; }
ufbx_abi ufbx_line_curve *ufbx_as_line_curve(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_LINE_CURVE ? (ufbx_line_curve*)element : NULL; }
ufbx_abi ufbx_nurbs_curve *ufbx_as_nurbs_curve(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_NURBS_CURVE ? (ufbx_nurbs_curve*)element : NULL; }
ufbx_abi ufbx_nurbs_surface *ufbx_as_nurbs_surface(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_NURBS_SURFACE ? (ufbx_nurbs_surface*)element : NULL; }
ufbx_abi ufbx_nurbs_trim_surface *ufbx_as_nurbs_trim_surface(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_NURBS_TRIM_SURFACE ? (ufbx_nurbs_trim_surface*)element : NULL; }
ufbx_abi ufbx_nurbs_trim_boundary *ufbx_as_nurbs_trim_boundary(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_NURBS_TRIM_BOUNDARY ? (ufbx_nurbs_trim_boundary*)element : NULL; }
ufbx_abi ufbx_procedural_geometry *ufbx_as_procedural_geometry(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_PROCEDURAL_GEOMETRY ? (ufbx_procedural_geometry*)element : NULL; }
ufbx_abi ufbx_stereo_camera *ufbx_as_stereo_camera(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_STEREO_CAMERA ? (ufbx_stereo_camera*)element : NULL; }
ufbx_abi ufbx_camera_switcher *ufbx_as_camera_switcher(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CAMERA_SWITCHER ? (ufbx_camera_switcher*)element : NULL; }
ufbx_abi ufbx_marker *ufbx_as_marker(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_MARKER ? (ufbx_marker*)element : NULL; }
ufbx_abi ufbx_lod_group *ufbx_as_lod_group(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_LOD_GROUP ? (ufbx_lod_group*)element : NULL; }
ufbx_abi ufbx_skin_deformer *ufbx_as_skin_deformer(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_SKIN_DEFORMER ? (ufbx_skin_deformer*)element : NULL; }
ufbx_abi ufbx_skin_cluster *ufbx_as_skin_cluster(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_SKIN_CLUSTER ? (ufbx_skin_cluster*)element : NULL; }
ufbx_abi ufbx_blend_deformer *ufbx_as_blend_deformer(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_BLEND_DEFORMER ? (ufbx_blend_deformer*)element : NULL; }
ufbx_abi ufbx_blend_channel *ufbx_as_blend_channel(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_BLEND_CHANNEL ? (ufbx_blend_channel*)element : NULL; }
ufbx_abi ufbx_blend_shape *ufbx_as_blend_shape(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_BLEND_SHAPE ? (ufbx_blend_shape*)element : NULL; }
ufbx_abi ufbx_cache_deformer *ufbx_as_cache_deformer(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CACHE_DEFORMER ? (ufbx_cache_deformer*)element : NULL; }
ufbx_abi ufbx_cache_file *ufbx_as_cache_file(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CACHE_FILE ? (ufbx_cache_file*)element : NULL; }
ufbx_abi ufbx_material *ufbx_as_material(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_MATERIAL ? (ufbx_material*)element : NULL; }
ufbx_abi ufbx_texture *ufbx_as_texture(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_TEXTURE ? (ufbx_texture*)element : NULL; }
ufbx_abi ufbx_video *ufbx_as_video(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_VIDEO ? (ufbx_video*)element : NULL; }
ufbx_abi ufbx_shader *ufbx_as_shader(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_SHADER ? (ufbx_shader*)element : NULL; }
ufbx_abi ufbx_shader_binding *ufbx_as_shader_binding(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_SHADER_BINDING ? (ufbx_shader_binding*)element : NULL; }
ufbx_abi ufbx_anim_stack *ufbx_as_anim_stack(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_ANIM_STACK ? (ufbx_anim_stack*)element : NULL; }
ufbx_abi ufbx_anim_layer *ufbx_as_anim_layer(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_ANIM_LAYER ? (ufbx_anim_layer*)element : NULL; }
ufbx_abi ufbx_anim_value *ufbx_as_anim_value(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_ANIM_VALUE ? (ufbx_anim_value*)element : NULL; }
ufbx_abi ufbx_anim_curve *ufbx_as_anim_curve(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_ANIM_CURVE ? (ufbx_anim_curve*)element : NULL; }
ufbx_abi ufbx_display_layer *ufbx_as_display_layer(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_DISPLAY_LAYER ? (ufbx_display_layer*)element : NULL; }
ufbx_abi ufbx_selection_set *ufbx_as_selection_set(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_SELECTION_SET ? (ufbx_selection_set*)element : NULL; }
ufbx_abi ufbx_selection_node *ufbx_as_selection_node(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_SELECTION_NODE ? (ufbx_selection_node*)element : NULL; }
ufbx_abi ufbx_character *ufbx_as_character(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CHARACTER ? (ufbx_character*)element : NULL; }
ufbx_abi ufbx_constraint *ufbx_as_constraint(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CONSTRAINT ? (ufbx_constraint*)element : NULL; }
ufbx_abi ufbx_audio_layer *ufbx_as_audio_layer(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_AUDIO_LAYER ? (ufbx_audio_layer*)element : NULL; }
ufbx_abi ufbx_audio_clip *ufbx_as_audio_clip(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_AUDIO_CLIP ? (ufbx_audio_clip*)element : NULL; }
ufbx_abi ufbx_pose *ufbx_as_pose(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_POSE ? (ufbx_pose*)element : NULL; }
ufbx_abi ufbx_metadata_object *ufbx_as_metadata_object(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_METADATA_OBJECT ? (ufbx_metadata_object*)element : NULL; }

// -- String API

ufbx_abi ufbx_prop *ufbx_find_prop(const ufbx_props *props, const char *name) { return ufbx_find_prop_len(props, name, strlen(name)); }
ufbx_abi ufbx_real ufbx_find_real(const ufbx_props *props, const char *name, ufbx_real def) { return ufbx_find_real_len(props, name, strlen(name), def); }
ufbx_abi ufbx_vec3 ufbx_find_vec3(const ufbx_props *props, const char *name, ufbx_vec3 def) { return ufbx_find_vec3_len(props, name, strlen(name), def); }
ufbx_abi int64_t ufbx_find_int(const ufbx_props *props, const char *name, int64_t def) { return ufbx_find_int_len(props, name, strlen(name), def); }
ufbx_abi bool ufbx_find_bool(const ufbx_props *props, const char *name, bool def) { return ufbx_find_bool_len(props, name, strlen(name), def); }
ufbx_abi ufbx_string ufbx_find_string(const ufbx_props *props, const char *name, ufbx_string def) { return ufbx_find_string_len(props, name, strlen(name), def); }
ufbx_abi ufbx_blob ufbx_find_blob(const ufbx_props *props, const char *name, ufbx_blob def) { return ufbx_find_blob_len(props, name, strlen(name), def); }
ufbx_abi ufbx_element *ufbx_find_prop_element(const ufbx_element *element, const char *name, ufbx_element_type type) { return ufbx_find_prop_element_len(element, name, strlen(name), type); }
ufbx_abi ufbx_element *ufbx_find_element(const ufbx_scene *scene, ufbx_element_type type, const char *name) { return ufbx_find_element_len(scene, type, name, strlen(name)); }
ufbx_abi ufbx_node *ufbx_find_node(const ufbx_scene *scene, const char *name) { return ufbx_find_node_len(scene, name, strlen(name)); }
ufbx_abi ufbx_anim_stack *ufbx_find_anim_stack(const ufbx_scene *scene, const char *name) { return ufbx_find_anim_stack_len(scene, name, strlen(name)); }
ufbx_abi ufbx_material *ufbx_find_material(const ufbx_scene *scene, const char *name) { return ufbx_find_material_len(scene, name, strlen(name)); }
ufbx_abi ufbx_anim_prop *ufbx_find_anim_prop(const ufbx_anim_layer *layer, const ufbx_element *element, const char *prop) { return ufbx_find_anim_prop_len(layer, element, prop, strlen(prop)); }
ufbx_abi ufbx_prop ufbx_evaluate_prop(const ufbx_anim *anim, const ufbx_element *element, const char *name, double time) { return ufbx_evaluate_prop_len(anim, element, name, strlen(name), time); }
ufbx_abi ufbx_texture *ufbx_find_prop_texture(const ufbx_material *material, const char *name) { return ufbx_find_prop_texture_len(material, name, strlen(name)); }
ufbx_abi ufbx_string ufbx_find_shader_prop(const ufbx_shader *shader, const char *name) { return ufbx_find_shader_prop_len(shader, name, strlen(name)); }
ufbx_abi ufbx_shader_prop_binding_list ufbx_find_shader_prop_bindings(const ufbx_shader *shader, const char *name) { return ufbx_find_shader_prop_bindings_len(shader, name, strlen(name)); }
ufbx_abi ufbx_shader_texture_input *ufbx_find_shader_texture_input(const ufbx_shader_texture *shader, const char *name) { return ufbx_find_shader_texture_input_len(shader, name, strlen(name)); }
ufbx_abi ufbx_dom_node *ufbx_dom_find(const ufbx_dom_node *parent, const char *name) { return ufbx_dom_find_len(parent, name, strlen(name)); }

// -- Catch API

ufbx_abi uint32_t ufbx_triangulate_face(uint32_t *indices, size_t num_indices, const ufbx_mesh *mesh, ufbx_face face) {
	return ufbx_catch_triangulate_face(NULL, indices, num_indices, mesh, face);
}
ufbx_abi void ufbx_compute_topology(const ufbx_mesh *mesh, ufbx_topo_edge *topo, size_t num_topo) {
	ufbx_catch_compute_topology(NULL, mesh, topo, num_topo);
}
ufbx_abi uint32_t ufbx_topo_next_vertex_edge(const ufbx_topo_edge *topo, size_t num_topo, uint32_t index) {
	return ufbx_catch_topo_next_vertex_edge(NULL, topo, num_topo, index);
}
ufbx_abi uint32_t ufbx_topo_prev_vertex_edge(const ufbx_topo_edge *topo, size_t num_topo, uint32_t index) {
	return ufbx_catch_topo_prev_vertex_edge(NULL, topo, num_topo, index);
}
ufbx_abi ufbx_vec3 ufbx_get_weighted_face_normal(const ufbx_vertex_vec3 *positions, ufbx_face face) {
	return ufbx_catch_get_weighted_face_normal(NULL, positions, face);
}

#ifdef __cplusplus
}
#endif

#endif

#if defined(UFBX_STRING_PREFIX)
	#undef strlen
	#undef memcpy
	#undef memmove
	#undef memset
	#undef memchr
	#undef memcmp
	#undef strcmp
	#undef strncmp
#endif

#if defined(_MSC_VER)
	#pragma warning(pop)
#elif defined(__clang__)
	#pragma clang diagnostic pop
#elif defined(__GNUC__)
	#pragma GCC diagnostic pop
#endif

