godot/thirdparty/ufbx/ufbx.c

#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

// 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
#define UFBXI_MAX_NODE_DEPTH
#define UFBXI_MAX_XML_DEPTH
#define UFBXI_MAX_SKIP_SIZE
#define UFBXI_MAP_MAX_SCAN
#define UFBXI_KD_FAST_DEPTH
#define UFBXI_HUGE_MAX_SCAN
#define UFBXI_MIN_FILE_FORMAT_LOOKAHEAD
#define UFBXI_FACE_GROUP_HASH_BITS
#define UFBXI_MIN_THREADED_DEFLATE_BYTES
#define UFBXI_MIN_THREADED_ASCII_VALUES
#define UFBXI_GEOMETRY_CACHE_BUFFER_SIZE

#ifndef UFBXI_MAX_NURBS_ORDER
#define UFBXI_MAX_NURBS_ORDER
#endif

// By default enough to have squares be non-denormal
#ifndef UFBX_EPSILON
#define UFBX_EPSILON
#endif

// -- Feature exclusion

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

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

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

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

// Derived features

#if UFBXI_FEATURE_GEOMETRY_CACHE
	#define UFBXI_FEATURE_XML
#else
	#define UFBXI_FEATURE_XML
#endif

#if UFBXI_FEATURE_TRIANGULATION
	#define UFBXI_FEATURE_KD
#else
	#define UFBXI_FEATURE_KD
#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
#endif

// -- Headers

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <locale.h>
#include <float.h>

#if !defined(UFBX_NO_MATH_H)
	#include <math.h>
	#define UFBX_INFINITY
	#define UFBX_NAN
#endif

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

#define ufbxi_math_cat2(a, b)
#define ufbxi_math_cat(a, b)
#define ufbxi_math_fn(name)

#if !defined(UFBX_NO_MATH_DEFINES)
	#define ufbx_sqrt
	#define ufbx_fabs
	#define ufbx_pow
	#define ufbx_sin
	#define ufbx_cos
	#define ufbx_tan
	#define ufbx_asin
	#define ufbx_acos
	#define ufbx_atan
	#define ufbx_atan2
	#define ufbx_copysign
	#define ufbx_fmin
	#define ufbx_fmax
	#define ufbx_nextafter
	#define ufbx_rint
	#define ufbx_ceil
	#define ufbx_isnan
#endif

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

#if !defined(UFBX_INFINITY)
	#define UFBX_INFINITY
#endif
#if !defined(UFBX_NAN)
	#define UFBX_NAN
#endif

// -- Platform

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

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

#if !defined(UFBX_STANDARD_C) && defined(_MSC_VER)
	#define ufbxi_noinline
	#define ufbxi_forceinline
	#define ufbxi_restrict
	#if defined(_Check_return_)
		#define ufbxi_nodiscard
	#else
		#define ufbxi_nodiscard
	#endif
	#define ufbxi_unused
	#define ufbxi_unlikely
#elif !defined(UFBX_STANDARD_C) && (defined(__GNUC__) || defined(__clang__))
	#define ufbxi_noinline
	#define ufbxi_forceinline
	#define ufbxi_restrict
	#define ufbxi_nodiscard
	#define ufbxi_unused
	#define ufbxi_unlikely(cond)
#else
	#define ufbxi_noinline
	#define ufbxi_forceinline
	#define ufbxi_nodiscard
	#define ufbxi_restrict
	#define ufbxi_unused
	#define ufbxi_unlikely
#endif

#if !defined(UFBX_STANDARD_C) && defined(__clang__)
	#define ufbxi_nounroll
#elif !defined(UFBX_STANDARD_C) && UFBXI_GNUC >= 8
	#define ufbxi_nounroll
#elif !defined(UFBX_STANDARD_C) && defined(_MSC_VER)
	#define ufbxi_nounroll
#else
	#define ufbxi_nounroll
#endif

#if defined(__GNUC__) && !defined(__clang__)
	#define ufbxi_ignore
#else
	#define ufbxi_ignore(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
	#else
		#define ufbx_static_assert(desc, cond)
	#endif
#endif

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

#if defined(__SANITIZE_UNDEFINED__)  && !defined(UFBX_UBSAN)
	#define UFBX_UBSAN
#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
#endif

#if defined(UFBX_STATIC_ANALYSIS)
	bool ufbxi_analysis_opaque;
	#define ufbxi_maybe_null
#else
	#define ufbxi_maybe_null(ptr)
#endif

#if !defined(ufbxi_trace)
	#if defined(UFBX_TRACE)
		#define ufbxi_trace
	#else
		#define ufbxi_trace(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(_MSC_VER) && defined(_M_X64)) || ((defined(__GNUC__) || defined(__clang__)) && defined(__x86_64__)) || defined(UFBX_USE_SSE)
	#define UFBXI_HAS_SSE
	#include <xmmintrin.h>
	#include <emmintrin.h>
#else
	#define UFBXI_HAS_SSE
#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
	#else
		#define UFBX_LITTLE_ENDIAN
	#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)

// 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
	#endif
#elif !defined(UFBX_STANDARD_C) && defined(__clang__)
	#if (__clang_major__ >= 4) || (__clang_major__ == 3 && __clang_minor__ >= 3)
		#define UFBXI_HAS_ATTRIBUTE_ALIGNED
	#endif
#elif !defined(UFBX_STANDARD_C) && defined(__GNUC__)
	#if __GNUC__ >= 5
		#define UFBXI_HAS_ATTRIBUTE_ALIGNED
	#endif
#endif

#if defined(UFBXI_HAS_ATTRIBUTE_ALIGNED)
	#define UFBXI_HAS_UNALIGNED
	#define ufbxi_unaligned
	ufbxi_unaligned_u16;
	ufbxi_unaligned_u32;
	ufbxi_unaligned_u64;
	ufbxi_unaligned_f32;
	ufbxi_unaligned_f64;
#elif !defined(UFBX_STANDARD_C) && defined(_MSC_VER)
	#define UFBXI_HAS_UNALIGNED
	#if defined(_M_IX86)
		// MSVC seems to assume all pointers are unaligned for x86
		#define ufbxi_unaligned
	#else
		#define ufbxi_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)
	#define ufbxi_read_u32(ptr)
	#define ufbxi_read_u64(ptr)
	#define ufbxi_read_f32(ptr)
	#define ufbxi_read_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)
#define ufbxi_read_i16(ptr)
#define ufbxi_read_i32(ptr)
#define ufbxi_read_i64(ptr)

ufbx_static_assert();
ufbx_static_assert();
ufbx_static_assert();
ufbx_static_assert();
ufbx_static_assert();
ufbx_static_assert();
ufbx_static_assert();
ufbx_static_assert();
ufbx_static_assert();
ufbx_static_assert();
ufbx_static_assert();
ufbx_static_assert();

// -- Version

#define UFBX_SOURCE_VERSION
ufbx_abi_data_def const uint32_t ufbx_source_version =;

ufbx_static_assert();

// -- Fast copy

#if UFBXI_HAS_SSE
	#define ufbxi_copy_16_bytes(dst, src)
#elif defined(UFBXI_HAS_UNALIGNED)
	#define ufbxi_copy_16_bytes
#else
	#define ufbxi_copy_16_bytes
#endif


// -- Large fast integer

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

// -- Wrapping right shift

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

// -- Atomic counter

#define UFBXI_THREAD_SAFE

#if defined(__cplusplus)
	#define ufbxi_extern_c
#else
	#define ufbxi_extern_c
#endif

#if !defined(UFBX_STANDARD_C) && (defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER))
	ufbxi_atomic_counter;
	#define ufbxi_atomic_counter_init(ptr)
	#define ufbxi_atomic_counter_free(ptr)
	#define ufbxi_atomic_counter_inc(ptr)
	#define ufbxi_atomic_counter_dec(ptr)
	#define ufbxi_atomic_counter_load(ptr)
#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
		#define ufbxi_atomic_counter_free
		#define ufbxi_atomic_counter_inc
		#define ufbxi_atomic_counter_dec
		#define ufbxi_atomic_counter_load
	#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
		#define ufbxi_atomic_counter_free
		#define ufbxi_atomic_counter_inc
		#define ufbxi_atomic_counter_dec
		#define ufbxi_atomic_counter_load
	#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
	#define ufbxi_atomic_counter_free
	#define ufbxi_atomic_counter_inc
	#define ufbxi_atomic_counter_dec
	#define ufbxi_atomic_counter_load
#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
	#define ufbxi_atomic_counter_free
	#define ufbxi_atomic_counter_inc
	#define ufbxi_atomic_counter_dec
	#define ufbxi_atomic_counter_load
#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
	#define ufbxi_atomic_counter_free
	#define ufbxi_atomic_counter_inc
	#define ufbxi_atomic_counter_dec
	#define ufbxi_atomic_counter_load
#else
	typedef volatile size_t ufbxi_atomic_counter;
	#define ufbxi_atomic_counter_init
	#define ufbxi_atomic_counter_free
	#define ufbxi_atomic_counter_inc
	#define ufbxi_atomic_counter_dec
	#define ufbxi_atomic_counter_load
	#undef UFBXI_THREAD_SAFE
	#define UFBXI_THREAD_SAFE
#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_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_lzcnt64(v)
#else
	// DeBrujin table lookup
	static const uint8_t ufbxi_lzcnt_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_lzcnt64(uint64_t v) {
		v |= v >> 1;
		v |= v >> 2;
		v |= v >> 4;
		v |= v >> 8;
		v |= v >> 16;
		v |= v >> 32;
		return ufbxi_lzcnt_table[(v * UINT64_C(0x03f79d71b4cb0a89)) >> 58];
	}
#endif

// -- Debug

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

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

	#undef UFBXI_MAP_MAX_SCAN
	#define UFBXI_MAP_MAX_SCAN

	#undef UFBXI_KD_FAST_DEPTH
	#define UFBXI_KD_FAST_DEPTH

	#undef UFBXI_FACE_GROUP_HASH_BITS
	#define UFBXI_FACE_GROUP_HASH_BITS
#endif

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

	#undef UFBXI_MIN_THREADED_ASCII_VALUES
	#define UFBXI_MIN_THREADED_ASCII_VALUES
#endif

#if defined(UFBX_REGRESSION)
	#define ufbxi_regression_assert
#else
	#define ufbxi_regression_assert(cond)
#endif

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

#define ufbxi_unreachable(reason)

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

#if defined(_MSC_VER)
	#define ufbxi_thread_local
#elif defined(__GNUC__) || defined(__clang__)
	#define ufbxi_thread_local
#elif UFBXI_HAS_CPP11
	#define ufbxi_thread_local
#elif UFBX_STDC >= 201112L
	#define ufbxi_thread_local
#endif

#if defined(UFBXI_ANALYSIS_RECURSIVE)
	#define ufbxi_recursive_function
	#define ufbxi_recursive_function_void
#elif UFBXI_IS_REGRESSION && defined(ufbxi_thread_local)
	#define ufbxi_recursive_function
	#define ufbxi_recursive_function_void
#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
	#define ufbxi_sub_ptr
#else
	#define ufbxi_add_ptr(ptr, offset)
	#define ufbxi_sub_ptr(ptr, offset)
#endif

#define ufbxi_arraycount(arr)
#define ufbxi_for(m_type, m_name, m_begin, m_num)
#define ufbxi_for_ptr(m_type, m_name, m_begin, m_num)

// WARNING: Evaluates `m_list` twice!
#define ufbxi_for_list(m_type, m_name, m_list)
#define ufbxi_for_ptr_list(m_type, m_name, m_list)

#define ufbxi_string_literal(str)

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

static ufbxi_forceinline int32_t ufbxi_f64_to_i32(double value)
{}

static ufbxi_forceinline int64_t ufbxi_f64_to_i64(double value)
{}

#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)
#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)

#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)

#define ufbxi_macro_upper_bound_eq(m_type, m_linear_size, m_result_ptr, m_data, m_begin, m_size, m_eq_lambda)

ufbxi_less_fn;

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)
{}

static ufbxi_forceinline void ufbxi_swap(void *a, void *b, size_t 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)
{}

// -- Float parsing
//
// Custom float parsing that handles floats up to (-)ddddddddddddddddddd.ddddddddddddddddddd
// If larger or scientific notation is used then it defers to `strtod()`.
// For the algorithm we need 128-bit division that is either provided by hardware on x64 or
// a custom implementation below.

#if !defined(UFBX_STANDARD_C) && UFBXI_MSC_VER >= 1920 && defined(_M_X64) && !defined(__clang__)
	ufbxi_extern_c extern unsigned __int64 __cdecl _udiv128(unsigned __int64  highdividend,
		unsigned __int64 lowdividend, unsigned __int64 divisor, unsigned __int64 *remainder);
	#define ufbxi_div128
#elif !defined(UFBX_STANDARD_C) && (defined(__GNUC__) || defined(__clang__)) && (defined(__x86_64__) || defined(_M_X64))
	static ufbxi_forceinline uint64_t ufbxi_div128(uint64_t a_hi, uint64_t a_lo, uint64_t b, uint64_t *p_rem) {}
#else
	static ufbxi_forceinline uint64_t ufbxi_div128(uint64_t a_hi, uint64_t a_lo, uint64_t b, uint64_t *p_rem) {
		// Divide `(a_hi << 64 | a_lo)` by `b`, returns quotinent and stores reminder in `p_rem`.
		// Based on TAOCP 2.4 multi-word division single algorithm digit step.
		//
		// Notation:
		//   b is the base (2^32) in this case
		//   aN is the Nth digit (base b) of a from the least significant digit
		//   { x y z } is a multi-digit number b^2*x + b*y + z
		//   ie. for a 64-bit number a = { a1 a0 } = b*a1 + a0
		//
		// We do the division in two steps by dividing three digits in each iteration:
		//
		//   q1, r = { a3 a2 a1 } / { b1 b0 }
		//   q0, r = { r1 r0 a0 } / { b1 b0 }
		//
		// In each step we want to compute the expression:
		//
		//   q, r = { u2 u1 u0 } / { v1 v0 }
		//
		// However we cannot rely on being able to do `u96 / u64` division we estimate
		// the result by considering only the leading digits:
		//
		//   q^ = { u2 u1 } / v1                                       [A]
		//   r^ = { u2 u1 } % v1 = { u2 u1 } - v1 * q^                 [B]
		//
		// As long as `v1 >= b/2` the estimate `q^` is at most two larger than the actual `q`
		// (proof in TAOCP 2.4) so we can compute the correction amount `c`:
		//
		//   q <= q^ <= q + 2
		//   q = q^ - c                                                [C]
		//
		// We can compute the final remainder (that must be non-negative) as follows:
		//
		//   r = { u2 u1 u0 } - v*q
		//   r = { u2 u1 u0 } - v*(q^ - c)
		//   r = { u2 u1 u0 } - v*q^ + v*c
		//   r = { u2 u1 u0 } - { v1 v0 } * q^ + v*c
		//   r = b^2*u2 + b*u1 + u0 - b*v1*q^ - v0*q^ + v*c
		//   r = b*(b*u2 + u1 - v1*q^) + u0 - v0*q^ + v*c
		//   r = b*({ u2 u1 } - v1*q^) + u0 - v0*q^ + v*c
		//   r = b*r^ + u0 - v0*q^ + v*c
		//   r = { r^ u0 } - v0*q^ + v*c                               [D]
		//
		// As we know `0 <= c <= 2` we can first check if `r < 0` requiring `c >= 1`:
		//
		//   { r^ u0 } - v0*q^ < 0
		//   { r^ u0 } < v0*q^                                         [E]
		//
		// If we know that `r < 0` we can check if `r < -v` requiring `c = 2`:
		//
		//   { r^ u0 } - v0*q^ < -v
		//   v0*q^ - { r^ u0 } > v                                     [F]
		//

		// First we need to make sure `v1 >= b/2`, we can do this by multiplying the whole
		// expression by `2^shift` so that the high bit of `v` is set.
		uint32_t shift = ufbxi_lzcnt64(b);
		a_hi = (a_hi << shift) | (shift ? a_lo >> (64 - shift) : 0);
		a_lo <<= shift;
		b <<= shift;

		uint64_t v = b;
		uint32_t v1 = (uint32_t)(v >> 32);
		uint32_t v0 = (uint32_t)(v);
		uint64_t q1, q0, r;

		// q1, r = { a3 a2 a1 } / { b1 b0 }
		{
			uint64_t u2_u1 = a_hi;
			uint32_t u0 = (uint32_t)(a_lo >> 32u);

			uint64_t qh = u2_u1 / v1;            // q^ = { u2 u1 } / v1          [A]
			uint64_t rh = u2_u1 % v1;            // r^ = { u2 u1 } % v1          [B]
			uint64_t rh_u0 = rh << 32u | u0;     // { r^ u0 }
			uint64_t v0qh = v0 * qh;             // v0*q^
			uint32_t c = rh_u0 < v0qh ? 1 : 0;   // { r^ u0 } < v0*q^            [E]
			c += c & (v0qh - rh_u0 > v ? 1 : 0); // v0*q^ - { r^ u0 } > v        [F]
			q1 = qh - c;                         // q1 = q^ - c                  [C]
			r = rh_u0 - v0qh + v*c;              // r = { r^ u0 } - v0*q^ + v*c  [D]
		}

		// q0, r = { r1 r0 a0 } / { b1 b0 }
		{
			uint64_t u2_u1 = r;
			uint32_t u0 = (uint32_t)a_lo;

			uint64_t qh = u2_u1 / v1;            // q^ = { u2 u1 } / v1          [A]
			uint64_t rh = u2_u1 % v1;            // r^ = { u2 u1 } % v1          [B]
			uint64_t rh_u0 = rh << 32u | u0;     // { r^ u0 }
			uint64_t v0qh = v0 * qh;             // v0*q^
			uint32_t c = rh_u0 < v0qh ? 1 : 0;   // { r^ u0 } < v0*q^            [E]
			c += c & (v0qh - rh_u0 > v ? 1 : 0); // v0*q^ - { r^ u0 } > v        [F]
			q0 = qh - c;                         // q0 = q^ - c                  [C]
			r = rh_u0 - v0qh + v*c;              // r = { r^ u0 } - v0*q^ + v*c  [D]
		}

		// Un-normalize the remainder and return the quotinent
		*p_rem = r >> shift;
		return q1 << 32u | q0;
	}
#endif

ufbxi_parse_double_flag;

static const uint64_t ufbxi_pow5_tab[] =;
static const int8_t ufbxi_pow2_tab[] =;
static const double ufbxi_pow10_tab_f64[] =;

static ufbxi_noinline uint32_t ufbxi_parse_double_init_flags()
{}

static ufbxi_noinline double ufbxi_parse_double_slow(const char *str, char **end)
{}

static ufbxi_noinline double ufbxi_parse_double(const char *str, size_t max_length, char **end, uint32_t flags)
{}

static ufbxi_forceinline int64_t ufbxi_parse_int64(const char *str, char **end)
{}

// -- 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[] =;
static const uint32_t ufbxi_deflate_dist_lut[] =;

static const uint8_t ufbxi_deflate_code_length_permutation[] =;

#define UFBXI_INFLATE_FAST_MIN_IN
#define UFBXI_INFLATE_FAST_MIN_OUT

#define UFBXI_HUFF_MAX_BITS
#define UFBXI_HUFF_MAX_VALUE
#define UFBXI_HUFF_FAST_BITS
#define UFBXI_HUFF_FAST_SIZE
#define UFBXI_HUFF_FAST_MASK
#define UFBXI_HUFF_MAX_LONG_BITS
#define UFBXI_HUFF_MAX_LONG_SYMS

#define UFBXI_HUFF_CODELEN_FAST_BITS
#define UFBXI_HUFF_CODELEN_FAST_MASK

#define UFBXI_HUFF_MAX_EXTRA_SYMS

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,
//
ufbxi_huff_sym;

#define ufbxi_huff_sym_total_bits(sym)
#define ufbxi_huff_sym_long_mask(sym)
#define ufbxi_huff_sym_long_offset(sym)
#define ufbxi_huff_sym_value(sym)

enum {};

#define UFBXI_HUFF_ERROR_SYM
#define UFBXI_HUFF_UNINITIALIZED_SYM

ufbxi_huff_tree;

ufbxi_trees;

ufbxi_inflate_retain_imp;

ufbx_static_assert();

ufbxi_deflate_context;

static ufbxi_forceinline uint32_t
ufbxi_bit_reverse(uint32_t mask, uint32_t num_bits)
{}

static ufbxi_noinline const char *
ufbxi_bit_chunk_refill(ufbxi_bit_stream *s, const char *ptr)
{}

static ufbxi_noinline void ufbxi_bit_stream_init(ufbxi_bit_stream *s, const ufbx_inflate_input *input)
{}

static ufbxi_noinline const char *
ufbxi_bit_yield(ufbxi_bit_stream *s, const char *ptr)
{}

static ufbxi_forceinline void
ufbxi_bit_refill(uint64_t *p_bits, size_t *p_left, const char **p_data, ufbxi_bit_stream *s)
{}

// See `ufbxi_bit_refill()`
#define ufbxi_macro_bit_refill_fast(m_bits, m_left, m_data, m_refill_bits)

static ufbxi_noinline int
ufbxi_bit_copy_bytes(void *dst, ufbxi_bit_stream *s, size_t 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)
{}

// 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)
{}

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)
{}

static ufbxi_noinline void ufbxi_init_static_huff(ufbxi_trees *trees, const ufbx_inflate_input *input)
{}

// 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)
{}

static ufbxi_noinline ptrdiff_t
ufbxi_init_dynamic_huff(ufbxi_deflate_context *dc, ufbxi_trees *trees)
{}

static ufbxi_noinline uint32_t ufbxi_adler32(const void *data, size_t size)
{}

static ufbxi_noinline int
ufbxi_inflate_block_slow(ufbxi_deflate_context *dc, ufbxi_trees *trees, size_t max_symbols)
{}

ufbx_static_assert(); // `fast lit, fast len, slow dist` in 56 bits
ufbx_static_assert(); // 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)
{}

static void ufbxi_inflate_init_retain(ufbx_inflate_retain *retain)
{}

// 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)
{}

#endif // !defined(ufbx_inflate)

// -- Errors

static const char ufbxi_empty_char[1] =;

static ufbxi_noinline int ufbxi_vsnprintf(char *buf, size_t buf_size, const char *fmt, va_list args)
{}

static ufbxi_noinline int ufbxi_snprintf(char *buf, size_t buf_size, const char *fmt, ...)
{}

static ufbxi_noinline void ufbxi_panicf_imp(ufbx_panic *panic, const char *fmt, ...)
{}

#define ufbxi_panicf(panic, cond, ...)

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

static ufbxi_noinline int ufbxi_fail_imp_err(ufbx_error *err, const char *cond, const char *func, uint32_t line)
{}

ufbxi_nodiscard static ufbxi_noinline size_t ufbxi_utf8_valid_length(const char *str, size_t length)
{}

static ufbxi_noinline void ufbxi_clean_string_utf8(char *str, size_t length)
{}

static ufbxi_noinline void ufbxi_set_err_info(ufbx_error *err, const char *data, size_t length)
{}

static ufbxi_noinline void ufbxi_fmt_err_info(ufbx_error *err, const char *fmt, ...)
{}

static ufbxi_noinline void ufbxi_clear_error(ufbx_error *err)
{}

#if UFBXI_FEATURE_ERROR_STACK
	#define ufbxi_function
	#define ufbxi_line
	#define ufbxi_cond_str
#else
	#define ufbxi_function
	#define ufbxi_line
	#define ufbxi_cond_str(cond)
#endif

#define ufbxi_check_err(err, cond)
#define ufbxi_check_return_err(err, cond, ret)
#define ufbxi_fail_err(err, desc)

#define ufbxi_check_err_msg(err, cond, msg)
#define ufbxi_check_return_err_msg(err, cond, ret, msg)
#define ufbxi_fail_err_msg(err, desc, msg)
#define ufbxi_report_err_msg(err, desc, msg)

static ufbxi_noinline void ufbxi_fix_error_type(ufbx_error *error, const char *default_desc)
{}

// -- 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] =;
#endif

static ufbxi_forceinline size_t ufbxi_align_to_mask(size_t value, size_t align_mask)
{}

static ufbxi_forceinline size_t ufbxi_size_align_mask(size_t size)
{}

ufbxi_allocator;

static ufbxi_forceinline bool ufbxi_does_overflow(size_t total, size_t a, size_t b)
{}

static ufbxi_noinline void *ufbxi_alloc_size(ufbxi_allocator *ator, size_t size, size_t n)
{}

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)
{}

static ufbxi_noinline void ufbxi_free_size(ufbxi_allocator *ator, size_t size, void *ptr, size_t n)
{}

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)
{}

static ufbxi_noinline void ufbxi_free_ator(ufbxi_allocator *ator)
{}

#define ufbxi_alloc(ator, type, n)
#define ufbxi_alloc_zero(ator, type, n)
#define ufbxi_realloc(ator, type, old_ptr, old_n, n)
#define ufbxi_realloc_zero(ator, type, old_ptr, old_n, n)
#define ufbxi_free(ator, type, ptr, n)

#define ufbxi_grow_array(ator, p_ptr, p_cap, n)

#define UFBXI_SCENE_IMP_MAGIC
#define UFBXI_MESH_IMP_MAGIC
#define UFBXI_LINE_CURVE_IMP_MAGIC
#define UFBXI_CACHE_IMP_MAGIC
#define UFBXI_ANIM_IMP_MAGIC
#define UFBXI_BAKED_ANIM_IMP_MAGIC
#define UFBXI_REFCOUNT_IMP_MAGIC
#define UFBXI_BUF_CHUNK_IMP_MAGIC

// -- 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]()`

ufbxi_buf_padding;
ufbxi_buf_chunk;

struct ufbxi_buf_padding {};

struct ufbxi_buf_chunk {};

ufbx_static_assert();

ufbxi_buf;

ufbxi_buf_state;

static ufbxi_noinline void *ufbxi_push_size_new_block(ufbxi_buf *b, size_t size)
{}

static ufbxi_noinline void *ufbxi_push_size(ufbxi_buf *b, size_t size, size_t n)
{}

static ufbxi_forceinline void *ufbxi_push_size_fast(ufbxi_buf *b, size_t size, size_t n)
{}

static ufbxi_noinline void *ufbxi_push_size_zero(ufbxi_buf *b, size_t size, size_t n)
{}

ufbxi_nodiscard static ufbxi_noinline void *ufbxi_push_size_copy(ufbxi_buf *b, size_t size, size_t n, const void *data)
{}

ufbxi_nodiscard static ufbxi_forceinline void *ufbxi_push_size_copy_fast(ufbxi_buf *b, size_t size, size_t n, const void *data)
{}

static ufbxi_noinline void ufbxi_buf_free_unused(ufbxi_buf *b)
{}

static ufbxi_noinline void ufbxi_pop_size(ufbxi_buf *b, size_t size, size_t n, void *dst, bool peek)
{}

static ufbxi_noinline void *ufbxi_push_pop_size(ufbxi_buf *dst, ufbxi_buf *src, size_t size, size_t n)
{}

static ufbxi_noinline void *ufbxi_push_peek_size(ufbxi_buf *dst, ufbxi_buf *src, size_t size, size_t n)
{}

static ufbxi_noinline void ufbxi_buf_free(ufbxi_buf *buf)
{}

static ufbxi_noinline void ufbxi_buf_clear(ufbxi_buf *buf)
{}

#define ufbxi_push(b, type, n)
#define ufbxi_push_zero(b, type, n)
#define ufbxi_push_copy(b, type, n, data)
#define ufbxi_push_copy_fast(b, type, n, data)
#define ufbxi_push_fast(b, type, n)
#define ufbxi_pop(b, type, n, dst)
#define ufbxi_peek(b, type, n, dst)
#define ufbxi_push_pop(dst, src, type, n)
#define ufbxi_push_peek(dst, src, 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.

ufbxi_aa_node;

ufbxi_cmp_fn;

struct ufbxi_aa_node {};

ufbxi_map;

static ufbxi_noinline void ufbxi_map_init(ufbxi_map *map, ufbxi_allocator *ator, ufbxi_cmp_fn *cmp_fn, void *cmp_user)
{}

static ufbxi_noinline void ufbxi_map_free(ufbxi_map *map)
{}

// 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))
{}

static ufbxi_noinline void *ufbxi_aa_tree_find(ufbxi_map *map, const void *value, size_t item_size)
{}

static ufbxi_noinline bool ufbxi_map_grow_size_imp(ufbxi_map *map, size_t item_size, size_t min_size)
{}

static ufbxi_forceinline bool ufbxi_map_grow_size(ufbxi_map *map, size_t size, size_t min_size)
{}

static ufbxi_noinline void *ufbxi_map_find_size(ufbxi_map *map, size_t size, uint32_t hash, const void *value)
{}

static ufbxi_noinline void *ufbxi_map_insert_size(ufbxi_map *map, size_t size, uint32_t hash, const void *value)
{}

#define ufbxi_map_grow(map, type, min_size)
#define ufbxi_map_find(map, type, hash, value)
#define ufbxi_map_insert(map, type, hash, value)

static int ufbxi_map_cmp_uint64(void *user, const void *va, const void *vb)
{}

static int ufbxi_map_cmp_const_char_ptr(void *user, const void *va, const void *vb)
{}

static int ufbxi_map_cmp_uintptr(void *user, const void *va, const void *vb)
{}

// -- Hash functions

static ufbxi_noinline uint32_t ufbxi_hash_string(const char *str, size_t length)
{}

// 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)
{}

static ufbxi_forceinline uint32_t ufbxi_hash32(uint32_t x)
{}

static ufbxi_forceinline uint32_t ufbxi_hash64(uint64_t x)
{}

static ufbxi_forceinline uint32_t ufbxi_hash_uptr(uintptr_t ptr)
{}

#define ufbxi_hash_ptr(ptr)

// -- Warnings

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)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_warnf_imp(ufbxi_warnings *ws, ufbx_warning_type type, uint32_t element_id, const char *fmt, ...)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_pop_warnings(ufbxi_warnings *ws, ufbx_warning_list *warnings, bool *p_has_warning)
{}

// -- 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.

ufbxi_string_pool;

ufbxi_sanitized_string;

static ufbxi_forceinline bool ufbxi_str_equal(ufbx_string a, ufbx_string b)
{}

static ufbxi_forceinline bool ufbxi_str_less(ufbx_string a, ufbx_string b)
{}

static ufbxi_forceinline int ufbxi_str_cmp(ufbx_string a, ufbx_string b)
{}

static ufbxi_forceinline ufbx_string ufbxi_str_c(const char *str)
{}

static ufbxi_noinline uint32_t ufbxi_get_concat_key(const ufbx_string *parts, size_t num_parts)
{}

static ufbxi_noinline int ufbxi_concat_str_cmp(const ufbx_string *ref, const ufbx_string *parts, size_t num_parts)
{}

static ufbxi_forceinline bool ufbxi_starts_with(ufbx_string str, ufbx_string prefix)
{}

static ufbxi_forceinline bool ufbxi_ends_with(ufbx_string str, ufbx_string suffix)
{}

static ufbxi_noinline bool ufbxi_remove_prefix_len(ufbx_string *str, const char *prefix, size_t prefix_len)
{}

static ufbxi_noinline bool ufbxi_remove_suffix_len(ufbx_string *str, const char *suffix, size_t suffix_len)
{}

static ufbxi_forceinline bool ufbxi_remove_prefix_str(ufbx_string *str, ufbx_string prefix)
{}

static ufbxi_forceinline bool ufbxi_remove_suffix_c(ufbx_string *str, const char *suffix)
{}

static int ufbxi_map_cmp_string(void *user, const void *va, const void *vb)
{}

static ufbxi_forceinline ufbx_string ufbxi_safe_string(const char *data, size_t length)
{}

static void ufbxi_string_pool_temp_free(ufbxi_string_pool *pool)
{}

ufbxi_nodiscard static size_t ufbxi_add_replacement_char(ufbxi_string_pool *pool, char *dst, char c)
{}

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)
{}

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_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)
{}

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)
{}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_push_string_place(ufbxi_string_pool *pool, const char **p_str, size_t *p_length, bool raw)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_string_place_str(ufbxi_string_pool *pool, ufbx_string *p_str, bool raw)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_string_place_blob(ufbxi_string_pool *pool, ufbx_blob *p_blob, bool raw)
{}

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

static const ufbx_string ufbxi_strings[] =;

static const ufbx_vec3 ufbxi_one_vec3 =;

#define UFBXI_PI
#define UFBXI_DPI
#define UFBXI_DEG_TO_RAD
#define UFBXI_RAD_TO_DEG
#define UFBXI_DEG_TO_RAD_DOUBLE
#define UFBXI_RAD_TO_DEG_DOUBLE
#define UFBXI_MM_TO_INCH

ufbx_inline ufbx_vec3 ufbxi_add3(ufbx_vec3 a, ufbx_vec3 b) {}

ufbx_inline ufbx_vec3 ufbxi_sub3(ufbx_vec3 a, ufbx_vec3 b) {}

ufbx_inline ufbx_vec3 ufbxi_mul3(ufbx_vec3 a, ufbx_real b) {}

ufbx_inline ufbx_vec3 ufbxi_lerp3(ufbx_vec3 a, ufbx_vec3 b, ufbx_real t) {}

ufbx_inline ufbx_real ufbxi_dot3(ufbx_vec3 a, ufbx_vec3 b) {}

ufbx_inline ufbx_real ufbxi_length3(ufbx_vec3 v)
{}

ufbx_inline ufbx_real ufbxi_min3(ufbx_vec3 v)
{}

ufbx_inline ufbx_vec3 ufbxi_cross3(ufbx_vec3 a, ufbx_vec3 b) {}

ufbx_inline ufbx_vec3 ufbxi_normalize3(ufbx_vec3 a) {}

ufbx_inline ufbx_real ufbxi_distsq2(ufbx_vec2 a, ufbx_vec2 b) {}

static ufbxi_noinline ufbx_vec3 ufbxi_slow_normalize3(const ufbx_vec3 *a) {}

static ufbxi_noinline ufbx_vec3 ufbxi_slow_normalized_cross3(const ufbx_vec3 *a, const ufbx_vec3 *b) {}

// -- Threading

ufbxi_task;
ufbxi_thread;
ufbxi_thread_pool;

ufbxi_task_fn;

struct ufbxi_task {};

ufbxi_task_imp;

ufbxi_task_group;

struct ufbxi_thread_pool {};

static void ufbxi_thread_pool_execute(ufbxi_thread_pool *pool, uint32_t index)
{}

ufbxi_noinline static void ufbxi_thread_pool_update_finished(ufbxi_thread_pool *pool, uint32_t max_index)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_thread_pool_wait_imp(ufbxi_thread_pool *pool, uint32_t group, bool can_fail)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_thread_pool_wait_group(ufbxi_thread_pool *pool)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_thread_pool_wait_all(ufbxi_thread_pool *pool)
{}

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)
{}

ufbxi_noinline static void ufbxi_thread_pool_free(ufbxi_thread_pool *pool)
{}

ufbxi_nodiscard ufbxi_noinline static uint32_t ufbxi_thread_pool_available_tasks(ufbxi_thread_pool *pool)
{}

ufbxi_noinline static void ufbxi_thread_pool_flush_group(ufbxi_thread_pool *pool)
{}

ufbxi_nodiscard ufbxi_noinline static ufbxi_task *ufbxi_thread_pool_create_task(ufbxi_thread_pool *pool, ufbxi_task_fn *fn)
{}

static void ufbxi_thread_pool_run_task(ufbxi_thread_pool *pool, ufbxi_task *task)
{}

// -- Type definitions

ufbxi_node;

ufbxi_value_type;

ufbxi_value;

ufbxi_value_array;

struct ufbxi_node {};

ufbxi_refcount;

struct ufbxi_refcount {};

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)

ufbxi_scene_imp;

ufbx_static_assert();

ufbxi_mesh_imp;

ufbx_static_assert();

ufbxi_ascii_token;

ufbxi_ascii;

ufbxi_template;

ufbxi_fbx_id_entry;

ufbxi_fbx_attr_entry;

// Temporary connection before we resolve the element pointers
ufbxi_tmp_connection;

ufbxi_element_info;

ufbxi_tmp_bone_pose;

ufbxi_tmp_mesh_texture;

ufbxi_mesh_extra;

ufbxi_tmp_material_texture;

ufbxi_texture_extra;

ufbxi_obj_attrib;

#define UFBXI_OBJ_NUM_ATTRIBS
#define UFBXI_OBJ_NUM_ATTRIBS_EXT

ufbxi_obj_index_range;

ufbxi_obj_mesh;

ufbxi_obj_group_entry;

ufbxi_obj_fast_indices;

// Temporary pointer to a `ufbx_anim_stack` by name used to patch start/stop
// time from "Takes" if necessary.
ufbxi_tmp_anim_stack;

ufbxi_file_content;

ufbxi_obj_context;

ufbxi_context;

static ufbxi_noinline int ufbxi_fail_imp(ufbxi_context *uc, const char *cond, const char *func, uint32_t line)
{}

#define ufbxi_check(cond)
#define ufbxi_check_return(cond, ret)
#define ufbxi_fail(desc)
#define ufbxi_fail_return(desc, ret)

#define ufbxi_check_msg(cond, msg)
#define ufbxi_check_return_msg(cond, ret, msg)
#define ufbxi_fail_msg(desc, msg)

#define ufbxi_warnf(type, ...)
#define ufbxi_warnf_tag(type, element_id, ...)

// -- Progress

static ufbxi_forceinline uint64_t ufbxi_get_read_offset(ufbxi_context *uc)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_report_progress(ufbxi_context *uc)
{}

// 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)
{}

// -- IO

static ufbxi_noinline const char *ufbxi_refill(ufbxi_context *uc, size_t size, bool require_size)
{}

static ufbxi_forceinline void ufbxi_pause_progress(ufbxi_context *uc)
{}

static ufbxi_noinline int ufbxi_resume_progress(ufbxi_context *uc)
{}

static ufbxi_noinline const char *ufbxi_yield(ufbxi_context *uc, size_t size)
{}

static ufbxi_forceinline const char *ufbxi_peek_bytes(ufbxi_context *uc, size_t size)
{}

static ufbxi_forceinline const char *ufbxi_read_bytes(ufbxi_context *uc, size_t size)
{}

static ufbxi_forceinline void ufbxi_consume_bytes(ufbxi_context *uc, size_t size)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_skip_bytes(ufbxi_context *uc, uint64_t size)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_to(ufbxi_context *uc, void *dst, size_t size)
{}

// -- File IO

static ufbxi_noinline void ufbxi_init_ator(ufbx_error *error, ufbxi_allocator *ator, const ufbx_allocator_opts *opts, const char *name)
{}

static ufbxi_noinline FILE *ufbxi_fopen(const char *path, size_t path_len, ufbxi_allocator *tmp_ator)
{}

static uint64_t ufbxi_ftell(FILE *file)
{}

static size_t ufbxi_file_read(void *user, void *data, size_t max_size)
{}

static bool ufbxi_file_skip(void *user, size_t size)
{}

static void ufbxi_file_close(void *user)
{}

ufbxi_memory_stream;

static size_t ufbxi_memory_read(void *user, void *data, size_t max_size)
{}

static bool ufbxi_memory_skip(void *user, size_t size)
{}

static void ufbxi_memory_close(void *user)
{}

// -- 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 = strtoul(entity + 2, NULL, 16);
				} else {
					code = strtoul(entity + 1, NULL, 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) {}

static ufbxi_noinline size_t ufbxi_array_type_size(char type)
{}

// -- Node operations

static ufbxi_noinline ufbxi_node *ufbxi_find_child(ufbxi_node *node, const char *name)
{}

// 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_nodiscard ufbxi_noinline static ufbxi_value_array *ufbxi_get_array(ufbxi_node *node, char fmt)
{}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val1(ufbxi_node *node, const char *fmt, void *v0)
{}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val2(ufbxi_node *node, const char *fmt, void *v0, void *v1)
{}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val3(ufbxi_node *node, const char *fmt, void *v0, void *v1, void *v2)
{}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val4(ufbxi_node *node, const char *fmt, void *v0, void *v1, void *v2, void *v3)
{}

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)
{}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_find_val1(ufbxi_node *node, const char *name, const char *fmt, void *v0)
{}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_find_val2(ufbxi_node *node, const char *name, const char *fmt, void *v0, void *v1)
{}

ufbxi_nodiscard static ufbxi_noinline ufbxi_value_array *ufbxi_find_array(ufbxi_node *node, const char *name, char fmt)
{}

static ufbxi_node *ufbxi_find_child_strcmp(ufbxi_node *node, const char *name)
{}

// -- Element extra data allocation

ufbxi_nodiscard static ufbxi_noinline void *ufbxi_push_element_extra_size(ufbxi_context *uc, uint32_t id, size_t size)
{}

static ufbxi_noinline void *ufbxi_get_element_extra(ufbxi_context *uc, uint32_t id)
{}

#define ufbxi_push_element_extra(uc, id, 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.

ufbxi_parse_state;

ufbxi_array_flags;

ufbxi_array_info;

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

static bool ufbxi_is_array_node(ufbxi_context *uc, ufbxi_parse_state parent, const char *name, ufbxi_array_info *info)
{}

static ufbxi_noinline bool ufbxi_is_raw_string(ufbxi_context *uc, ufbxi_parse_state parent, const char *name, size_t index)
{}

// -- Binary parsing

ufbxi_nodiscard static ufbxi_noinline char *ufbxi_swap_endian(ufbxi_context *uc, const void *src, size_t count, size_t elem_size)
{}

// 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)
{}

// 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)
{}

// 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)
{}

// 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)
{}

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)
{}

ufbxi_noinline static void ufbxi_postprocess_bool_array(char *data, size_t size)
{}

ufbxi_deflate_task;

static bool ufbxi_deflate_task_fn(ufbxi_task *task)
{}

// 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))
{}

#define UFBXI_BINARY_MAGIC_SIZE
#define UFBXI_BINARY_HEADER_SIZE
static const char ufbxi_binary_magic[] =;

// -- ASCII parsing

#define UFBXI_ASCII_END
#define UFBXI_ASCII_NAME
#define UFBXI_ASCII_BARE_WORD
#define UFBXI_ASCII_INT
#define UFBXI_ASCII_FLOAT
#define UFBXI_ASCII_STRING

static ufbxi_noinline char ufbxi_ascii_refill(ufbxi_context *uc)
{}

static ufbxi_noinline char ufbxi_ascii_yield(ufbxi_context *uc)
{}

static ufbxi_forceinline char ufbxi_ascii_peek(ufbxi_context *uc)
{}

static ufbxi_forceinline char ufbxi_ascii_next(ufbxi_context *uc)
{}

static ufbxi_noinline uint32_t ufbxi_ascii_parse_version(ufbxi_context *uc)
{}

static const uint32_t ufbxi_space_mask = ;

ufbx_static_assert();

static ufbxi_forceinline bool ufbxi_is_space(char c)
{}

static ufbxi_noinline char ufbxi_ascii_skip_whitespace(ufbxi_context *uc)
{}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_ascii_push_token_char(ufbxi_context *uc, ufbxi_ascii_token *token, char c)
{}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_ascii_push_token_string(ufbxi_context *uc, ufbxi_ascii_token *token, const char *data, size_t length)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_skip_until(ufbxi_context *uc, char dst)
{}

ufbxi_ascii_span;

ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_store_array(ufbxi_context *uc, ufbxi_buf *tmp_buf)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_try_ignore_string(ufbxi_context *uc, ufbxi_ascii_token *token)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_next_token(ufbxi_context *uc, ufbxi_ascii_token *token)
{}

ufbxi_nodiscard static int ufbxi_ascii_accept(ufbxi_context *uc, char type)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_ascii_read_int_array(ufbxi_context *uc, char type, size_t *p_num_read)
{}

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)
{}

ufbxi_noinline static const char *ufbxi_ascii_array_task_parse_ints(ufbxi_ascii_array_task *t, const char *src, const char *src_end)
{}

ufbxi_noinline static const char *ufbxi_ascii_array_task_parse(ufbxi_ascii_array_task *t, const char *src, const char *src_end)
{}

ufbxi_ascii_scan_state;

ufbxi_noinline static bool ufbxi_ascii_array_task_imp(ufbxi_ascii_array_task *t)
{}

ufbxi_noinline static bool ufbxi_ascii_array_task_fn(ufbxi_task *task)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_ascii_read_float_array(ufbxi_context *uc, char type, size_t *p_num_read)
{}

// 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))
{}

// -- DOM retention

ufbxi_dom_mapping;

ufbxi_nodiscard static ufbxi_noinline ufbx_dom_node *ufbxi_get_dom_node_imp(ufbxi_context *uc, ufbxi_node *node)
{}

ufbxi_nodiscard static ufbxi_forceinline ufbx_dom_node *ufbxi_get_dom_node(ufbxi_context *uc, ufbxi_node *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))
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_retain_toplevel(ufbxi_context *uc, ufbxi_node *node)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_retain_toplevel_child(ufbxi_context *uc, ufbxi_node *child)
{}

// -- General parsing

static ufbxi_noinline bool ufbxi_next_line(ufbx_string *line, ufbx_string *buf, bool skip_space)
{}

// 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))
{}

// 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))
{}

static ufbxi_noinline bool ufbxi_match(const ufbx_string *str, const char *fmt)
{}

static ufbxi_noinline bool ufbxi_is_format(const char *data, size_t size, ufbx_file_format format)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_determine_format(ufbxi_context *uc)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_begin_parse(ufbxi_context *uc)
{}

ufbxi_nodiscard static int ufbxi_parse_toplevel_child_imp(ufbxi_context *uc, ufbxi_parse_state state, ufbxi_buf *buf, bool *p_end)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_parse_toplevel(ufbxi_context *uc, const char *name)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_parse_toplevel_child(ufbxi_context *uc, ufbxi_node **p_node, ufbxi_buf *tmp_buf)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_parse_legacy_toplevel(ufbxi_context *uc)
{}

// -- Setup

ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_strings(ufbxi_context *uc)
{}

ufbxi_prop_type_name;

static const ufbxi_prop_type_name ufbxi_prop_type_names[] =;

static ufbx_prop_type ufbxi_get_prop_type(ufbxi_context *uc, const char *name)
{}

static ufbxi_noinline ufbx_prop *ufbxi_find_prop_with_key(const ufbx_props *props, const char *name, uint32_t key)
{}

ufbxi_texture_file_entry;

#define ufbxi_find_prop(props, name)

static ufbxi_forceinline ufbx_real ufbxi_find_real(const ufbx_props *props, const char *name, ufbx_real 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)
{}

static ufbxi_forceinline int64_t ufbxi_find_int(const ufbx_props *props, const char *name, int64_t def)
{}

static ufbxi_forceinline int64_t ufbxi_find_enum(const ufbx_props *props, const char *name, int64_t def, int64_t max_value)
{}

ufbxi_noinline static bool ufbxi_matrix_all_zero(const ufbx_matrix *matrix)
{}

static ufbxi_forceinline bool ufbxi_is_vec3_zero(ufbx_vec3 v)
{}

static ufbxi_forceinline bool ufbxi_is_vec4_zero(ufbx_vec4 v)
{}

static ufbxi_forceinline bool ufbxi_is_vec3_one(ufbx_vec3 v)
{}

static ufbxi_forceinline bool ufbxi_is_quat_identity(ufbx_quat v)
{}

static ufbxi_noinline bool ufbxi_is_transform_identity(const ufbx_transform *t)
{}

static ufbxi_forceinline uint32_t ufbxi_get_name_key(const char *name, size_t len)
{}

static ufbxi_forceinline uint32_t ufbxi_get_name_key_c(const char *name)
{}

static ufbxi_forceinline bool ufbxi_name_key_less(ufbx_prop *prop, const char *data, size_t name_len, uint32_t key)
{}

static const char *const ufbxi_node_prop_names[] =;

ufbxi_nodiscard static ufbxi_noinline int ufbxi_init_node_prop_names(ufbxi_context *uc)
{}

static bool ufbxi_is_node_property(ufbxi_context *uc, const char *name)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_maps(ufbxi_context *uc)
{}

// -- Reading the parsed data

ufbxi_noinline static void ufbxi_decode_base64(char *dst, const char *src, size_t src_length)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_embedded_blob(ufbxi_context *uc, ufbx_blob *dst_blob, ufbxi_node *node)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_property(ufbxi_context *uc, ufbxi_node *node, ufbx_prop *prop, int version)
{}

static ufbxi_forceinline bool ufbxi_prop_less(ufbx_prop *a, ufbx_prop *b)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_properties(ufbxi_context *uc, ufbx_prop *props, size_t count)
{}

ufbxi_noinline static void ufbxi_deduplicate_properties(ufbx_prop_list *list)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_properties(ufbxi_context *uc, ufbxi_node *parent, ufbx_props *props)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_thumbnail(ufbxi_context *uc, ufbxi_node *node, ufbx_thumbnail *thumbnail)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_scene_info(ufbxi_context *uc, ufbxi_node *node)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_header_extension(ufbxi_context *uc)
{}

static bool ufbxi_match_version_string(const char *fmt, ufbx_string str, uint32_t *p_version)
{}

ufbxi_nodiscard static int ufbxi_match_exporter(ufbxi_context *uc)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_document(ufbxi_context *uc)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_definitions(ufbxi_context *uc)
{}

ufbxi_nodiscard static ufbx_props *ufbxi_find_template(ufbxi_context *uc, const char *name, const char *sub_type)
{}

// Name ID categories
#define UFBXI_SYNTHETIC_ID_BIT

ufbx_static_assert();

static ufbxi_forceinline uint64_t ufbxi_synthetic_id_from_pointer(const void *ptr)
{}

static ufbxi_forceinline uint64_t ufbxi_synthetic_id_from_string(const char *str)
{}

static ufbxi_noinline int ufbxi_push_synthetic_id(ufbxi_context *uc, uint64_t *p_dst)
{}

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)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_insert_fbx_id(ufbxi_context *uc, uint64_t fbx_id, uint32_t element_id)
{}

static ufbxi_noinline ufbxi_fbx_id_entry *ufbxi_find_fbx_id(ufbxi_context *uc, uint64_t fbx_id)
{}

static ufbxi_forceinline bool ufbxi_fbx_id_exists(ufbxi_context *uc, uint64_t fbx_id)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_insert_fbx_attr(ufbxi_context *uc, uint64_t fbx_id, uint64_t attrib_fbx_id)
{}

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)
{}

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)
{}

#define ufbxi_push_element(uc, info, type_name, type_enum)
#define ufbxi_push_synthetic_element(uc, p_fbx_id, node, name, type_name, type_enum)

ufbxi_nodiscard ufbxi_noinline static int ufbxi_connect_oo(ufbxi_context *uc, uint64_t src, uint64_t dst)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_connect_op(ufbxi_context *uc, uint64_t src, uint64_t dst, ufbx_string prop)
{}

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_noinline static void ufbxi_init_synthetic_int_prop(ufbx_prop *dst, const char *name, int64_t value, ufbx_prop_type type)
{}

ufbxi_noinline static void ufbxi_init_synthetic_real_prop(ufbx_prop *dst, const char *name, ufbx_real value, ufbx_prop_type type)
{}

ufbxi_noinline static void ufbxi_init_synthetic_vec3_prop(ufbx_prop *dst, const char *name, const ufbx_vec3 *value, ufbx_prop_type type)
{}

ufbxi_noinline static void ufbxi_set_own_prop_vec3_uniform(ufbx_props *props, const char *name, ufbx_real value)
{}

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)
{}

ufbxi_scale_helper_prop;

static const ufbxi_scale_helper_prop ufbxi_scale_helper_props[] =;

ufbxi_nodiscard ufbxi_noinline static int ufbxi_setup_scale_helper(ufbxi_context *uc, ufbx_node *node, uint64_t node_fbx_id)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_model(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

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)
{}

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)
{}

ufbxi_tangent_layer;

static ufbx_real ufbxi_zero_element[8] =;

// Sentinel pointers used for zero/sequential index buffers
static const uint32_t ufbxi_sentinel_index_zero[1] =;
static const uint32_t ufbxi_sentinel_index_consecutive[1] =;

ufbxi_noinline static int ufbxi_fix_index(ufbxi_context *uc, uint32_t *p_dst, uint32_t index, size_t one_past_max_val)
{}

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)
{}

ufbx_static_assert();
ufbx_static_assert();
ufbx_static_assert();
ufbx_static_assert();

ufbxi_nodiscard ufbxi_noinline static int ufbxi_warn_polygon_mapping(ufbxi_context *uc, const char *data_name, const char *mapping)
{}

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)
{}

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_noinline static bool ufbxi_uv_set_less(void *user, const void *va, const void *vb)
{}

ufbxi_noinline static bool ufbxi_color_set_less(void *user, const void *va, const void *vb)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_uv_sets(ufbxi_context *uc, ufbx_uv_set *sets, size_t count)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_color_sets(ufbxi_context *uc, ufbx_color_set *sets, size_t count)
{}

ufbxi_blend_offset;

static ufbxi_noinline bool ufbxi_blend_offset_less(void *user, const void *va, const void *vb)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_blend_offsets(ufbxi_context *uc, ufbxi_blend_offset *offsets, size_t count)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_shape(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_synthetic_blend_shapes(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_process_indices(ufbxi_context *uc, ufbx_mesh *mesh, uint32_t *index_data)
{}

ufbxi_noinline static void ufbxi_patch_mesh_reals(ufbx_mesh *mesh)
{}

ufbxi_id_group;

static bool ufbxi_less_int32(void *user, const void *va, const void *vb)
{}

ufbx_static_assert();
ufbx_static_assert();
static ufbxi_forceinline void ufbxi_mesh_part_add_face(ufbx_mesh_part *part, uint32_t 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)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_update_face_groups(ufbxi_buf *buf, ufbx_error *error, ufbx_mesh *mesh, bool need_copy)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_mesh(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_noinline static ufbx_nurbs_topology ufbxi_read_nurbs_topology(const char *form)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_nurbs_curve(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_nurbs_surface(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_line(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_noinline static void ufbxi_read_transform_matrix(ufbx_matrix *m, ufbx_real *data)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_bone(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info, const char *sub_type)
{}

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)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_skin(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_skin_cluster(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_blend_channel(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

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)
{}

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)
{}

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)
{}

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)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_animation_curve(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_material(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_texture(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_layered_texture(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_video(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_anim_stack(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_pose(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info, const char *sub_type)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_shader_prop_bindings(ufbxi_context *uc, ufbx_shader_prop_binding *bindings, size_t count)
{}


ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_binding_table(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_selection_set(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_noinline static void ufbxi_find_uint32_list(ufbx_uint32_list *dst, ufbxi_node *node, const char *name)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_selection_node(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_character(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_audio_clip(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_constraint_type;

static const ufbxi_constraint_type ufbxi_constraint_types[] =;

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_constraint(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

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)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_global_settings(ufbxi_context *uc, ufbxi_node *node)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_object(ufbxi_context *uc, ufbxi_node *node)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_objects(ufbxi_context *uc)
{}

ufbxi_object_batch;

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_objects_threaded(ufbxi_context *uc)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_connections(ufbxi_context *uc)
{}

// -- Pre-7000 "Take" based animation

ufbxi_forceinline static char ufbxi_double_to_char(double value)
{}

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)
{}

// 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))
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_take_object(ufbxi_context *uc, ufbxi_node *node, uint64_t layer_fbx_id)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_take(ufbxi_context *uc, ufbxi_node *node)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_takes(ufbxi_context *uc)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_settings(ufbxi_context *uc, ufbxi_node *node)
{}

ufbxi_noinline static ufbx_matrix ufbxi_unscaled_transform_to_matrix(const ufbx_transform *t)
{}

ufbxi_noinline static void ufbxi_setup_root_node(ufbxi_context *uc, ufbx_node *root)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_root(ufbxi_context *uc)
{}

ufbxi_legacy_prop;

// Must be alphabetically sorted!
static const ufbxi_legacy_prop ufbxi_legacy_light_props[] =;

// Must be alphabetically sorted!
static const ufbxi_legacy_prop ufbxi_legacy_camera_props[] =;

// Must be alphabetically sorted!
static const ufbxi_legacy_prop ufbxi_legacy_bone_props[] =;

// Must be alphabetically sorted!
static const ufbxi_legacy_prop ufbxi_legacy_material_props[] =;

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_prop(ufbxi_node *node, ufbx_prop *prop, const ufbxi_legacy_prop *legacy_prop)
{}

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)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_material(ufbxi_context *uc, ufbxi_node *node, uint64_t *p_fbx_id, const char *name)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_link(ufbxi_context *uc, ufbxi_node *node, uint64_t *p_fbx_id, const char *name)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_light(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_camera(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_limb_node(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_mesh(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_media(ufbxi_context *uc, ufbxi_node *node)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_model(ufbxi_context *uc, ufbxi_node *node)
{}

// 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)
{}

// Filename manipulation

ufbxi_nodiscard ufbxi_noinline static size_t ufbxi_trim_delimiters(ufbxi_context *uc, const char *data, size_t length)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_init_file_paths(ufbxi_context *uc)
{}

ufbxi_strblob;

static ufbxi_noinline void ufbxi_strblob_set(ufbxi_strblob *dst, const char *data, size_t length, bool raw)
{}

static ufbxi_forceinline const char *ufbxi_strblob_data(const ufbxi_strblob *strblob, bool raw)
{}

static ufbxi_forceinline size_t ufbxi_strblob_length(const ufbxi_strblob *strblob, bool raw)
{}

ufbxi_nodiscard ufbxi_noinline static bool ufbxi_is_absolute_path(const char *path, size_t length)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_relative_filename(ufbxi_context *uc, ufbxi_strblob *p_dst, const ufbxi_strblob *p_src, bool raw)
{}

// Open file utility

static void *ufbxi_ator_alloc(void *user, size_t size)
{}

static void *ufbxi_ator_realloc(void *user, void *old_ptr, size_t old_size, size_t new_size)
{}

static void ufbxi_ator_free(void *user, void *ptr, size_t size)
{}

static ufbxi_noinline void ufbxi_setup_ator_allocator(ufbx_allocator *allocator, ufbxi_allocator *ator)
{}

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)
{}

#define ufbxi_patch_zero(dst, src)

static void ufbxi_update_vertex_first_index(ufbx_mesh *mesh)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_mesh(ufbxi_buf *buf, ufbx_error *error, ufbx_mesh *mesh)
{}

// -- .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
#define ufbxi_obj_cmd2
#define ufbxi_obj_cmd3

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")) {
				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_nodiscard static ufbxi_forceinline int ufbxi_mtl_load(ufbxi_context *uc)
{}

static ufbxi_forceinline void ufbxi_obj_free(ufbxi_context *uc)
{}
#endif

// -- Scene pre-processing

ufbxi_pre_connection;

ufbxi_pre_node;

ufbxi_pre_mesh;

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)
{}

// -- Scene processing

static ufbxi_noinline ufbx_element *ufbxi_find_element_by_fbx_id(ufbxi_context *uc, uint64_t fbx_id)
{}

ufbxi_forceinline static bool ufbxi_cmp_name_element_less(const ufbx_name_element *a, const ufbx_name_element *b)
{}

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)
{}

ufbxi_forceinline static bool ufbxi_cmp_prop_less_ref(const ufbx_prop *a, ufbx_string name, uint32_t key)
{}

ufbxi_forceinline static bool ufbxi_cmp_prop_less_concat(const ufbx_prop *a, const ufbx_string *parts, size_t num_parts, uint32_t key)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_name_elements(ufbxi_context *uc, ufbx_name_element *name_elems, size_t count)
{}

ufbxi_noinline static bool ufbxi_cmp_node_less(ufbx_node *a, ufbx_node *b)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_node_ptrs(ufbxi_context *uc, ufbx_node **nodes, size_t count)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_cmp_tmp_material_texture_less(const ufbxi_tmp_material_texture *a, const ufbxi_tmp_material_texture *b)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_tmp_material_textures(ufbxi_context *uc, ufbxi_tmp_material_texture *mat_texs, size_t count)
{}

// We need to be able to assume no padding!
ufbx_static_assert();

ufbxi_forceinline static bool ufbxi_cmp_connection_less(ufbx_connection *a, ufbx_connection *b, size_t index)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_connections(ufbxi_context *uc, ufbx_connection *connections, size_t count, size_t index)
{}

static uint64_t ufbxi_find_attribute_fbx_id(ufbxi_context *uc, uint64_t node_fbx_id)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_connections(ufbxi_context *uc)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_add_connections_to_elements(ufbxi_context *uc)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_linearize_nodes(ufbxi_context *uc)
{}


ufbxi_nodiscard ufbxi_noinline static ufbx_connection_list ufbxi_find_dst_connections(ufbx_element *element, const char *prop)
{}

ufbxi_nodiscard ufbxi_noinline static ufbx_connection_list ufbxi_find_src_connections(ufbx_element *element, const char *prop)
{}

ufbxi_nodiscard static ufbx_element *ufbxi_get_element_node(ufbx_element *element)
{}

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)
{}

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)
{}

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)
{}

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)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_textures(ufbxi_context *uc, ufbx_material_texture_list *list, ufbx_element *element, bool search_node)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_mesh_materials(ufbxi_context *uc, ufbx_material_list *list, ufbx_element *element, bool search_node)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_deformers(ufbxi_context *uc,  ufbx_element_list *list, ufbx_element *element, bool search_node)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_blend_keyframes(ufbxi_context *uc, ufbx_blend_keyframe_list *list, ufbx_element *element)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_texture_layers(ufbxi_context *uc, ufbx_texture_layer_list *list, ufbx_element *element)
{}

static ufbxi_forceinline bool ufbxi_prop_connection_less(const ufbx_connection *a, const char *prop)
{}

ufbxi_nodiscard ufbxi_noinline static ufbx_connection *ufbxi_find_prop_connection(const ufbx_element *element, const char *prop)
{}

ufbxi_forceinline static void ufbxi_patch_index_pointer(ufbxi_context *uc, uint32_t **p_index)
{}

ufbxi_nodiscard static bool ufbxi_cmp_anim_prop_less(const ufbx_anim_prop *a, const ufbx_anim_prop *b)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_anim_props(ufbxi_context *uc, ufbx_anim_prop *aprops, size_t count)
{}

ufbxi_noinline static bool ufbxi_material_texture_less(void *user, const void *va, const void *vb)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_material_textures(ufbxi_context *uc, ufbx_material_texture *textures, size_t count)
{}

static ufbxi_noinline bool ufbxi_bone_pose_less(void *user, const void *va, const void *vb)
{}

ufbxi_nodiscard ufbxi_noinline static ufbx_anim_prop *ufbxi_find_anim_prop_start(ufbx_anim_layer *layer, const ufbx_element *element)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_bone_poses(ufbxi_context *uc, ufbx_pose *pose)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_skin_weights(ufbxi_context *uc, ufbx_skin_deformer *skin)
{}

ufbxi_noinline static bool ufbxi_blend_keyframe_less(void *user, const void *va, const void *vb)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_blend_keyframes(ufbxi_context *uc, ufbx_blend_keyframe *keyframes, size_t count)
{}

// Material tables

ufbxi_mat_transform_fn;

static void ufbxi_mat_transform_invert_x(ufbx_vec4 *v) {}
static void ufbxi_mat_transform_unknown_shininess(ufbx_vec4 *v) {}
static void ufbxi_mat_transform_blender_opacity(ufbx_vec4 *v) {}
static void ufbxi_mat_transform_blender_shininess(ufbx_vec4 *v) {}

ufbxi_mat_transform;

ufbxi_shader_mapping_flag;

ufbxi_shader_feature_flag;

static const ufbxi_mat_transform_fn ufbxi_mat_transform_fns[] =;

ufbx_static_assert();

ufbxi_shader_mapping;

ufbxi_shader_mapping_list;

#define ufbxi_mat_string(str)

static const ufbxi_shader_mapping ufbxi_base_fbx_mapping[] =;

static const ufbxi_shader_mapping ufbxi_obj_fbx_mapping[] =;

static const ufbxi_shader_mapping ufbxi_fbx_lambert_shader_pbr_mapping[] =;

static const ufbxi_shader_mapping ufbxi_fbx_phong_shader_pbr_mapping[] =;

static const ufbxi_shader_mapping ufbxi_osl_standard_shader_pbr_mapping[] =;

static const ufbxi_shader_mapping ufbxi_osl_standard_shader_features[] =;

static const ufbxi_shader_mapping ufbxi_arnold_shader_pbr_mapping[] =;

static const ufbxi_shader_mapping ufbxi_arnold_shader_features[] =;

static const ufbxi_shader_mapping ufbxi_3ds_max_physical_material_pbr_mapping[] =;

static const ufbxi_shader_mapping ufbxi_3ds_max_physical_material_features[] =;

static const ufbxi_shader_mapping ufbxi_gltf_material_pbr_mapping[] =;

static const ufbxi_shader_mapping ufbxi_3ds_max_pbr_metal_rough_pbr_mapping[] =;

static const ufbxi_shader_mapping ufbxi_3ds_max_pbr_spec_gloss_pbr_mapping[] =;

static const ufbxi_shader_mapping ufbxi_3ds_max_pbr_features[] =;

static const ufbxi_shader_mapping ufbxi_gltf_material_features[] =;

// 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[] =;

static const ufbxi_shader_mapping ufbxi_blender_phong_shader_pbr_mapping[] =;

static const ufbxi_shader_mapping ufbxi_obj_pbr_mapping[] =;

static const ufbxi_shader_mapping ufbxi_obj_features[] =;

enum {};

static const ufbxi_shader_mapping_list ufbxi_shader_pbr_mappings[] =;

ufbx_static_assert();

enum {};

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)
{}

ufbxi_noinline static void ufbxi_update_factor(ufbx_material_map *factor_map, ufbx_material_map *color_map)
{}

// 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.
ufbxi_glossiness_remap;

static const ufbxi_glossiness_remap ufbxi_glossiness_remaps[] =;

ufbxi_noinline static void ufbxi_fetch_maps(ufbx_scene *scene, ufbx_material *material)
{}

ufbxi_constraint_prop_type;

ufbxi_constraint_prop;

static const ufbxi_constraint_prop ufbxi_constraint_props[] =;

ufbxi_nodiscard ufbxi_noinline static int ufbxi_add_constraint_prop(ufbxi_context *uc, ufbx_constraint *constraint, ufbx_node *node, const char *prop)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_nurbs_basis(ufbxi_context *uc, ufbx_nurbs_basis *basis)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_lod_group(ufbxi_context *uc, ufbx_lod_group *lod)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_generate_normals(ufbxi_context *uc, ufbx_mesh *mesh)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_push_prop_prefix(ufbxi_context *uc, ufbx_string *dst, ufbx_string prefix)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_shader_texture_find_prefix(ufbxi_context *uc, ufbx_texture *texture, ufbx_shader_texture *shader)
{}

ufbxi_file_shader;

// Known shaders that represent sampled images.
static const ufbxi_file_shader ufbxi_file_shaders[] =;

ufbxi_noinline static void ufbxi_update_shader_texture(ufbx_texture *texture, ufbx_shader_texture *shader)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_shader_texture(ufbxi_context *uc, ufbx_texture *texture)
{}

ufbxi_noinline static void ufbxi_propagate_main_textures(ufbx_scene *scene)
{}

#define ufbxi_patch_empty(m_dst, m_len, m_src)

ufbxi_nodiscard ufbxi_noinline static int ufbxi_insert_texture_file(ufbxi_context *uc, ufbx_texture *texture)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_pop_texture_files(ufbxi_context *uc)
{}

ufbxi_ordered_texture;

ufbxi_noinline static bool ufbxi_ordered_texture_less_texture(void *user, const void *va, const void *vb)
{}

ufbxi_noinline static bool ufbxi_ordered_texture_less_order(void *user, const void *va, const void *vb)
{}

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_file_texture_fetch_state;

// Populate `ufbx_texture.file_textures[]` arrays.
ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_file_textures(ufbxi_context *uc)
{}

ufbxi_nodiscard ufbxi_noinline static ufbx_node *ufbxi_get_geometry_transform_node(ufbx_element *element)
{}

ufbxi_noinline static void ufbxi_mirror_vec3_list(const void *v_list, ufbx_mirror_axis axis, size_t stride)
{}

ufbxi_noinline static void ufbxi_scale_vec3_list(const void *v_list, ufbx_real scale, size_t stride)
{}

ufbxi_noinline static void ufbxi_transform_vec3_list(const void *v_list, const ufbx_matrix *matrix, size_t stride)
{}

ufbxi_noinline static void ufbxi_normalize_vec3_list(const ufbx_vec3_list *list)
{}

// 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)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_flip_winding(ufbxi_context *uc, ufbx_mesh *mesh)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_modify_geometry(ufbxi_context *uc)
{}

ufbxi_noinline static void ufbxi_postprocess_scene(ufbxi_context *uc)
{}

ufbxi_noinline static size_t ufbxi_next_path_segment(const char *data, size_t begin, size_t 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)
{}

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)
{}

ufbxi_noinline static bool ufbxi_file_content_less(void *user, const void *va, const void *vb)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_file_contents(ufbxi_context *uc, ufbxi_file_content *content, size_t count)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_push_file_content(ufbxi_context *uc, ufbx_string *p_filename, ufbx_blob *p_data)
{}

ufbxi_noinline static void ufbxi_fetch_file_content(ufbxi_context *uc, ufbx_string *p_filename, ufbx_blob *p_data)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_file_content(ufbxi_context *uc)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_validate_indices(ufbxi_context *uc, ufbx_uint32_list *indices, size_t max_index)
{}

static bool ufbxi_material_part_usage_less(void *user, const void *va, const void *vb)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_mesh_material(ufbxi_buf *buf, ufbx_error *error, ufbx_mesh *mesh)
{}

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)
{}

ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_scene(ufbxi_context *uc)
{}

// -- Interpret the read scene

static ufbxi_forceinline void ufbxi_add_translate(ufbx_transform *t, ufbx_vec3 v)
{}

static ufbxi_forceinline void ufbxi_sub_translate(ufbx_transform *t, ufbx_vec3 v)
{}

static ufbxi_forceinline void ufbxi_mul_scale(ufbx_transform *t, ufbx_vec3 v)
{}

static ufbxi_forceinline void ufbxi_mul_scale_real(ufbx_transform *t, ufbx_real v)
{}

static ufbxi_noinline ufbx_quat ufbxi_mul_quat(ufbx_quat a, ufbx_quat b)
{}

static ufbxi_forceinline void ufbxi_add_weighted_vec3(ufbx_vec3 *r, ufbx_vec3 b, ufbx_real w)
{}

static ufbxi_forceinline void ufbxi_add_weighted_quat(ufbx_quat *r, ufbx_quat b, ufbx_real w)
{}

static ufbxi_noinline void ufbxi_add_weighted_mat(ufbx_matrix *r, const ufbx_matrix *b, ufbx_real w)
{}

static void ufbxi_mul_rotate(ufbx_transform *t, ufbx_vec3 v, ufbx_rotation_order order)
{}

static void ufbxi_mul_rotate_quat(ufbx_transform *t, ufbx_quat q)
{}

static void ufbxi_mul_inv_rotate(ufbx_transform *t, ufbx_vec3 v, ufbx_rotation_order order)
{}

// -- Updating state from properties

ufbxi_forceinline static void ufbxi_mirror_translation(ufbx_vec3 *p_vec, ufbx_mirror_axis axis)
{}

ufbxi_forceinline static void ufbxi_mirror_rotation(ufbx_quat *p_quat, ufbx_mirror_axis axis)
{}

ufbxi_noinline static ufbx_transform ufbxi_get_geometry_transform(const ufbx_props *props, ufbx_node *node)
{}

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)
{}

ufbxi_noinline static ufbx_quat ufbxi_get_rotation(const ufbx_props *props, ufbx_rotation_order order, const ufbx_node *node)
{}

ufbxi_noinline static ufbx_vec3 ufbxi_get_scale(const ufbx_props *props, const ufbx_node *node)
{}

ufbxi_noinline static ufbx_transform ufbxi_get_texture_transform(const ufbx_props *props)
{}

ufbxi_noinline static ufbx_transform ufbxi_get_constraint_transform(const ufbx_props *props)
{}

ufbxi_noinline static void ufbxi_update_node(ufbx_node *node, const ufbx_transform_override *overrides, size_t num_overrides)
{}

ufbxi_noinline static void ufbxi_update_light(ufbx_light *light)
{}

ufbxi_aperture_format;

static const ufbxi_aperture_format ufbxi_aperture_formats[] =;

ufbxi_noinline static void ufbxi_update_camera(ufbx_scene *scene, ufbx_camera *camera)
{}

ufbxi_noinline static void ufbxi_update_bone(ufbx_scene *scene, ufbx_bone *bone)
{}

ufbxi_noinline static void ufbxi_update_line_curve(ufbx_line_curve *line)
{}

ufbxi_noinline static void ufbxi_update_pose(ufbx_pose *pose)
{}

ufbxi_noinline static void ufbxi_update_skin_cluster(ufbx_skin_cluster *cluster)
{}

ufbxi_noinline static void ufbxi_update_blend_channel(ufbx_blend_channel *channel)
{}

ufbxi_noinline static void ufbxi_update_material(ufbx_scene *scene, ufbx_material *material)
{}

ufbxi_noinline static void ufbxi_update_texture(ufbx_texture *texture)
{}

ufbxi_noinline static void ufbxi_update_anim_stack(ufbx_scene *scene, ufbx_anim_stack *stack)
{}

ufbxi_noinline static void ufbxi_update_display_layer(ufbx_display_layer *layer)
{}

ufbxi_noinline static void ufbxi_find_bool3(bool *dst, ufbx_props *props, const char *name, bool default_value)
{}

ufbxi_noinline static void ufbxi_update_constraint(ufbx_constraint *constraint)
{}

ufbxi_noinline static void ufbxi_update_anim(ufbx_scene *scene)
{}

static ufbxi_forceinline void ufbxi_mirror_matrix_dst(ufbx_matrix *m, ufbx_mirror_axis axis)
{}

static ufbxi_forceinline void ufbxi_mirror_matrix_src(ufbx_matrix *m, ufbx_mirror_axis axis)
{}

static ufbxi_noinline void ufbxi_mirror_matrix(ufbx_matrix *m, ufbx_mirror_axis axis)
{}

ufbxi_noinline static void ufbxi_update_initial_clusters(ufbx_scene *scene)
{}

ufbxi_noinline static ufbx_coordinate_axis ufbxi_find_axis(const ufbx_props *props, const char *axis_name, const char *sign_name)
{}

static const ufbx_real ufbxi_time_mode_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)
{}

ufbxi_noinline static void ufbxi_update_adjust_transforms(ufbxi_context *uc, ufbx_scene *scene)
{}

ufbxi_noinline static void ufbxi_update_scene(ufbx_scene *scene, bool initial, const ufbx_transform_override *transform_overrides, size_t num_transform_overrides)
{}

static ufbxi_noinline void ufbxi_update_scene_metadata(ufbx_metadata *metadata)
{}

static const ufbx_real ufbxi_pow10_targets[] =;

static ufbxi_noinline ufbx_real ufbxi_round_if_near(const ufbx_real *targets, size_t num_targets, ufbx_real value)
{}

static ufbxi_noinline void ufbxi_update_scene_settings(ufbx_scene_settings *settings)
{}

static ufbxi_noinline void ufbxi_update_scene_settings_obj(ufbxi_context *uc)
{}

// -- 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

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) {
				int value = atoi(fps->value.data);
				if (value > 0) {
					cc->xml_ticks_per_frame = (uint32_t)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 = (uint32_t)atoi(sampling_rate->value.data);
					channel->start_time = (uint32_t)atoi(start_time->value.data);
					channel->end_time = (uint32_t)atoi(end_time->value.data);
					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");
		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

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)
{}

static ufbxi_forceinline void ufbxi_free_geometry_cache_imp(ufbxi_geometry_cache_imp *imp)
{}

#endif

// -- External files

ufbxi_external_file_type;

ufbxi_external_file;

static bool ufbxi_less_external_file(void *user, const void *va, const void *vb)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_external_cache(ufbxi_context *uc, ufbxi_external_file *file)
{}

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)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_external_files(ufbxi_context *uc)
{}

static ufbxi_noinline void ufbxi_transform_to_axes(ufbxi_context *uc, ufbx_coordinate_axes dst_axes)
{}

static ufbxi_noinline int ufbxi_scale_units(ufbxi_context *uc, ufbx_real target_meters)
{}

// -- Curve evaluation

static ufbxi_forceinline double ufbxi_find_cubic_bezier_t(double p1, double p2, double x0)
{}

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)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_fixup_opts_string(ufbxi_context *uc, ufbx_string *str, bool push)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_resolve_warning_elements(ufbxi_context *uc)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_imp(ufbxi_context *uc)
{}

static ufbxi_noinline void ufbxi_free_temp(ufbxi_context *uc)
{}

static ufbxi_noinline void ufbxi_free_result(ufbxi_context *uc)
{}

static ufbxi_noinline ufbx_scene *ufbxi_load(ufbxi_context *uc, const ufbx_load_opts *user_opts, ufbx_error *p_error)
{}

static ufbxi_noinline ufbx_scene *ufbxi_load_not_found(const char *filename, size_t filename_len, ufbx_error *p_error)
{}

// -- Animation evaluation

static ufbxi_forceinline bool ufbxi_override_less_than_prop(const ufbx_prop_override *over, uint32_t element_id, const ufbx_prop *prop)
{}

static ufbxi_forceinline bool ufbxi_override_equals_to_prop(const ufbx_prop_override *over, uint32_t element_id, const ufbx_prop *prop)
{}

static ufbxi_noinline bool ufbxi_find_prop_override(const ufbx_prop_override_list *overrides, uint32_t element_id, ufbx_prop *prop)
{}

static ufbxi_noinline ufbx_prop_override_list ufbxi_find_element_prop_overrides(const ufbx_prop_override_list *overrides, uint32_t element_id)
{}

ufbxi_anim_layer_combine_ctx;

static ufbxi_noinline double ufbxi_pow_abs(double v, double 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))
{}

static ufbxi_forceinline bool ufbxi_anim_layer_might_contain_id(const ufbx_anim_layer *layer, uint32_t id)
{}

static ufbxi_noinline void ufbxi_evaluate_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *props, size_t num_props)
{}

// 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))
{}

ufbxi_prop_iter;

static ufbxi_noinline void ufbxi_init_prop_iter_slow(ufbxi_prop_iter *iter, const ufbx_anim *anim, const ufbx_element *element)
{}

static ufbxi_forceinline void ufbxi_init_prop_iter(ufbxi_prop_iter *iter, const ufbx_anim *anim, const ufbx_element *element)
{}

static ufbxi_noinline const ufbx_prop *ufbxi_next_prop_slow(ufbxi_prop_iter *iter)
{}

static ufbxi_forceinline const ufbx_prop *ufbxi_next_prop(ufbxi_prop_iter *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)
{}

#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");
		if (p_error) *p_error = ec->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

ufbxi_create_anim_context;

ufbxi_nodiscard static ufbxi_noinline int ufbxi_check_string(ufbx_error *error, ufbx_string *dst, const ufbx_string *src)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_anim_string(ufbxi_create_anim_context *ac, ufbx_string *str)
{}

static bool ufbxi_prop_override_prop_name_less(void *user, const void *va, const void *vb)
{}

static bool ufbxi_prop_override_less(void *user, const void *va, const void *vb)
{}

static int ufbxi_cmp_transform_override(const void *va, const void *vb)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_create_anim_imp(ufbxi_create_anim_context *ac)
{}

// -- Animation baking

ufbxi_baked_anim_imp;

#if UFBXI_FEATURE_ANIMATION_BAKING

ufbxi_bake_time;

UFBX_LIST_TYPE();

ufbxi_bake_context;

ufbxi_bake_prop;

static int ufbxi_cmp_bake_prop(const void *va, const void *vb)
{}

ufbx_static_assert();
ufbx_static_assert();
ufbx_static_assert();
static ufbxi_forceinline int ufbxi_cmp_bake_time(ufbxi_bake_time a, ufbxi_bake_time b)
{}

static int ufbxi_cmp_bake_time_fn(const void *va, const void *vb)
{}

ufbxi_nodiscard static ufbxi_forceinline int ufbxi_bake_push_time(ufbxi_bake_context *bc, double time, uint32_t flags)
{}

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)
{}

static const char *const ufbxi_transform_props[] =;

static const char *const ufbxi_complex_translation_props[] =;

static const char *const ufbxi_complex_rotation_props[] =;

static const char *const ufbxi_complex_rotation_sources[] =;

ufbxi_nodiscard static ufbxi_noinline bool ufbxi_in_list(const char *const *items, size_t count, const char *item)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_bake_times(ufbxi_bake_context *bc, ufbxi_bake_time_list *p_dst)
{}

#define ufbxi_add_epsilon(a, epsilon)
#define ufbxi_sub_epsilon(a, epsilon)

static ufbxi_noinline bool ufbxi_postprocess_step(ufbxi_bake_context *bc, double prev_time, double next_time, double *p_time, uint32_t flags)
{}

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)
{}

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)
{}

static ufbxi_forceinline double ufbxi_bake_time_sample_time(ufbxi_bake_time time)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_resampled_times(ufbxi_bake_context *bc, const ufbx_baked_vec3_list *p_keys)
{}

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)
{}

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_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_nodiscard static ufbxi_noinline int ufbxi_bake_element(ufbxi_bake_context *bc, uint32_t element_id, ufbxi_bake_prop *props, size_t count)
{}

static ufbxi_noinline bool ufbxi_baked_node_less(void *user, const void *va, const void *vb)
{}

static ufbxi_noinline bool ufbxi_baked_element_less(void *user, const void *va, const void *vb)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim(ufbxi_bake_context *bc)
{}

ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim_imp(ufbxi_bake_context *bc, const ufbx_anim *anim)
{}

#endif

// -- NURBS

static ufbxi_forceinline ufbx_real ufbxi_nurbs_weight(const ufbx_real_list *knots, size_t knot, size_t degree, ufbx_real u)
{}

static ufbxi_forceinline ufbx_real ufbxi_nurbs_deriv(const ufbx_real_list *knots, size_t knot, size_t degree)
{}

ufbxi_line_curve_imp;

ufbx_static_assert();

#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

ufbxi_kd_node;

ufbxi_ngon_context;

ufbxi_kd_triangle;

ufbxi_noinline static ufbx_vec2 ufbxi_ngon_project(ufbxi_ngon_context *nc, uint32_t index)
{}

ufbxi_forceinline static ufbx_real ufbxi_orient2d(ufbx_vec2 a, ufbx_vec2 b, ufbx_vec2 c)
{}

ufbxi_noinline static bool ufbxi_kd_check_point(ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t index)
{}

// 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))
{}

// 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))
{}

ufbxi_noinline static bool ufbxi_kd_check(ufbxi_ngon_context *nc, const ufbx_vec2 *points, const uint32_t *indices)
{}

ufbxi_noinline static bool ufbxi_kd_index_less(void *user, const void *va, const void *vb)
{}

// 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))
{}

#endif

#if UFBXI_FEATURE_TRIANGULATION

ufbxi_noinline static ufbx_real ufbxi_ngon_tri_weight(const ufbx_vec2 *points)
{}

ufbxi_noinline static uint32_t ufbxi_triangulate_ngon(ufbxi_ngon_context *nc, uint32_t *indices, uint32_t num_indices)
{}

#endif

static int ufbxi_cmp_topo_index_prev_next(const void *va, const void *vb)
{}

static int ufbxi_cmp_topo_index_index(const void *va, const void *vb)
{}

ufbxi_noinline static void ufbxi_compute_topology(const ufbx_mesh *mesh, ufbx_topo_edge *topo)
{}

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)
{}

// -- 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 int ufbxi_cmp_subdivision_weight(const void *va, const void *vb)
{
	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 ? -1 : +1;
	return a.index < b.index ? -1 : +1;
}

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;
	}

	qsort(tmp_weights, num_weights, sizeof(ufbx_subdivision_weight), ufbxi_cmp_subdivision_weight);

	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");
		if (p_error) *p_error = sc.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)
{}

#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");
	}

	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)
{}

#endif

static ufbxi_noinline void ufbxi_free_scene_imp(ufbxi_scene_imp *imp)
{}

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)
{}

static ufbxi_noinline void ufbxi_release_ref(ufbxi_refcount *refcount)
{}

static ufbxi_noinline void *ufbxi_uninitialized_options(ufbx_error *p_error)
{}

#define ufbxi_check_opts_ptr(m_type, m_opts, m_error)

#define ufbxi_check_opts_return(m_value, m_opts, m_error)

#define ufbxi_check_opts_return_no_error(m_value, m_opts)

// -- API

#ifdef __cplusplus
extern "C" {
#endif

ufbx_abi_data_def const ufbx_string ufbx_empty_string =;
ufbx_abi_data_def const ufbx_blob ufbx_empty_blob =;
ufbx_abi_data_def const ufbx_matrix ufbx_identity_matrix =;
ufbx_abi_data_def const ufbx_transform ufbx_identity_transform =;
ufbx_abi_data_def const ufbx_vec2 ufbx_zero_vec2 =;
ufbx_abi_data_def const ufbx_vec3 ufbx_zero_vec3 =;
ufbx_abi_data_def const ufbx_vec4 ufbx_zero_vec4 =;
ufbx_abi_data_def const ufbx_quat ufbx_identity_quat =;

ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_right_handed_y_up =;
ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_right_handed_z_up =;
ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_left_handed_y_up =;
ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_left_handed_z_up =;

ufbx_abi_data_def const size_t ufbx_element_type_size[UFBX_ELEMENT_TYPE_COUNT] =;

ufbx_abi bool ufbx_open_file(ufbx_stream *stream, const char *path, size_t path_len)
{}

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)
{}

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)
{}

ufbx_abi bool ufbx_is_thread_safe(void)
{}

ufbx_abi ufbx_scene *ufbx_load_memory(const void *data, size_t size, const ufbx_load_opts *opts, ufbx_error *error)
{}

ufbx_abi ufbx_scene *ufbx_load_file(const char *filename, const ufbx_load_opts *opts, ufbx_error *error)
{}

ufbx_abi ufbx_scene *ufbx_load_file_len(const char *filename, size_t filename_len, const ufbx_load_opts *opts, ufbx_error *error)
{}

ufbx_abi ufbx_scene *ufbx_load_stdio(void *file_void, const ufbx_load_opts *opts, ufbx_error *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)
{}

ufbx_abi ufbx_scene *ufbx_load_stream(const ufbx_stream *stream, const ufbx_load_opts *opts, ufbx_error *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)
{}

ufbx_abi void ufbx_free_scene(ufbx_scene *scene)
{}

ufbx_abi void ufbx_retain_scene(ufbx_scene *scene)
{}

ufbx_abi ufbxi_noinline size_t ufbx_format_error(char *dst, size_t dst_size, const ufbx_error *error)
{}

ufbx_abi ufbx_prop *ufbx_find_prop_len(const ufbx_props *props, const char *name, size_t name_len)
{}

ufbx_abi ufbx_real ufbx_find_real_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_real 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_abi ufbxi_noinline int64_t ufbx_find_int_len(const ufbx_props *props, const char *name, size_t name_len, int64_t def)
{}

ufbx_abi bool ufbx_find_bool_len(const ufbx_props *props, const char *name, size_t name_len, bool 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_abi ufbx_blob ufbx_find_blob_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_blob def)
{}

ufbx_abi ufbx_prop *ufbx_find_prop_concat(const ufbx_props *props, const ufbx_string *parts, size_t num_parts)
{}

ufbx_abi ufbx_element *ufbx_find_element_len(const ufbx_scene *scene, ufbx_element_type type, const char *name, size_t name_len)
{}

ufbx_abi ufbx_element *ufbx_get_prop_element(const ufbx_element *element, const ufbx_prop *prop, ufbx_element_type 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)
{}

ufbx_abi ufbx_node *ufbx_find_node_len(const ufbx_scene *scene, const char *name, size_t name_len)
{}

ufbx_abi ufbx_anim_stack *ufbx_find_anim_stack_len(const ufbx_scene *scene, const char *name, size_t name_len)
{}

ufbx_abi ufbx_material *ufbx_find_material_len(const ufbx_scene *scene, const char *name, size_t 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_abi ufbxi_noinline ufbx_anim_prop_list ufbx_find_anim_props(const ufbx_anim_layer *layer, const ufbx_element *element)
{}

ufbx_abi ufbxi_noinline ufbx_matrix ufbx_get_compatible_matrix_for_normals(const ufbx_node *node)
{}

ufbx_abi ufbx_real ufbx_evaluate_curve(const ufbx_anim_curve *curve, double time, ufbx_real default_value)
{}

ufbx_abi ufbxi_noinline ufbx_real ufbx_evaluate_anim_value_real(const ufbx_anim_value *anim_value, double time)
{}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_evaluate_anim_value_vec3(const ufbx_anim_value *anim_value, double time)
{}

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_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_abi ufbxi_noinline ufbx_transform ufbx_evaluate_transform(const ufbx_anim *anim, const ufbx_node *node, double time)
{}

static const char *const ufbxi_transform_props_all[] =;

static const char *const ufbxi_transform_props_rotation[] =;

static const char *const ufbxi_transform_props_scale[] =;

static const char *const ufbxi_transform_props_rotation_scale[] =;

ufbx_abi ufbxi_noinline ufbx_transform ufbx_evaluate_transform_flags(const ufbx_anim *anim, const ufbx_node *node, double time, uint32_t flags)
{}

ufbx_abi ufbx_real ufbx_evaluate_blend_weight(const ufbx_anim *anim, const ufbx_blend_channel *channel, double time)
{}

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)
{}

ufbx_abi ufbx_anim *ufbx_create_anim(const ufbx_scene *scene, const ufbx_anim_opts *opts, ufbx_error *error)
{}

ufbx_abi void ufbx_free_anim(ufbx_anim *anim)
{}

ufbx_abi void ufbx_retain_anim(ufbx_anim *anim)
{}

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_abi void ufbx_retain_baked_anim(ufbx_baked_anim *bake)
{}

ufbx_abi void ufbx_free_baked_anim(ufbx_baked_anim *bake)
{}


ufbx_abi ufbx_baked_node *ufbx_find_baked_node_by_typed_id(ufbx_baked_anim *bake, uint32_t typed_id)
{}

ufbx_abi ufbx_baked_node *ufbx_find_baked_node(ufbx_baked_anim *bake, ufbx_node *node)
{}

ufbx_abi ufbx_baked_element *ufbx_find_baked_element_by_element_id(ufbx_baked_anim *bake, uint32_t element_id)
{}

ufbx_abi ufbx_baked_element *ufbx_find_baked_element(ufbx_baked_anim *bake, ufbx_element *element)
{}

ufbx_abi ufbx_vec3 ufbx_evaluate_baked_vec3(ufbx_baked_vec3_list keyframes, double time)
{}

ufbx_abi ufbx_quat ufbx_evaluate_baked_quat(ufbx_baked_quat_list keyframes, double time)
{}

ufbx_abi ufbx_bone_pose *ufbx_get_bone_pose(const ufbx_pose *pose, const ufbx_node *node)
{}

ufbx_abi ufbx_texture *ufbx_find_prop_texture_len(const ufbx_material *material, const char *name, size_t name_len)
{}

ufbx_abi ufbx_string ufbx_find_shader_prop_len(const ufbx_shader *shader, const char *name, size_t name_len)
{}

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_abi ufbx_shader_texture_input *ufbx_find_shader_texture_input_len(const ufbx_shader_texture *shader, const char *name, size_t name_len)
{}

ufbx_abi bool ufbx_coordinate_axes_valid(ufbx_coordinate_axes axes)
{}

ufbx_abi ufbx_quat ufbx_quat_mul(ufbx_quat a, ufbx_quat b)
{}

ufbx_abi ufbx_vec3 ufbx_vec3_normalize(ufbx_vec3 v)
{}

ufbx_abi ufbxi_noinline ufbx_real ufbx_quat_dot(ufbx_quat a, ufbx_quat b)
{}

ufbx_abi ufbxi_noinline ufbx_quat ufbx_quat_normalize(ufbx_quat q)
{}

ufbx_abi ufbxi_noinline ufbx_quat ufbx_quat_fix_antipodal(ufbx_quat q, ufbx_quat reference)
{}

ufbx_abi ufbxi_noinline ufbx_quat ufbx_quat_slerp(ufbx_quat a, ufbx_quat b, ufbx_real t)
{}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_quat_rotate_vec3(ufbx_quat q, ufbx_vec3 v)
{}

ufbx_abi ufbxi_noinline ufbx_quat ufbx_euler_to_quat(ufbx_vec3 v, ufbx_rotation_order order)
{}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_quat_to_euler(ufbx_quat q, ufbx_rotation_order order)
{}

ufbx_abi ufbxi_noinline ufbx_matrix ufbx_matrix_mul(const ufbx_matrix *a, const ufbx_matrix *b)
{}

ufbx_abi ufbx_real ufbx_matrix_determinant(const ufbx_matrix *m)
{}

ufbx_abi ufbx_matrix ufbx_matrix_invert(const ufbx_matrix *m)
{}

ufbx_abi ufbxi_noinline ufbx_matrix ufbx_matrix_for_normals(const ufbx_matrix *m)
{}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_transform_position(const ufbx_matrix *m, ufbx_vec3 v)
{}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_transform_direction(const ufbx_matrix *m, ufbx_vec3 v)
{}

ufbx_abi ufbxi_noinline ufbx_matrix ufbx_transform_to_matrix(const ufbx_transform *t)
{}

ufbx_abi ufbxi_noinline ufbx_transform ufbx_matrix_to_transform(const ufbx_matrix *m)
{}

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_abi ufbxi_noinline uint32_t ufbx_get_blend_shape_offset_index(const ufbx_blend_shape *shape, size_t vertex)
{}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_get_blend_shape_vertex_offset(const ufbx_blend_shape *shape, size_t vertex)
{}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_get_blend_vertex_offset(const ufbx_blend_deformer *blend, size_t vertex)
{}

ufbx_abi void ufbx_add_blend_shape_vertex_offsets(const ufbx_blend_shape *shape, ufbx_vec3 *vertices, size_t num_vertices, ufbx_real 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_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_abi ufbxi_noinline ufbx_curve_point ufbx_evaluate_nurbs_curve(const ufbx_nurbs_curve *curve, ufbx_real u)
{}

ufbx_abi ufbxi_noinline ufbx_surface_point ufbx_evaluate_nurbs_surface(const ufbx_nurbs_surface *surface, ufbx_real u, ufbx_real v)
{}

ufbx_abi ufbx_line_curve *ufbx_tessellate_nurbs_curve(const ufbx_nurbs_curve *curve, const ufbx_tessellate_curve_opts *opts, ufbx_error *error)
{}

ufbx_abi ufbx_mesh *ufbx_tessellate_nurbs_surface(const ufbx_nurbs_surface *surface, const ufbx_tessellate_surface_opts *opts, ufbx_error *error)
{}

ufbx_abi void ufbx_free_line_curve(ufbx_line_curve *line_curve)
{}

ufbx_abi void ufbx_retain_line_curve(ufbx_line_curve *line_curve)
{}

ufbx_abi uint32_t ufbx_find_face_index(ufbx_mesh *mesh, size_t index)
{}

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)
{}

ufbx_abi void ufbx_catch_compute_topology(ufbx_panic *panic, const ufbx_mesh *mesh, ufbx_topo_edge *indices, size_t num_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)
{}

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)
{}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_catch_get_weighted_face_normal(ufbx_panic *panic, const ufbx_vertex_vec3 *positions, ufbx_face face)
{}

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)
{}

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)
{}

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)
{}

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_abi ufbx_mesh *ufbx_subdivide_mesh(const ufbx_mesh *mesh, size_t level, const ufbx_subdivide_opts *opts, ufbx_error *error)
{}

ufbx_abi void ufbx_free_mesh(ufbx_mesh *mesh)
{}

ufbx_abi void ufbx_retain_mesh(ufbx_mesh *mesh)
{}

ufbx_abi ufbx_geometry_cache *ufbx_load_geometry_cache(
	const char *filename,
	const ufbx_geometry_cache_opts *opts, ufbx_error *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)
{}

ufbx_abi void ufbx_free_geometry_cache(ufbx_geometry_cache *cache)
{}

ufbx_abi void ufbx_retain_geometry_cache(ufbx_geometry_cache *cache)
{}

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)
{}

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)
{}

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)
{}

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)
{}

ufbx_abi ufbx_dom_node *ufbx_dom_find_len(const ufbx_dom_node *parent, const char *name, size_t name_len)
{}

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_abi void ufbx_thread_pool_run_task(ufbx_thread_pool_context ctx, uint32_t index)
{}

ufbx_abi void ufbx_thread_pool_set_user_ptr(ufbx_thread_pool_context ctx, void *user)
{}

ufbx_abi void *ufbx_thread_pool_get_user_ptr(ufbx_thread_pool_context ctx)
{}

ufbx_abi ufbxi_noinline ufbx_real ufbx_catch_get_vertex_real(ufbx_panic *panic, const ufbx_vertex_real *v, size_t index)
{}

ufbx_abi ufbxi_noinline ufbx_vec2 ufbx_catch_get_vertex_vec2(ufbx_panic *panic, const ufbx_vertex_vec2 *v, size_t index)
{}

ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_catch_get_vertex_vec3(ufbx_panic *panic, const ufbx_vertex_vec3 *v, size_t index)
{}

ufbx_abi ufbxi_noinline ufbx_vec4 ufbx_catch_get_vertex_vec4(ufbx_panic *panic, const ufbx_vertex_vec4 *v, size_t index)
{}

ufbx_abi ufbx_real ufbx_catch_get_vertex_w_vec3(ufbx_panic *panic, const ufbx_vertex_vec3 *v, size_t index)
{}

ufbx_abi ufbx_unknown *ufbx_as_unknown(const ufbx_element *element) {}
ufbx_abi ufbx_node *ufbx_as_node(const ufbx_element *element) {}
ufbx_abi ufbx_mesh *ufbx_as_mesh(const ufbx_element *element) {}
ufbx_abi ufbx_light *ufbx_as_light(const ufbx_element *element) {}
ufbx_abi ufbx_camera *ufbx_as_camera(const ufbx_element *element) {}
ufbx_abi ufbx_bone *ufbx_as_bone(const ufbx_element *element) {}
ufbx_abi ufbx_empty *ufbx_as_empty(const ufbx_element *element) {}
ufbx_abi ufbx_line_curve *ufbx_as_line_curve(const ufbx_element *element) {}
ufbx_abi ufbx_nurbs_curve *ufbx_as_nurbs_curve(const ufbx_element *element) {}
ufbx_abi ufbx_nurbs_surface *ufbx_as_nurbs_surface(const ufbx_element *element) {}
ufbx_abi ufbx_nurbs_trim_surface *ufbx_as_nurbs_trim_surface(const ufbx_element *element) {}
ufbx_abi ufbx_nurbs_trim_boundary *ufbx_as_nurbs_trim_boundary(const ufbx_element *element) {}
ufbx_abi ufbx_procedural_geometry *ufbx_as_procedural_geometry(const ufbx_element *element) {}
ufbx_abi ufbx_stereo_camera *ufbx_as_stereo_camera(const ufbx_element *element) {}
ufbx_abi ufbx_camera_switcher *ufbx_as_camera_switcher(const ufbx_element *element) {}
ufbx_abi ufbx_marker *ufbx_as_marker(const ufbx_element *element) {}
ufbx_abi ufbx_lod_group *ufbx_as_lod_group(const ufbx_element *element) {}
ufbx_abi ufbx_skin_deformer *ufbx_as_skin_deformer(const ufbx_element *element) {}
ufbx_abi ufbx_skin_cluster *ufbx_as_skin_cluster(const ufbx_element *element) {}
ufbx_abi ufbx_blend_deformer *ufbx_as_blend_deformer(const ufbx_element *element) {}
ufbx_abi ufbx_blend_channel *ufbx_as_blend_channel(const ufbx_element *element) {}
ufbx_abi ufbx_blend_shape *ufbx_as_blend_shape(const ufbx_element *element) {}
ufbx_abi ufbx_cache_deformer *ufbx_as_cache_deformer(const ufbx_element *element) {}
ufbx_abi ufbx_cache_file *ufbx_as_cache_file(const ufbx_element *element) {}
ufbx_abi ufbx_material *ufbx_as_material(const ufbx_element *element) {}
ufbx_abi ufbx_texture *ufbx_as_texture(const ufbx_element *element) {}
ufbx_abi ufbx_video *ufbx_as_video(const ufbx_element *element) {}
ufbx_abi ufbx_shader *ufbx_as_shader(const ufbx_element *element) {}
ufbx_abi ufbx_shader_binding *ufbx_as_shader_binding(const ufbx_element *element) {}
ufbx_abi ufbx_anim_stack *ufbx_as_anim_stack(const ufbx_element *element) {}
ufbx_abi ufbx_anim_layer *ufbx_as_anim_layer(const ufbx_element *element) {}
ufbx_abi ufbx_anim_value *ufbx_as_anim_value(const ufbx_element *element) {}
ufbx_abi ufbx_anim_curve *ufbx_as_anim_curve(const ufbx_element *element) {}
ufbx_abi ufbx_display_layer *ufbx_as_display_layer(const ufbx_element *element) {}
ufbx_abi ufbx_selection_set *ufbx_as_selection_set(const ufbx_element *element) {}
ufbx_abi ufbx_selection_node *ufbx_as_selection_node(const ufbx_element *element) {}
ufbx_abi ufbx_character *ufbx_as_character(const ufbx_element *element) {}
ufbx_abi ufbx_constraint *ufbx_as_constraint(const ufbx_element *element) {}
ufbx_abi ufbx_audio_layer *ufbx_as_audio_layer(const ufbx_element *element) {}
ufbx_abi ufbx_audio_clip *ufbx_as_audio_clip(const ufbx_element *element) {}
ufbx_abi ufbx_pose *ufbx_as_pose(const ufbx_element *element) {}
ufbx_abi ufbx_metadata_object *ufbx_as_metadata_object(const ufbx_element *element) {}

#ifdef __cplusplus
}
#endif

#endif

#if defined(_MSC_VER)
	#pragma warning(pop)
#elif defined(__clang__)
	#pragma clang diagnostic pop
#elif defined(__GNUC__)
	#pragma GCC diagnostic pop
#endif