mirror of
https://github.com/syoyo/tinygltf.git
synced 2026-06-08 03:03:50 +00:00
Adds a structured DIGEST block (asset, buffers w/ FNV-1a hash, bufferViews,
accessors w/ min/max, mesh primitives w/ sorted attribute maps, nodes w/
normalized TRS+matrix, materials, textures/samplers/images, skins,
animations, cameras, scenes) emitted by both loader_example (v1) and
tester_v3_c (v3 C, now accepting a file arg). test_runner.py runs both,
diffs the digests, and reports counts/digest mismatches with v1 as truth.
Also rolls in /simplify follow-ups on top of 7f736d1: a shared
tg3__json_number_to_double helper to dedupe inline number coercions, a
collapsed fuzz_gltf_v3_c harness using a single tg3_fuzz_run dispatcher,
a rewritten max_safe_uint64_real comment explaining the 53-bit mantissa
constraint, and a tests/Makefile fix so tester_v3_c is a real prerequisite
of `all` (built once via the dedicated rule, not duplicated).
Verifier passes 134/134 on the Khronos glTF-Sample-Models/2.0 suite.
bufferView.target and image.mime_type/uri are intentionally excluded from
the digest: v1 infers target from accessor usage and rewrites image
URIs/mime via stb_image, neither of which is a parse-fidelity concern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
169 lines
4.9 KiB
Python
169 lines
4.9 KiB
Python
#!/usr/bin/env python
|
|
|
|
import glob
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
## Cross-version verifier: parses each sample model with the mature v1
|
|
## (loader_example) and the new v3 C tester, then compares both the COUNTS
|
|
## summary line and the structured DIGEST block both binaries emit.
|
|
## v1 is the ground truth.
|
|
|
|
# -- config -----------------------
|
|
|
|
sample_model_dir = "/mnt/nfs/syoyo/glTF-Sample-Models"
|
|
base_model_dir = os.path.join(sample_model_dir, "2.0")
|
|
|
|
v1_bin = "./loader_example"
|
|
v3_bin = "./tests/tester_v3_c"
|
|
|
|
kinds = ["glTF", "glTF-Binary", "glTF-Embedded", "glTF-MaterialsCommon"]
|
|
# ---------------------------------
|
|
|
|
COUNTS_RE = re.compile(r"^COUNTS\s+(.*)$", re.MULTILINE)
|
|
DIGEST_RE = re.compile(r"^DIGEST_BEGIN\n(.*?)^DIGEST_END$", re.MULTILINE | re.DOTALL)
|
|
|
|
|
|
def parse_counts(output):
|
|
m = COUNTS_RE.search(output)
|
|
if not m:
|
|
return None
|
|
counts = {}
|
|
for tok in m.group(1).split():
|
|
if "=" not in tok:
|
|
continue
|
|
k, v = tok.split("=", 1)
|
|
counts[k] = int(v)
|
|
return counts
|
|
|
|
|
|
def parse_digest(output):
|
|
m = DIGEST_RE.search(output)
|
|
if not m:
|
|
return None
|
|
return [line for line in m.group(1).splitlines() if line]
|
|
|
|
|
|
def run_binary(binary, filename):
|
|
p = subprocess.Popen(
|
|
[binary, filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
)
|
|
out, err = p.communicate()
|
|
return p.returncode, out.decode("utf-8", "replace"), err.decode("utf-8", "replace")
|
|
|
|
|
|
def diff_digests(v1, v3, max_lines=20):
|
|
"""Return a short summary of differences between two digest line lists."""
|
|
diffs = []
|
|
n = max(len(v1), len(v3))
|
|
for i in range(n):
|
|
a = v1[i] if i < len(v1) else "<missing>"
|
|
b = v3[i] if i < len(v3) else "<missing>"
|
|
if a != b:
|
|
diffs.append(" v1[{0}]: {1}".format(i, a))
|
|
diffs.append(" v3[{0}]: {1}".format(i, b))
|
|
if len(diffs) >= max_lines * 2:
|
|
diffs.append(" ... (truncated)")
|
|
break
|
|
return diffs
|
|
|
|
|
|
parse_failed = [] # v3 returned non-zero or no COUNTS/DIGEST
|
|
v1_skipped = [] # v1 returned non-zero or no COUNTS/DIGEST
|
|
counts_diff = [] # counts disagree
|
|
digest_diff = [] # digest disagrees
|
|
ok = []
|
|
|
|
|
|
def verify(filename):
|
|
print("Testing: " + filename)
|
|
|
|
rc1, out1, err1 = run_binary(v1_bin, filename)
|
|
c1 = parse_counts(out1) if rc1 == 0 else None
|
|
d1 = parse_digest(out1) if rc1 == 0 else None
|
|
if c1 is None or d1 is None:
|
|
v1_skipped.append(filename)
|
|
print(" v1 ground truth unavailable (rc={0}); skipping".format(rc1))
|
|
return
|
|
|
|
rc3, out3, err3 = run_binary(v3_bin, filename)
|
|
c3 = parse_counts(out3) if rc3 == 0 else None
|
|
d3 = parse_digest(out3) if rc3 == 0 else None
|
|
if c3 is None or d3 is None:
|
|
parse_failed.append((filename, rc3, err3.strip()))
|
|
print(" v3 FAILED (rc={0}): {1}".format(rc3, err3.strip()[:200]))
|
|
return
|
|
|
|
cdiffs = []
|
|
for k in sorted(set(c1) | set(c3)):
|
|
if c1.get(k) != c3.get(k):
|
|
cdiffs.append((k, c1.get(k), c3.get(k)))
|
|
if cdiffs:
|
|
counts_diff.append((filename, cdiffs))
|
|
print(" COUNTS MISMATCH:")
|
|
for k, a, b in cdiffs:
|
|
print(" {0}: v1={1} v3={2}".format(k, a, b))
|
|
return
|
|
|
|
if d1 != d3:
|
|
diffs = diff_digests(d1, d3)
|
|
digest_diff.append((filename, diffs))
|
|
print(" DIGEST MISMATCH ({0} v1 lines, {1} v3 lines):".format(len(d1), len(d3)))
|
|
for line in diffs[:8]:
|
|
print(line)
|
|
return
|
|
|
|
ok.append(filename)
|
|
|
|
|
|
def test():
|
|
for d in sorted(os.listdir(base_model_dir)):
|
|
p = os.path.join(base_model_dir, d)
|
|
if not os.path.isdir(p):
|
|
continue
|
|
for k in kinds:
|
|
targetDir = os.path.join(p, k)
|
|
g = sorted(
|
|
glob.glob(targetDir + "/*.gltf")
|
|
+ glob.glob(targetDir + "/*.glb")
|
|
)
|
|
for gltf in g:
|
|
verify(gltf)
|
|
|
|
|
|
def main():
|
|
if not os.path.exists(v1_bin):
|
|
sys.exit("error: v1 binary not found at {0}".format(v1_bin))
|
|
if not os.path.exists(v3_bin):
|
|
sys.exit("error: v3 binary not found at {0}".format(v3_bin))
|
|
|
|
test()
|
|
|
|
print("")
|
|
print("=== Summary ===")
|
|
print("OK : {0}".format(len(ok)))
|
|
print("Counts diff : {0}".format(len(counts_diff)))
|
|
print("Digest diff : {0}".format(len(digest_diff)))
|
|
print("v3 failed : {0}".format(len(parse_failed)))
|
|
print("v1 skipped : {0}".format(len(v1_skipped)))
|
|
|
|
for f, diffs in counts_diff:
|
|
print("COUNTS DIFF: " + f)
|
|
for k, a, b in diffs:
|
|
print(" {0}: v1={1} v3={2}".format(k, a, b))
|
|
for f, diffs in digest_diff:
|
|
print("DIGEST DIFF: " + f)
|
|
for line in diffs:
|
|
print(line)
|
|
for f, rc, err in parse_failed:
|
|
print("V3 FAIL: {0} (rc={1}) {2}".format(f, rc, err[:200]))
|
|
|
|
if counts_diff or digest_diff or parse_failed:
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|