Compare commits

...

9 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
0fb9c9421d Add non-blocking glTF validator CI step
Agent-Logs-Url: https://github.com/syoyo/tinygltf/sessions/12f39a87-22c8-439b-aa5e-c72eade32ee8

Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com>
2026-05-02 20:02:00 +00:00
copilot-swe-agent[bot]
ebc9f765c5 Add tg3_validate CLI validator tool
Agent-Logs-Url: https://github.com/syoyo/tinygltf/sessions/2ad9e4bb-7df4-45fa-b24b-b0117c3eb7a4

Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com>
2026-05-02 18:33:51 +00:00
copilot-swe-agent[bot]
5a3f56d43c Polish validator follow-up changes
Agent-Logs-Url: https://github.com/syoyo/tinygltf/sessions/0e775683-9da6-4f7f-8895-b73bc251faee

Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com>
2026-05-02 16:52:27 +00:00
copilot-swe-agent[bot]
577f08a680 Address validator review findings
Agent-Logs-Url: https://github.com/syoyo/tinygltf/sessions/0e775683-9da6-4f7f-8895-b73bc251faee

Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com>
2026-05-02 16:48:48 +00:00
Syoyo Fujita
aa41d25054 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-03 01:42:48 +09:00
copilot-swe-agent[bot]
721430aa5f Fix missing effective accessor alignment validation
Agent-Logs-Url: https://github.com/syoyo/tinygltf/sessions/acb44b6b-9b47-4b84-a4ab-fae35b4b5a71

Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com>
2026-05-01 21:51:03 +00:00
copilot-swe-agent[bot]
87b0d175c6 Fix 3 bugs found in deep review: first_err tracking, false positive buffer bounds, OOM dangling pointer
Agent-Logs-Url: https://github.com/syoyo/tinygltf/sessions/93d57405-0fe2-43ef-a8fa-87f7bdd8447d

Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com>
2026-04-05 19:52:23 +00:00
copilot-swe-agent[bot]
9b3947c976 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
2026-03-25 02:32:29 +00:00
copilot-swe-agent[bot]
9bcfcea6e0 Initial plan 2026-03-25 02:04:44 +00:00
6 changed files with 2123 additions and 8 deletions

View File

@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v4
- name: Configure
run: cmake -B build -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON
run: cmake -B build -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON -DTINYGLTF_BUILD_V3_VALIDATOR_TOOL=ON
- name: Build
run: cmake --build build
@@ -37,6 +37,17 @@ jobs:
- name: Run tests
run: ctest --test-dir build --output-on-failure
- name: Run v3 validator on tracked glTF files
continue-on-error: true
run: |
rc=0
while IFS= read -r path; do
echo "::group::Validate ${path}"
./build/tools/validator/tinygltf3-validator "${path}" || rc=1
echo "::endgroup::"
done < <(git ls-files '*.gltf')
exit $rc
# Linux x64 - Clang 21
linux-clang-x64:
runs-on: ubuntu-24.04

View File

@@ -13,6 +13,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
option(TINYGLTF_BUILD_LOADER_EXAMPLE "Build loader_example(load glTF and dump infos)" ON)
option(TINYGLTF_BUILD_GL_EXAMPLES "Build GL exampels(requires glfw, OpenGL, etc)" OFF)
option(TINYGLTF_BUILD_VALIDATOR_EXAMPLE "Build validator exampe" OFF)
option(TINYGLTF_BUILD_V3_VALIDATOR_TOOL "Build tg3_validate CLI tool" OFF)
option(TINYGLTF_BUILD_BUILDER_EXAMPLE "Build glTF builder example" OFF)
option(TINYGLTF_BUILD_TESTS "Build unit tests" OFF)
option(TINYGLTF_HEADER_ONLY "On: header-only mode. Off: create tinygltf library(No TINYGLTF_IMPLEMENTATION required in your project)" OFF)
@@ -35,6 +36,10 @@ if (TINYGLTF_BUILD_VALIDATOR_EXAMPLE)
add_subdirectory( examples/validator )
endif (TINYGLTF_BUILD_VALIDATOR_EXAMPLE)
if (TINYGLTF_BUILD_V3_VALIDATOR_TOOL)
add_subdirectory( tools/validator )
endif (TINYGLTF_BUILD_V3_VALIDATOR_TOOL)
if (TINYGLTF_BUILD_BUILDER_EXAMPLE)
add_subdirectory ( examples/build-gltf )
endif (TINYGLTF_BUILD_BUILDER_EXAMPLE)
@@ -65,6 +70,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)
#

1079
tests/v3/tester_v3.cc Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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 */
@@ -388,6 +392,7 @@ typedef struct tg3_asset {
typedef struct tg3_buffer {
tg3_str name;
tg3_span_u8 data;
uint64_t byte_length; /* Declared glTF buffer.byteLength */
tg3_str uri;
tg3_extras_ext ext;
} tg3_buffer;
@@ -623,6 +628,8 @@ typedef struct tg3_perspective_camera {
double yfov;
double zfar; /* 0 = infinite */
double znear;
int32_t has_aspect_ratio;
int32_t has_zfar;
tg3_extras_ext ext;
} tg3_perspective_camera;
@@ -970,6 +977,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 +1131,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 +1493,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));
}
@@ -2333,6 +2360,7 @@ static int tg3__parse_buffer(tg3__parse_ctx *ctx, const tg3__json &o,
uint64_t byte_length = 0;
tg3__parse_uint64(ctx, o, "byteLength", &byte_length, 1, "/buffer");
buf->byte_length = byte_length;
/* Load buffer data */
if (ctx->is_binary && buf_idx == 0 && buf->uri.len == 0) {
@@ -2801,6 +2829,10 @@ static int tg3__parse_camera(tg3__parse_ctx *ctx, const tg3__json &o,
if (cam->type.data && tg3_str_equals_cstr(cam->type, "perspective")) {
auto p_it = o.find("perspective");
if (p_it != o.end() && p_it->is_object()) {
cam->perspective.has_aspect_ratio =
((*p_it).find("aspectRatio") != (*p_it).end()) ? 1 : 0;
cam->perspective.has_zfar =
((*p_it).find("zfar") != (*p_it).end()) ? 1 : 0;
tg3__parse_double(ctx, *p_it, "aspectRatio",
&cam->perspective.aspect_ratio, 0, "/camera/perspective");
tg3__parse_double(ctx, *p_it, "yfov",
@@ -3717,7 +3749,7 @@ static tg3__json tg3__serialize_buffer(const tg3_buffer *b, int wd,
(void)wd;
tg3__json o = tg3__json::object();
tg3__serialize_str(o, "name", b->name);
o["byteLength"] = (int64_t)b->data.count;
o["byteLength"] = (int64_t)(b->byte_length ? b->byte_length : b->data.count);
if (b->uri.data && b->uri.len > 0) {
tg3__serialize_str(o, "uri", b->uri);
@@ -4054,10 +4086,10 @@ static tg3__json tg3__serialize_camera(const tg3_camera *c, int wd) {
if (c->type.data && tg3_str_equals_cstr(c->type, "perspective")) {
tg3__json p = tg3__json::object();
if (c->perspective.aspect_ratio > 0)
if (c->perspective.has_aspect_ratio)
p["aspectRatio"] = c->perspective.aspect_ratio;
p["yfov"] = c->perspective.yfov;
if (c->perspective.zfar > 0) p["zfar"] = c->perspective.zfar;
if (c->perspective.has_zfar) p["zfar"] = c->perspective.zfar;
p["znear"] = c->perspective.znear;
tg3__serialize_extras_ext(p, &c->perspective.ext);
o["perspective"] = static_cast<tg3__json&&>(p);
@@ -4431,6 +4463,868 @@ 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.
* If arena allocation fails (OOM) the error entry is silently dropped
* rather than storing dangling stack pointers. */
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 = NULL;
const char *path = NULL;
if (arena) {
msg = tg3__arena_strdup(arena, msg_buf, (size_t)n);
if (json_path_buf) {
path = tg3__arena_strdup(arena, json_path_buf,
strlen(json_path_buf));
}
}
/* If arena duplication failed (OOM), drop the entry to avoid storing
* dangling stack pointers. */
if (!msg) return;
tg3__error_push(es, sev, code, msg, path, -1);
}
/* Helper: validate a tg3_texture_info index.
* Returns 1 if an error was pushed, 0 otherwise. */
static int 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 0; /* 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);
return 1;
}
return 0;
}
static uint64_t tg3__val_accessor_elem_size_with_padding(
int32_t component_type,
int32_t type) {
int32_t comp_sz = tg3_component_size(component_type);
if (comp_sz <= 0) return 0;
switch (type) {
case TG3_TYPE_MAT2: {
uint64_t column_stride = ((uint64_t)(2 * comp_sz) + 3ull) & ~3ull;
return 2ull * column_stride;
}
case TG3_TYPE_MAT3: {
uint64_t column_stride = ((uint64_t)(3 * comp_sz) + 3ull) & ~3ull;
return 3ull * column_stride;
}
case TG3_TYPE_MAT4: {
uint64_t column_stride = ((uint64_t)(4 * comp_sz) + 3ull) & ~3ull;
return 4ull * column_stride;
}
default: {
int32_t num_comp = tg3_num_components(type);
if (num_comp <= 0) return 0;
return (uint64_t)comp_sz * (uint64_t)num_comp;
}
}
}
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];
uint64_t declared_len =
(buf->byte_length > 0) ? buf->byte_length : buf->data.count;
uint64_t end = bv->byte_offset + bv->byte_length;
if (end > declared_len) {
TG3__VERR(TG3_ERR_INVALID_BUFFER_VIEW, path,
"bufferView[%u] byteOffset+byteLength (%llu) exceeds "
"buffer[%d].byteLength (%llu)",
i,
(unsigned long long)end,
bv->buffer,
(unsigned long long)declared_len);
} else if (buf->data.data != NULL && end > buf->data.count) {
TG3__VERR(TG3_ERR_INVALID_BUFFER_VIEW, path,
"bufferView[%u] byteOffset+byteLength (%llu) exceeds "
"loaded buffer[%d] size (%llu)",
i,
(unsigned long long)end,
bv->buffer,
(unsigned long long)buf->data.count);
}
}
if (bv->byte_length == 0) {
TG3__VERR(TG3_ERR_INVALID_BUFFER_VIEW, path,
"bufferView[%u].byteLength must be > 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);
uint64_t elem_sz =
tg3__val_accessor_elem_size_with_padding(
acc->component_type, acc->type);
if (comp_sz > 0 && elem_sz > 0) {
uint64_t stride = (bv->byte_stride > 0)
? (uint64_t)bv->byte_stride
: elem_sz;
uint64_t max_byte =
acc->byte_offset +
(uint64_t)(acc->count - 1) * stride +
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);
}
/* glTF also requires the effective offset into the
* underlying buffer (bufferView.byteOffset +
* accessor.byteOffset) to be component-size aligned. */
if (((bv->byte_offset + acc->byte_offset) %
(uint64_t)comp_sz) != 0) {
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
"accessor[%u] effective byte offset %llu "
"(bufferView[%d].byteOffset %llu + "
"accessor.byteOffset %llu) is not aligned "
"to componentType size %d",
i,
(unsigned long long)
(bv->byte_offset + acc->byte_offset),
acc->buffer_view,
(unsigned long long)bv->byte_offset,
(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 ((uint64_t)acc->sparse.count > acc->count) {
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
"accessor[%u].sparse.count %d exceeds accessor.count %llu",
i, acc->sparse.count,
(unsigned long long)acc->count);
}
if (acc->sparse.indices.component_type !=
TG3_COMPONENT_TYPE_UNSIGNED_BYTE &&
acc->sparse.indices.component_type !=
TG3_COMPONENT_TYPE_UNSIGNED_SHORT &&
acc->sparse.indices.component_type !=
TG3_COMPONENT_TYPE_UNSIGNED_INT) {
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
"accessor[%u].sparse.indices.componentType %d must be "
"UNSIGNED_BYTE, UNSIGNED_SHORT, or UNSIGNED_INT",
i, acc->sparse.indices.component_type);
}
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
* ------------------------------------------------------------------ */
uint32_t *node_parent_counts = NULL;
if (model->nodes_count > 0) {
node_parent_counts = (uint32_t *)calloc(model->nodes_count,
sizeof(uint32_t));
if (!node_parent_counts) {
TG3__VERR(TG3_ERR_OUT_OF_MEMORY, "/nodes",
"OOM while validating node graph");
}
}
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);
} else if (node_parent_counts) {
uint32_t child_u = (uint32_t)child_idx;
node_parent_counts[child_u] += 1;
if (node_parent_counts[child_u] > 1) {
char cpath[256];
snprintf(cpath, sizeof(cpath), "/nodes/%u", child_u);
TG3__VERR(TG3_ERR_INVALID_NODE, cpath,
"node[%u] has multiple parents", child_u);
}
}
}
}
if (node_parent_counts) {
/* Queue entries are written before they are read, so zero-init is not
* required here. */
uint32_t *queue = (uint32_t *)malloc(sizeof(uint32_t) * model->nodes_count);
if (!queue) {
TG3__VERR(TG3_ERR_OUT_OF_MEMORY, "/nodes",
"OOM while validating node graph");
} else {
uint32_t queue_head = 0;
uint32_t queue_tail = 0;
uint32_t visited = 0;
for (uint32_t i = 0; i < model->nodes_count; i++) {
if (node_parent_counts[i] == 0) {
queue[queue_tail++] = i;
}
}
while (queue_head < queue_tail) {
uint32_t node_idx = queue[queue_head++];
const tg3_node *node = &model->nodes[node_idx];
visited++;
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 ||
(uint32_t)child_idx == node_idx) {
continue;
}
if (node_parent_counts[(uint32_t)child_idx] > 0) {
node_parent_counts[(uint32_t)child_idx]--;
if (node_parent_counts[(uint32_t)child_idx] == 0) {
queue[queue_tail++] = (uint32_t)child_idx;
}
}
}
}
if (visited != model->nodes_count) {
for (uint32_t i = 0; i < model->nodes_count; i++) {
if (node_parent_counts[i] > 0) {
snprintf(path, sizeof(path), "/nodes/%u", i);
TG3__VERR(TG3_ERR_INVALID_NODE, path,
"node[%u] participates in a cycle", i);
}
}
}
free(queue);
}
free(node_parent_counts);
}
/* ------------------------------------------------------------------
* 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) {
TG3__VERR(TG3_ERR_MISSING_REQUIRED, ipath,
"animation[%u].channels[%u].target.node is required",
i, ci);
} else if ((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);
}
}
/* Texture index checks: update first_err when any out-of-range
* index is detected (the helper bypasses TG3__VERR). */
if (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")) {
if (first_err == TG3_OK) first_err = TG3_ERR_INVALID_INDEX;
}
}
/* ------------------------------------------------------------------
* 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);
}
if (cam->perspective.has_aspect_ratio &&
cam->perspective.aspect_ratio <= 0.0) {
TG3__VERR(TG3_ERR_INVALID_CAMERA, path,
"camera[%u].perspective.aspectRatio must be > 0", i);
}
if (cam->perspective.has_zfar &&
cam->perspective.zfar <= 0.0) {
TG3__VERR(TG3_ERR_INVALID_CAMERA, path,
"camera[%u].perspective.zfar 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);
}
if (img->buffer_view >= 0 &&
(img->mime_type.data == NULL || img->mime_type.len == 0)) {
TG3__VERR(TG3_ERR_INVALID_IMAGE, path,
"image[%u] uses bufferView but is missing mimeType",
i);
}
/* 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__VERR(TG3_ERR_INVALID_IMAGE, path,
"image[%u] must not have both URI and bufferView",
i);
}
}
}
#undef TG3__VERR
#undef TG3__VWARN
return first_err;
}
#endif /* TINYGLTF3_IMPLEMENTATION */
#endif /* TINY_GLTF_V3_H_ */

View File

@@ -0,0 +1,22 @@
add_executable(tinygltf3_validator
validator.cc
)
target_include_directories(tinygltf3_validator PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../..
)
if (TINYGLTF_USE_CUSTOM_JSON)
target_compile_definitions(tinygltf3_validator PRIVATE TINYGLTF_USE_CUSTOM_JSON)
endif ()
set_target_properties(tinygltf3_validator PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
OUTPUT_NAME tinygltf3-validator
)
install(TARGETS tinygltf3_validator
DESTINATION bin
)

View File

@@ -0,0 +1,95 @@
#include <cstdlib>
#include <cstring>
#include <iostream>
#define TINYGLTF3_IMPLEMENTATION
#define TINYGLTF3_ENABLE_FS
#include "tiny_gltf_v3.h"
namespace {
const char *severity_name(tg3_severity severity) {
switch (severity) {
case TG3_SEVERITY_WARNING:
return "warning";
case TG3_SEVERITY_ERROR:
return "error";
default:
return "info";
}
}
void print_errors(std::ostream &os, const tg3_error_stack *errors) {
const uint32_t count = tg3_errors_count(errors);
for (uint32_t i = 0; i < count; ++i) {
const tg3_error_entry *entry = tg3_errors_get(errors, i);
if (!entry) {
continue;
}
os << severity_name(entry->severity);
if (entry->json_path && entry->json_path[0] != '\0') {
os << " " << entry->json_path;
}
if (entry->message && entry->message[0] != '\0') {
os << ": " << entry->message;
}
os << '\n';
}
}
int usage(const char *name) {
std::cerr << "Usage: " << name << " <path/to/model.gltf|model.glb>\n";
return EXIT_FAILURE;
}
} // namespace
int main(int argc, char **argv) {
if (argc != 2) {
return usage(argv[0]);
}
const char *filename = argv[1];
tg3_model model;
tg3_error_stack errors;
tg3_parse_options options;
std::memset(&model, 0, sizeof(model));
model.default_scene = -1;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&options);
tg3_error_code rc =
tg3_parse_file(&model, &errors, filename,
static_cast<uint32_t>(std::strlen(filename)), &options);
if (tg3_errors_count(&errors) > 0) {
print_errors(std::cerr, &errors);
}
if (rc != TG3_OK) {
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return EXIT_FAILURE;
}
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
rc = tg3_validate(&model, &errors);
if (tg3_errors_count(&errors) > 0) {
print_errors(rc == TG3_OK ? std::cout : std::cerr, &errors);
}
if (rc == TG3_OK) {
std::cout << filename << ": valid glTF 2.0\n";
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return (rc == TG3_OK) ? EXIT_SUCCESS : EXIT_FAILURE;
}