From 9b3947c976d88bb1fd9875c102115a38ec3aff38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:32:29 +0000 Subject: [PATCH] Add tg3_validate() glTF 2.0 validation API to tiny_gltf_v3.h with tests Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> Agent-Logs-Url: https://github.com/syoyo/tinygltf/sessions/5e03ce1e-cc19-46d5-a9ae-04c373be3919 --- CMakeLists.txt | 9 + tests/v3/tester_v3.cc | 612 +++++++++++++++++++++++++++++++++++ tiny_gltf_v3.h | 717 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 1334 insertions(+), 4 deletions(-) create mode 100644 tests/v3/tester_v3.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index cfcf403..52def14 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,15 @@ if (TINYGLTF_BUILD_TESTS) ) target_compile_definitions(tester_intensive_customjson PRIVATE TINYGLTF_USE_CUSTOM_JSON) add_test(NAME tester_intensive_customjson COMMAND tester_intensive_customjson WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests) + + # v3 API tests (parser + validator) + add_executable(tester_v3 tests/v3/tester_v3.cc) + target_include_directories(tester_v3 PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/tests + ) + set_target_properties(tester_v3 PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED ON) + add_test(NAME tester_v3 COMMAND tester_v3 WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests) endif (TINYGLTF_BUILD_TESTS) # diff --git a/tests/v3/tester_v3.cc b/tests/v3/tester_v3.cc new file mode 100644 index 0000000..6cc54ba --- /dev/null +++ b/tests/v3/tester_v3.cc @@ -0,0 +1,612 @@ +/* + * tester_v3.cc โ€” Unit tests for tiny_gltf_v3.h (v3 API), including + * the tg3_validate() validation feature. + */ + +#define TINYGLTF3_IMPLEMENTATION +#include "tiny_gltf_v3.h" + +#define CATCH_CONFIG_MAIN +#include "../catch.hpp" + +#include +#include + +/* ====================================================================== + * Helpers + * ====================================================================== */ + +/* Build a minimal valid glTF 2.0 JSON string */ +static const char *MINIMAL_GLTF_JSON = + "{\"asset\":{\"version\":\"2.0\"}}"; + +/* Parse a JSON string into a tg3_model, returns error code. */ +static tg3_error_code parse_json(tg3_model *model, tg3_error_stack *errors, + const char *json) { + tg3_parse_options opts; + tg3_parse_options_init(&opts); + opts.required_sections = TG3_NO_REQUIRE; + return tg3_parse(model, errors, + (const uint8_t *)json, (uint64_t)strlen(json), + "", 0, &opts); +} + +/* ====================================================================== + * Tests: tg3_validate() โ€” basic API behaviour + * ====================================================================== */ + +TEST_CASE("v3-validate-null-model", "[v3][validate]") { + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(NULL, &errors); + REQUIRE(rc == TG3_ERR_INVALID_VALUE); + + tg3_error_stack_free(&errors); +} + +TEST_CASE("v3-validate-null-errors", "[v3][validate]") { + tg3_model model; + memset(&model, 0, sizeof(model)); + model.default_scene = -1; + + /* errors may be NULL โ€” results are silently discarded */ + tg3_error_code rc = tg3_validate(&model, NULL); + /* Empty model (no asset.version) should still return an error code */ + REQUIRE(rc != TG3_OK); /* missing asset.version */ + + tg3_model_free(&model); +} + +TEST_CASE("v3-validate-minimal-valid", "[v3][validate]") { + tg3_model model; + tg3_error_stack errors; + memset(&model, 0, sizeof(model)); + tg3_error_stack_init(&errors); + + tg3_error_code rc = parse_json(&model, &errors, MINIMAL_GLTF_JSON); + REQUIRE(rc == TG3_OK); + + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_OK); + REQUIRE(tg3_errors_has_error(&errors) == 0); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +/* ====================================================================== + * Tests: asset.version validation + * ====================================================================== */ + +TEST_CASE("v3-validate-missing-asset-version", "[v3][validate][asset]") { + tg3_model model; + tg3_error_stack errors; + memset(&model, 0, sizeof(model)); + tg3_error_stack_init(&errors); + + /* asset.version.data == NULL means absent */ + model.default_scene = -1; + tg3_error_code rc = tg3_validate(&model, &errors); + + REQUIRE(rc == TG3_ERR_MISSING_REQUIRED); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + bool found = false; + for (uint32_t i = 0; i < tg3_errors_count(&errors); i++) { + const tg3_error_entry *e = tg3_errors_get(&errors, i); + if (e->code == TG3_ERR_MISSING_REQUIRED) { found = true; break; } + } + REQUIRE(found); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +TEST_CASE("v3-validate-wrong-asset-version-warn", "[v3][validate][asset]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + /* Parse a model with version "1.0" */ + const char *json = "{\"asset\":{\"version\":\"1.0\"}}"; + tg3_error_code rc = parse_json(&model, &errors, json); + REQUIRE(rc == TG3_OK); + + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + rc = tg3_validate(&model, &errors); + /* Wrong version emits a WARNING, not an error */ + REQUIRE(rc == TG3_OK); + REQUIRE(tg3_errors_has_error(&errors) == 0); + + bool found_warn = false; + for (uint32_t i = 0; i < tg3_errors_count(&errors); i++) { + const tg3_error_entry *e = tg3_errors_get(&errors, i); + if (e->severity == TG3_SEVERITY_WARNING && + e->code == TG3_ERR_INVALID_VALUE) { + found_warn = true; break; + } + } + REQUIRE(found_warn); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +/* ====================================================================== + * Tests: default scene index + * ====================================================================== */ + +TEST_CASE("v3-validate-invalid-default-scene", "[v3][validate][scene]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + parse_json(&model, &errors, MINIMAL_GLTF_JSON); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + /* Point default_scene to a non-existent scene */ + model.default_scene = 5; + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_INDEX); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +/* ====================================================================== + * Tests: accessor validation + * ====================================================================== */ + +TEST_CASE("v3-validate-accessor-invalid-component-type", "[v3][validate][accessor]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"accessors\":[{" + " \"componentType\":9999," + " \"count\":1," + " \"type\":\"SCALAR\"" + "}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_ACCESSOR); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +TEST_CASE("v3-validate-accessor-zero-count", "[v3][validate][accessor]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + /* Build model directly */ + parse_json(&model, &errors, MINIMAL_GLTF_JSON); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + /* Inject a fake accessor with count=0 */ + tg3_accessor *acc = (tg3_accessor *)calloc(1, sizeof(tg3_accessor)); + acc->buffer_view = -1; + acc->component_type = TG3_COMPONENT_TYPE_FLOAT; + acc->type = TG3_TYPE_SCALAR; + acc->count = 0; /* invalid */ + model.accessors = acc; + model.accessors_count = 1; + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_ACCESSOR); + + /* Restore so tg3_model_free doesn't double-free */ + model.accessors = NULL; + model.accessors_count = 0; + free(acc); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +TEST_CASE("v3-validate-accessor-invalid-bufferView-index", "[v3][validate][accessor]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"accessors\":[{" + " \"bufferView\":99," + " \"componentType\":5126," + " \"count\":1," + " \"type\":\"SCALAR\"" + "}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_INDEX); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +/* ====================================================================== + * Tests: mesh validation + * ====================================================================== */ + +TEST_CASE("v3-validate-mesh-no-primitives", "[v3][validate][mesh]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"meshes\":[{\"primitives\":[]}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_MESH); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +TEST_CASE("v3-validate-mesh-indices-out-of-range", "[v3][validate][mesh]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"accessors\":[" + " {\"componentType\":5126,\"count\":3,\"type\":\"VEC3\"}" + "]," + "\"meshes\":[{" + " \"primitives\":[{" + " \"attributes\":{\"POSITION\":0}," + " \"indices\":99" + " }]" + "}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_INDEX); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +/* ====================================================================== + * Tests: node validation + * ====================================================================== */ + +TEST_CASE("v3-validate-node-invalid-mesh-index", "[v3][validate][node]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"nodes\":[{\"mesh\":42}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_INDEX); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +TEST_CASE("v3-validate-node-self-reference", "[v3][validate][node]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"nodes\":[{\"children\":[0]}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_NODE); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +/* ====================================================================== + * Tests: texture validation + * ====================================================================== */ + +TEST_CASE("v3-validate-texture-invalid-source", "[v3][validate][texture]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"textures\":[{\"source\":99}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_INDEX); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +/* ====================================================================== + * Tests: material validation + * ====================================================================== */ + +TEST_CASE("v3-validate-material-invalid-alpha-mode", "[v3][validate][material]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"materials\":[{\"alphaMode\":\"INVALID\"}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_MATERIAL); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +/* ====================================================================== + * Tests: camera validation + * ====================================================================== */ + +TEST_CASE("v3-validate-camera-perspective-bad-yfov", "[v3][validate][camera]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"cameras\":[{" + " \"type\":\"perspective\"," + " \"perspective\":{\"yfov\":0.0,\"znear\":0.01,\"zfar\":100.0," + " \"aspectRatio\":1.0}" + "}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_CAMERA); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +TEST_CASE("v3-validate-camera-invalid-type", "[v3][validate][camera]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"cameras\":[{\"type\":\"unknown\"}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_CAMERA); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +/* ====================================================================== + * Tests: scene validation + * ====================================================================== */ + +TEST_CASE("v3-validate-scene-invalid-node-index", "[v3][validate][scene]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"scenes\":[{\"nodes\":[99]}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_SCENE); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +/* ====================================================================== + * Tests: animation validation + * ====================================================================== */ + +TEST_CASE("v3-validate-animation-invalid-sampler-ref", "[v3][validate][animation]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"accessors\":[" + " {\"componentType\":5126,\"count\":2,\"type\":\"SCALAR\"}," + " {\"componentType\":5126,\"count\":2,\"type\":\"VEC3\"}" + "]," + "\"animations\":[{" + " \"samplers\":[{\"input\":0,\"output\":1,\"interpolation\":\"LINEAR\"}]," + " \"channels\":[{" + " \"sampler\":99," + " \"target\":{\"path\":\"translation\"}" + " }]" + "}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_INDEX); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +TEST_CASE("v3-validate-animation-invalid-target-path", "[v3][validate][animation]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"accessors\":[" + " {\"componentType\":5126,\"count\":2,\"type\":\"SCALAR\"}," + " {\"componentType\":5126,\"count\":2,\"type\":\"VEC3\"}" + "]," + "\"animations\":[{" + " \"samplers\":[{\"input\":0,\"output\":1,\"interpolation\":\"LINEAR\"}]," + " \"channels\":[{" + " \"sampler\":0," + " \"target\":{\"path\":\"bogus\"}" + " }]" + "}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_error_code rc = tg3_validate(&model, &errors); + REQUIRE(rc == TG3_ERR_INVALID_VALUE); + REQUIRE(tg3_errors_has_error(&errors) == 1); + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +/* ====================================================================== + * Tests: error stack msg_arena_ lifetime + * ====================================================================== */ + +TEST_CASE("v3-validate-error-messages-persist", "[v3][validate][errorstack]") { + tg3_model model; + tg3_error_stack errors; + tg3_error_stack_init(&errors); + + /* Trigger multiple validation errors */ + const char *json = + "{" + "\"asset\":{\"version\":\"2.0\"}," + "\"nodes\":[{\"mesh\":99,\"camera\":88}]," + "\"textures\":[{\"source\":77}]" + "}"; + parse_json(&model, &errors, json); + tg3_error_stack_free(&errors); + tg3_error_stack_init(&errors); + + tg3_validate(&model, &errors); + + uint32_t count = tg3_errors_count(&errors); + REQUIRE(count >= 3); + + /* All message and path pointers must be non-NULL and readable */ + for (uint32_t i = 0; i < count; i++) { + const tg3_error_entry *e = tg3_errors_get(&errors, i); + REQUIRE(e != NULL); + REQUIRE(e->message != NULL); + /* Sanity: message should not be empty */ + REQUIRE(strlen(e->message) > 0); + } + + tg3_model_free(&model); + tg3_error_stack_free(&errors); +} + +/* ====================================================================== + * Tests: C++ wrapper + * ====================================================================== */ + +TEST_CASE("v3-validate-cpp-wrapper", "[v3][validate][cpp]") { + tinygltf3::Model model; + tinygltf3::ErrorStack errors; + + const char *json = "{\"asset\":{\"version\":\"2.0\"}}"; + tg3_parse_options opts; + tg3_parse_options_init(&opts); + opts.required_sections = TG3_NO_REQUIRE; + tg3_parse_auto(model.get(), errors.get(), + (const uint8_t *)json, strlen(json), + "", 0, &opts); + + tg3_error_code rc = tinygltf3::validate(model, errors); + REQUIRE(rc == TG3_OK); + REQUIRE(errors.has_error() == false); +} diff --git a/tiny_gltf_v3.h b/tiny_gltf_v3.h index 8d8a92b..934f5d1 100644 --- a/tiny_gltf_v3.h +++ b/tiny_gltf_v3.h @@ -231,6 +231,9 @@ typedef struct tg3_allocator { void *user_data; } tg3_allocator; +/* Forward declaration of arena type (defined in the implementation section) */ +struct tg3_arena; + /* ====================================================================== * Section 7: Error Reporting * ====================================================================== */ @@ -309,10 +312,11 @@ typedef struct tg3_error_entry { } tg3_error_entry; typedef struct tg3_error_stack { - tg3_error_entry *entries; - uint32_t count; - uint32_t capacity; - int32_t has_error; /* 1 if any entry with severity == ERROR */ + tg3_error_entry *entries; + uint32_t count; + uint32_t capacity; + int32_t has_error; /* 1 if any entry with severity == ERROR */ + struct tg3_arena *msg_arena_; /* Internal: arena for dynamic validation message strings */ } tg3_error_stack; /* Error stack query functions */ @@ -970,6 +974,19 @@ TINYGLTF3_API void tg3_write_options_init(tg3_write_options *options); TINYGLTF3_API void tg3_error_stack_init(tg3_error_stack *es); TINYGLTF3_API void tg3_error_stack_free(tg3_error_stack *es); +/* ====================================================================== + * Section 13.5: Validation API + * ====================================================================== */ + +/* Validate a parsed model against the glTF 2.0 specification. + * Errors and warnings are appended to 'errors'. + * Returns TG3_OK if no validation errors are found, or the first error + * code encountered otherwise. + * 'model' must not be NULL; 'errors' may be NULL (results discarded). */ +TINYGLTF3_API tg3_error_code tg3_validate( + const tg3_model *model, + tg3_error_stack *errors); + /* ====================================================================== * Section 14: Writer API * ====================================================================== */ @@ -1111,6 +1128,10 @@ inline tg3_error_code parse(Model &model, ErrorStack &errors, options); } +inline tg3_error_code validate(const Model &model, ErrorStack &errors) { + return tg3_validate(model.get(), errors.get()); +} + } /* namespace tinygltf3 */ #endif /* __cplusplus */ @@ -1469,6 +1490,9 @@ TINYGLTF3_API void tg3_error_stack_init(tg3_error_stack *es) { TINYGLTF3_API void tg3_error_stack_free(tg3_error_stack *es) { if (!es) return; free(es->entries); + if (es->msg_arena_) { + tg3__arena_destroy(es->msg_arena_); + } memset(es, 0, sizeof(tg3_error_stack)); } @@ -4431,6 +4455,691 @@ TINYGLTF3_API void tg3_writer_destroy(tg3_writer *w) { delete w; } +/* ====================================================================== + * Public: Validation API Implementation + * ====================================================================== */ + +/* Internal helper: push a validation error/warning with arena-duped + * message and json_path strings so they outlive the validate function. */ +static void tg3__val_push(tg3_error_stack *es, tg3_arena *arena, + tg3_severity sev, tg3_error_code code, + const char *json_path_buf, const char *fmt, ...) { + if (!es) return; + + char msg_buf[1024]; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(msg_buf, sizeof(msg_buf), fmt, ap); + va_end(ap); + if (n < 0) n = 0; + if ((size_t)n >= sizeof(msg_buf)) n = (int)(sizeof(msg_buf) - 1); + + const char *msg = msg_buf; + const char *path = json_path_buf; + + if (arena) { + char *m = tg3__arena_strdup(arena, msg_buf, (size_t)n); + if (m) msg = m; + if (json_path_buf) { + char *p = tg3__arena_strdup(arena, json_path_buf, strlen(json_path_buf)); + if (p) path = p; + } + } + + tg3__error_push(es, sev, code, msg, path, -1); +} + +/* Helper: validate a tg3_texture_info index. */ +static void tg3__val_check_tex_index(tg3_error_stack *es, tg3_arena *arena, + int32_t tex_idx, uint32_t textures_count, + uint32_t mat_idx, const char *field_name) { + if (tex_idx < 0) return; /* absent */ + if ((uint32_t)tex_idx >= textures_count) { + char path[128]; + snprintf(path, sizeof(path), "/materials/%u", mat_idx); + tg3__val_push(es, arena, TG3_SEVERITY_ERROR, TG3_ERR_INVALID_INDEX, + path, + "material[%u].%s texture index %d out of range [0, %u)", + mat_idx, field_name, tex_idx, textures_count); + } +} + +TINYGLTF3_API tg3_error_code tg3_validate( + const tg3_model *model, + tg3_error_stack *errors) { + + if (!model) return TG3_ERR_INVALID_VALUE; + + /* Set up arena for message and path strings */ + tg3_arena *arena = NULL; + if (errors) { + if (!errors->msg_arena_) { + tg3_memory_config cfg; + memset(&cfg, 0, sizeof(cfg)); + errors->msg_arena_ = tg3__arena_create(&cfg); + } + arena = errors->msg_arena_; + } + + tg3_error_code first_err = TG3_OK; + +/* Push an error and record the first error code seen */ +#define TG3__VERR(code, path_buf, ...) \ + do { \ + tg3__val_push(errors, arena, TG3_SEVERITY_ERROR, (code), (path_buf), __VA_ARGS__); \ + if (first_err == TG3_OK) first_err = (code); \ + } while (0) + +/* Push a warning (does not set first_err) */ +#define TG3__VWARN(code, path_buf, ...) \ + tg3__val_push(errors, arena, TG3_SEVERITY_WARNING, (code), (path_buf), __VA_ARGS__) + + /* Temporary path buffer used in all per-entity loops. + * Content is always arena-duped inside tg3__val_push before returning. */ + char path[256]; + char ipath[512]; /* inner path for nested entities */ + + /* ------------------------------------------------------------------ + * 1. Asset + * ------------------------------------------------------------------ */ + if (!model->asset.version.data) { + TG3__VERR(TG3_ERR_MISSING_REQUIRED, "/asset", + "asset.version is required"); + } else if (!tg3_str_equals_cstr(model->asset.version, "2.0")) { + TG3__VWARN(TG3_ERR_INVALID_VALUE, "/asset/version", + "asset.version is \"%.*s\", expected \"2.0\"", + (int)model->asset.version.len, model->asset.version.data); + } + + /* ------------------------------------------------------------------ + * 2. Default scene index + * ------------------------------------------------------------------ */ + if (model->default_scene >= 0 && + (uint32_t)model->default_scene >= model->scenes_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, "/scene", + "default scene index %d out of range [0, %u)", + model->default_scene, model->scenes_count); + } + + /* ------------------------------------------------------------------ + * 3. Buffer views + * ------------------------------------------------------------------ */ + for (uint32_t i = 0; i < model->buffer_views_count; i++) { + const tg3_buffer_view *bv = &model->buffer_views[i]; + snprintf(path, sizeof(path), "/bufferViews/%u", i); + + if (bv->buffer < 0 || (uint32_t)bv->buffer >= model->buffers_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "bufferView[%u].buffer index %d out of range [0, %u)", + i, bv->buffer, model->buffers_count); + } else { + const tg3_buffer *buf = &model->buffers[(uint32_t)bv->buffer]; + if (bv->byte_offset + bv->byte_length > buf->data.count) { + TG3__VERR(TG3_ERR_INVALID_BUFFER_VIEW, path, + "bufferView[%u] byteOffset+byteLength (%llu) exceeds " + "buffer[%d].byteLength (%llu)", + i, + (unsigned long long)(bv->byte_offset + bv->byte_length), + bv->buffer, + (unsigned long long)buf->data.count); + } + } + + if (bv->byte_length == 0) { + TG3__VWARN(TG3_ERR_INVALID_BUFFER_VIEW, path, + "bufferView[%u].byteLength is 0", i); + } + + if (bv->byte_stride != 0) { + /* glTF 2.0 spec ยง3.6.2.4: byteStride must be in [4, 252] and + * a multiple of 4. */ + if (bv->byte_stride < 4 || bv->byte_stride > 252) { + TG3__VERR(TG3_ERR_INVALID_BUFFER_VIEW, path, + "bufferView[%u].byteStride %u must be in [4, 252]", + i, bv->byte_stride); + } else if (bv->byte_stride % 4 != 0) { + TG3__VERR(TG3_ERR_INVALID_BUFFER_VIEW, path, + "bufferView[%u].byteStride %u must be a multiple of 4", + i, bv->byte_stride); + } + } + } + + /* ------------------------------------------------------------------ + * 4. Accessors + * ------------------------------------------------------------------ */ + for (uint32_t i = 0; i < model->accessors_count; i++) { + const tg3_accessor *acc = &model->accessors[i]; + snprintf(path, sizeof(path), "/accessors/%u", i); + + /* Validate componentType */ + int valid_ct = (acc->component_type == TG3_COMPONENT_TYPE_BYTE || + acc->component_type == TG3_COMPONENT_TYPE_UNSIGNED_BYTE || + acc->component_type == TG3_COMPONENT_TYPE_SHORT || + acc->component_type == TG3_COMPONENT_TYPE_UNSIGNED_SHORT || + acc->component_type == TG3_COMPONENT_TYPE_UNSIGNED_INT || + acc->component_type == TG3_COMPONENT_TYPE_FLOAT); + if (!valid_ct) { + TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path, + "accessor[%u].componentType %d is not a valid glTF 2.0 componentType", + i, acc->component_type); + } + + /* Validate type */ + int valid_type = (acc->type == TG3_TYPE_SCALAR || + acc->type == TG3_TYPE_VEC2 || + acc->type == TG3_TYPE_VEC3 || + acc->type == TG3_TYPE_VEC4 || + acc->type == TG3_TYPE_MAT2 || + acc->type == TG3_TYPE_MAT3 || + acc->type == TG3_TYPE_MAT4); + if (!valid_type) { + TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path, + "accessor[%u].type %d is not a valid glTF 2.0 accessor type", + i, acc->type); + } + + /* count >= 1 */ + if (acc->count == 0) { + TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path, + "accessor[%u].count must be >= 1", i); + } + + /* bufferView cross-reference and byte bounds */ + if (acc->buffer_view >= 0) { + if ((uint32_t)acc->buffer_view >= model->buffer_views_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "accessor[%u].bufferView index %d out of range [0, %u)", + i, acc->buffer_view, model->buffer_views_count); + } else if (valid_ct && valid_type && acc->count > 0) { + const tg3_buffer_view *bv = + &model->buffer_views[(uint32_t)acc->buffer_view]; + int32_t comp_sz = tg3_component_size(acc->component_type); + int32_t num_comp = tg3_num_components(acc->type); + if (comp_sz > 0 && num_comp > 0) { + int32_t elem_sz = comp_sz * num_comp; + int32_t stride = (bv->byte_stride > 0) + ? (int32_t)bv->byte_stride + : elem_sz; + uint64_t max_byte = + acc->byte_offset + + (uint64_t)(acc->count - 1) * (uint64_t)stride + + (uint64_t)elem_sz; + if (max_byte > bv->byte_length) { + TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path, + "accessor[%u] byte range exceeds " + "bufferView[%d].byteLength (%llu): " + "needs %llu bytes", + i, acc->buffer_view, + (unsigned long long)bv->byte_length, + (unsigned long long)max_byte); + } + /* byteOffset alignment */ + if (acc->byte_offset % (uint64_t)comp_sz != 0) { + TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path, + "accessor[%u].byteOffset %llu is not aligned " + "to componentType size %d", + i, (unsigned long long)acc->byte_offset, + comp_sz); + } + } + } + } + + /* Sparse accessor sub-structure */ + if (acc->sparse.is_sparse) { + if (acc->sparse.count <= 0) { + TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path, + "accessor[%u].sparse.count must be > 0", i); + } + if (acc->sparse.indices.buffer_view < 0 || + (uint32_t)acc->sparse.indices.buffer_view >= + model->buffer_views_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "accessor[%u].sparse.indices.bufferView index %d " + "out of range [0, %u)", + i, acc->sparse.indices.buffer_view, + model->buffer_views_count); + } + if (acc->sparse.values.buffer_view < 0 || + (uint32_t)acc->sparse.values.buffer_view >= + model->buffer_views_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "accessor[%u].sparse.values.bufferView index %d " + "out of range [0, %u)", + i, acc->sparse.values.buffer_view, + model->buffer_views_count); + } + } + } + + /* ------------------------------------------------------------------ + * 5. Meshes + * ------------------------------------------------------------------ */ + for (uint32_t i = 0; i < model->meshes_count; i++) { + const tg3_mesh *mesh = &model->meshes[i]; + snprintf(path, sizeof(path), "/meshes/%u", i); + + if (mesh->primitives_count == 0) { + TG3__VERR(TG3_ERR_INVALID_MESH, path, + "mesh[%u] must have at least one primitive", i); + } + + for (uint32_t pi = 0; pi < mesh->primitives_count; pi++) { + const tg3_primitive *prim = &mesh->primitives[pi]; + snprintf(ipath, sizeof(ipath), "/meshes/%u/primitives/%u", i, pi); + + if (prim->attributes_count == 0) { + TG3__VERR(TG3_ERR_INVALID_MESH, ipath, + "mesh[%u].primitives[%u] must have at least one attribute", + i, pi); + } + + /* Validate attribute accessor indices */ + for (uint32_t ai = 0; ai < prim->attributes_count; ai++) { + int32_t acc_idx = prim->attributes[ai].value; + if (acc_idx < 0 || + (uint32_t)acc_idx >= model->accessors_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, ipath, + "mesh[%u].primitives[%u] attribute \"%.*s\" " + "accessor index %d out of range [0, %u)", + i, pi, + (int)prim->attributes[ai].key.len, + prim->attributes[ai].key.data, + acc_idx, model->accessors_count); + } + } + + /* Validate indices accessor */ + if (prim->indices >= 0) { + if ((uint32_t)prim->indices >= model->accessors_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, ipath, + "mesh[%u].primitives[%u].indices accessor index %d " + "out of range [0, %u)", + i, pi, prim->indices, model->accessors_count); + } else { + const tg3_accessor *idx_acc = + &model->accessors[(uint32_t)prim->indices]; + if (idx_acc->type != TG3_TYPE_SCALAR) { + TG3__VERR(TG3_ERR_INVALID_ACCESSOR, ipath, + "mesh[%u].primitives[%u].indices accessor " + "must be SCALAR type", + i, pi); + } + if (idx_acc->component_type != + TG3_COMPONENT_TYPE_UNSIGNED_BYTE && + idx_acc->component_type != + TG3_COMPONENT_TYPE_UNSIGNED_SHORT && + idx_acc->component_type != + TG3_COMPONENT_TYPE_UNSIGNED_INT) { + TG3__VERR(TG3_ERR_INVALID_ACCESSOR, ipath, + "mesh[%u].primitives[%u].indices accessor " + "componentType must be UNSIGNED_BYTE, " + "UNSIGNED_SHORT, or UNSIGNED_INT", + i, pi); + } + } + } + + /* Validate material index */ + if (prim->material >= 0 && + (uint32_t)prim->material >= model->materials_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, ipath, + "mesh[%u].primitives[%u].material index %d " + "out of range [0, %u)", + i, pi, prim->material, model->materials_count); + } + + /* Validate primitive mode */ + if (prim->mode < TG3_MODE_POINTS || prim->mode > TG3_MODE_TRIANGLE_FAN) { + TG3__VERR(TG3_ERR_INVALID_MESH, ipath, + "mesh[%u].primitives[%u].mode %d is not valid " + "(must be TG3_MODE_POINTS..TG3_MODE_TRIANGLE_FAN)", + i, pi, prim->mode); + } + } + } + + /* ------------------------------------------------------------------ + * 6. Nodes + * ------------------------------------------------------------------ */ + for (uint32_t i = 0; i < model->nodes_count; i++) { + const tg3_node *node = &model->nodes[i]; + snprintf(path, sizeof(path), "/nodes/%u", i); + + if (node->camera >= 0 && (uint32_t)node->camera >= model->cameras_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "node[%u].camera index %d out of range [0, %u)", + i, node->camera, model->cameras_count); + } + if (node->skin >= 0 && (uint32_t)node->skin >= model->skins_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "node[%u].skin index %d out of range [0, %u)", + i, node->skin, model->skins_count); + } + if (node->mesh >= 0 && (uint32_t)node->mesh >= model->meshes_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "node[%u].mesh index %d out of range [0, %u)", + i, node->mesh, model->meshes_count); + } + if (node->light >= 0 && (uint32_t)node->light >= model->lights_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "node[%u].light index %d out of range [0, %u)", + i, node->light, model->lights_count); + } + + for (uint32_t ci = 0; ci < node->children_count; ci++) { + int32_t child_idx = node->children[ci]; + if (child_idx < 0 || (uint32_t)child_idx >= model->nodes_count) { + TG3__VERR(TG3_ERR_INVALID_NODE, path, + "node[%u].children[%u] = %d is out of range [0, %u)", + i, ci, child_idx, model->nodes_count); + } else if ((uint32_t)child_idx == i) { + TG3__VERR(TG3_ERR_INVALID_NODE, path, + "node[%u].children[%u] = %d creates a self-reference", + i, ci, child_idx); + } + } + } + + /* ------------------------------------------------------------------ + * 7. Skins + * ------------------------------------------------------------------ */ + for (uint32_t i = 0; i < model->skins_count; i++) { + const tg3_skin *skin = &model->skins[i]; + snprintf(path, sizeof(path), "/skins/%u", i); + + if (skin->joints_count == 0) { + TG3__VERR(TG3_ERR_INVALID_SKIN, path, + "skin[%u] must have at least one joint", i); + } + + for (uint32_t ji = 0; ji < skin->joints_count; ji++) { + int32_t j = skin->joints[ji]; + if (j < 0 || (uint32_t)j >= model->nodes_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "skin[%u].joints[%u] = %d out of range [0, %u)", + i, ji, j, model->nodes_count); + } + } + + if (skin->inverse_bind_matrices >= 0) { + if ((uint32_t)skin->inverse_bind_matrices >= model->accessors_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "skin[%u].inverseBindMatrices index %d " + "out of range [0, %u)", + i, skin->inverse_bind_matrices, + model->accessors_count); + } else { + const tg3_accessor *ibm = + &model->accessors[(uint32_t)skin->inverse_bind_matrices]; + if (ibm->type != TG3_TYPE_MAT4) { + TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path, + "skin[%u].inverseBindMatrices accessor " + "must be MAT4 type", i); + } + if (ibm->component_type != TG3_COMPONENT_TYPE_FLOAT) { + TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path, + "skin[%u].inverseBindMatrices accessor " + "must have FLOAT componentType", i); + } + if (ibm->count < (uint64_t)skin->joints_count) { + TG3__VERR(TG3_ERR_INVALID_SKIN, path, + "skin[%u].inverseBindMatrices count %llu < " + "joints count %u", + i, (unsigned long long)ibm->count, + skin->joints_count); + } + } + } + + if (skin->skeleton >= 0 && + (uint32_t)skin->skeleton >= model->nodes_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "skin[%u].skeleton index %d out of range [0, %u)", + i, skin->skeleton, model->nodes_count); + } + } + + /* ------------------------------------------------------------------ + * 8. Animations + * ------------------------------------------------------------------ */ + for (uint32_t i = 0; i < model->animations_count; i++) { + const tg3_animation *anim = &model->animations[i]; + snprintf(path, sizeof(path), "/animations/%u", i); + + for (uint32_t si = 0; si < anim->samplers_count; si++) { + const tg3_animation_sampler *samp = &anim->samplers[si]; + snprintf(ipath, sizeof(ipath), + "/animations/%u/samplers/%u", i, si); + + if (samp->input < 0 || + (uint32_t)samp->input >= model->accessors_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, ipath, + "animation[%u].samplers[%u].input accessor index %d " + "out of range [0, %u)", + i, si, samp->input, model->accessors_count); + } + if (samp->output < 0 || + (uint32_t)samp->output >= model->accessors_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, ipath, + "animation[%u].samplers[%u].output accessor index %d " + "out of range [0, %u)", + i, si, samp->output, model->accessors_count); + } + + if (samp->interpolation.data) { + if (!tg3_str_equals_cstr(samp->interpolation, "LINEAR") && + !tg3_str_equals_cstr(samp->interpolation, "STEP") && + !tg3_str_equals_cstr(samp->interpolation, "CUBICSPLINE")) { + TG3__VWARN(TG3_ERR_INVALID_VALUE, ipath, + "animation[%u].samplers[%u].interpolation " + "\"%.*s\" is not a recognised value", + i, si, + (int)samp->interpolation.len, + samp->interpolation.data); + } + } + } + + for (uint32_t ci = 0; ci < anim->channels_count; ci++) { + const tg3_animation_channel *chan = &anim->channels[ci]; + snprintf(ipath, sizeof(ipath), + "/animations/%u/channels/%u", i, ci); + + if (chan->sampler < 0 || + (uint32_t)chan->sampler >= anim->samplers_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, ipath, + "animation[%u].channels[%u].sampler index %d " + "out of range [0, %u)", + i, ci, chan->sampler, anim->samplers_count); + } + + if (chan->target.node >= 0 && + (uint32_t)chan->target.node >= model->nodes_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, ipath, + "animation[%u].channels[%u].target.node index %d " + "out of range [0, %u)", + i, ci, chan->target.node, model->nodes_count); + } + + if (!chan->target.path.data || chan->target.path.len == 0) { + TG3__VERR(TG3_ERR_MISSING_REQUIRED, ipath, + "animation[%u].channels[%u].target.path is required", + i, ci); + } else if ( + !tg3_str_equals_cstr(chan->target.path, "translation") && + !tg3_str_equals_cstr(chan->target.path, "rotation") && + !tg3_str_equals_cstr(chan->target.path, "scale") && + !tg3_str_equals_cstr(chan->target.path, "weights")) { + TG3__VERR(TG3_ERR_INVALID_VALUE, ipath, + "animation[%u].channels[%u].target.path \"%.*s\" " + "must be \"translation\", \"rotation\", " + "\"scale\", or \"weights\"", + i, ci, + (int)chan->target.path.len, + chan->target.path.data); + } + } + } + + /* ------------------------------------------------------------------ + * 9. Textures + * ------------------------------------------------------------------ */ + for (uint32_t i = 0; i < model->textures_count; i++) { + const tg3_texture *tex = &model->textures[i]; + snprintf(path, sizeof(path), "/textures/%u", i); + + if (tex->sampler >= 0 && + (uint32_t)tex->sampler >= model->samplers_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "texture[%u].sampler index %d out of range [0, %u)", + i, tex->sampler, model->samplers_count); + } + if (tex->source >= 0 && + (uint32_t)tex->source >= model->images_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "texture[%u].source index %d out of range [0, %u)", + i, tex->source, model->images_count); + } + } + + /* ------------------------------------------------------------------ + * 10. Materials + * ------------------------------------------------------------------ */ + for (uint32_t i = 0; i < model->materials_count; i++) { + const tg3_material *mat = &model->materials[i]; + snprintf(path, sizeof(path), "/materials/%u", i); + + if (mat->alpha_mode.data) { + if (!tg3_str_equals_cstr(mat->alpha_mode, "OPAQUE") && + !tg3_str_equals_cstr(mat->alpha_mode, "MASK") && + !tg3_str_equals_cstr(mat->alpha_mode, "BLEND")) { + TG3__VERR(TG3_ERR_INVALID_MATERIAL, path, + "material[%u].alphaMode \"%.*s\" must be " + "\"OPAQUE\", \"MASK\", or \"BLEND\"", + i, (int)mat->alpha_mode.len, mat->alpha_mode.data); + } + } + + tg3__val_check_tex_index(errors, arena, + mat->pbr_metallic_roughness.base_color_texture.index, + model->textures_count, i, + "pbrMetallicRoughness.baseColorTexture"); + tg3__val_check_tex_index(errors, arena, + mat->pbr_metallic_roughness.metallic_roughness_texture.index, + model->textures_count, i, + "pbrMetallicRoughness.metallicRoughnessTexture"); + tg3__val_check_tex_index(errors, arena, + mat->normal_texture.index, + model->textures_count, i, "normalTexture"); + tg3__val_check_tex_index(errors, arena, + mat->occlusion_texture.index, + model->textures_count, i, "occlusionTexture"); + tg3__val_check_tex_index(errors, arena, + mat->emissive_texture.index, + model->textures_count, i, "emissiveTexture"); + } + + /* ------------------------------------------------------------------ + * 11. Cameras + * ------------------------------------------------------------------ */ + for (uint32_t i = 0; i < model->cameras_count; i++) { + const tg3_camera *cam = &model->cameras[i]; + snprintf(path, sizeof(path), "/cameras/%u", i); + + if (!cam->type.data || cam->type.len == 0) { + TG3__VERR(TG3_ERR_MISSING_REQUIRED, path, + "camera[%u].type is required", i); + } else if (tg3_str_equals_cstr(cam->type, "perspective")) { + if (cam->perspective.yfov <= 0.0) { + TG3__VERR(TG3_ERR_INVALID_CAMERA, path, + "camera[%u].perspective.yfov must be > 0", i); + } + if (cam->perspective.znear <= 0.0) { + TG3__VERR(TG3_ERR_INVALID_CAMERA, path, + "camera[%u].perspective.znear must be > 0", i); + } + } else if (tg3_str_equals_cstr(cam->type, "orthographic")) { + if (cam->orthographic.xmag == 0.0) { + TG3__VERR(TG3_ERR_INVALID_CAMERA, path, + "camera[%u].orthographic.xmag must not be 0", i); + } + if (cam->orthographic.ymag == 0.0) { + TG3__VERR(TG3_ERR_INVALID_CAMERA, path, + "camera[%u].orthographic.ymag must not be 0", i); + } + if (cam->orthographic.znear < 0.0) { + TG3__VERR(TG3_ERR_INVALID_CAMERA, path, + "camera[%u].orthographic.znear must be >= 0", i); + } + if (cam->orthographic.zfar <= cam->orthographic.znear) { + TG3__VERR(TG3_ERR_INVALID_CAMERA, path, + "camera[%u].orthographic.zfar must be > znear", i); + } + } else { + TG3__VERR(TG3_ERR_INVALID_CAMERA, path, + "camera[%u].type \"%.*s\" must be " + "\"perspective\" or \"orthographic\"", + i, (int)cam->type.len, cam->type.data); + } + } + + /* ------------------------------------------------------------------ + * 12. Scenes + * ------------------------------------------------------------------ */ + for (uint32_t i = 0; i < model->scenes_count; i++) { + const tg3_scene *scene = &model->scenes[i]; + snprintf(path, sizeof(path), "/scenes/%u", i); + + for (uint32_t ni = 0; ni < scene->nodes_count; ni++) { + int32_t n = scene->nodes[ni]; + if (n < 0 || (uint32_t)n >= model->nodes_count) { + TG3__VERR(TG3_ERR_INVALID_SCENE, path, + "scene[%u].nodes[%u] = %d out of range [0, %u)", + i, ni, n, model->nodes_count); + } + } + } + + /* ------------------------------------------------------------------ + * 13. Images + * ------------------------------------------------------------------ */ + for (uint32_t i = 0; i < model->images_count; i++) { + const tg3_image *img = &model->images[i]; + snprintf(path, sizeof(path), "/images/%u", i); + + if (img->buffer_view >= 0 && + (uint32_t)img->buffer_view >= model->buffer_views_count) { + TG3__VERR(TG3_ERR_INVALID_INDEX, path, + "image[%u].bufferView index %d out of range [0, %u)", + i, img->buffer_view, model->buffer_views_count); + } + + /* An image should have either a URI or a bufferView */ + { + int has_uri = (img->uri.data != NULL && img->uri.len > 0); + int has_bv = (img->buffer_view >= 0); + if (!has_uri && !has_bv && img->image.count == 0) { + TG3__VWARN(TG3_ERR_INVALID_IMAGE, path, + "image[%u] has no URI, bufferView, or decoded data", + i); + } + if (has_uri && has_bv) { + TG3__VWARN(TG3_ERR_INVALID_IMAGE, path, + "image[%u] has both URI and bufferView; " + "bufferView takes precedence", + i); + } + } + } + +#undef TG3__VERR +#undef TG3__VWARN + + return first_err; +} + #endif /* TINYGLTF3_IMPLEMENTATION */ #endif /* TINY_GLTF_V3_H_ */