Compare commits

..

1 Commits

Author SHA1 Message Date
Syoyo Fujita
c6817d206b Remove some assert() 2023-06-07 19:10:44 +09:00
43 changed files with 794 additions and 18232 deletions

View File

@@ -1,45 +0,0 @@
## Description
What does this PR do? Provide a brief summary of the changes.
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Refactoring (no functional changes)
- [ ] Documentation update
- [ ] Other (please describe):
## Checklist
### Required for All PRs
- [ ] Reproducible glTF test file(s) are included (e.g., `models/regression/`, `tests/issue-***.gltf`, etc.)
- [ ] Unit tests are written and pass locally (`cd tests && ./tester`)
### Required for Feature PRs
- [ ] Specification document is included (e.g., `docs/spec/<feature-name>.md`)
- The spec should cover: purpose, API design, usage examples, and edge cases
### Security Policy
This project manages CVE assignments exclusively through GitHub Security Advisories.
PRs that include or reference independently obtained CVE IDs or external vulnerability disclosures will be closed.
## Test Instructions
How can reviewers verify your changes?
```
# Example:
cd build && cmake .. && make test
```
## Related Issues
Link related issues:
## Additional Notes
Any other context, screenshots, or information relevant to the review.

View File

@@ -1,92 +0,0 @@
# Copilot Review Instructions for TinyGLTF
This document provides guidelines for reviewing code changes in the TinyGLTF repository.
## Memory Safety
- **Buffer Overflows**: Check for proper bounds checking when accessing arrays, vectors, and buffers. Verify that buffer sizes are validated before read/write operations.
- **Null Pointer Dereferences**: Ensure all pointers are checked for null before dereferencing, especially when handling optional glTF fields.
- **Memory Leaks**: Verify proper resource management, including RAII patterns for file handles, image data, and dynamically allocated memory.
- **Use-After-Free**: Check for proper lifetime management of objects, especially when dealing with callbacks and asynchronous operations.
## Error Handling
- **File I/O**: Verify that all file operations have proper error checking and meaningful error messages.
- **JSON Parsing**: Ensure JSON parsing errors are caught and reported with helpful context about the location and nature of the error.
- **Resource Loading**: Check that failures in loading images, buffers, and other resources are properly handled and don't cause crashes.
- **Error Propagation**: Verify that errors are properly propagated through the call stack with appropriate error messages.
## glTF 2.0 Specification Compliance
- **Required Fields**: Ensure all required glTF fields are validated and present.
- **Data Types**: Verify that data types match the glTF specification (e.g., component types, accessor types).
- **Constraints**: Check that glTF constraints are enforced (e.g., valid ranges for enums, buffer stride requirements).
- **Extensions**: Verify proper handling of glTF extensions and that unknown extensions are handled gracefully.
- **Validation**: Ensure new features align with the glTF 2.0 specification from the Khronos Group.
## Cross-Platform Compatibility
- **Windows**: Check for proper handling of Windows-specific issues (path separators, line endings, file operations).
- **Linux**: Verify compatibility with various Linux distributions and compilers (GCC, Clang).
- **macOS**: Ensure macOS-specific considerations are addressed (case-sensitive filesystems, Clang compatibility).
- **Mobile Platforms**: Consider Android and iOS compatibility where applicable.
- **Endianness**: Verify proper handling of byte order when reading binary data.
- **Compiler Compatibility**: Ensure code compiles with C++11 standard and supported compilers (MSVC, GCC, Clang).
## Edge Cases in glTF Parsing
- **Empty/Minimal Files**: Verify handling of minimal valid glTF files.
- **Large Files**: Check for proper handling of large glTF files and buffers without memory exhaustion.
- **Malformed Data**: Ensure graceful handling of malformed or invalid glTF data.
- **Missing Optional Fields**: Verify correct behavior when optional glTF fields are absent.
- **Edge Values**: Check handling of boundary values (e.g., maximum buffer sizes, extreme floating-point values).
- **Base64 Encoding**: Verify proper handling of base64-encoded data URIs and invalid encodings.
## Backwards Compatibility
- **API Changes**: Ensure public API changes maintain backwards compatibility or are properly deprecated.
- **Breaking Changes**: Flag any breaking changes for major version updates and document migration paths.
- **Binary Compatibility**: Consider ABI stability for header-only library changes.
- **Default Behavior**: Verify that default behavior of existing functionality remains unchanged.
## Performance Considerations
- **Parsing Performance**: Check for unnecessary copies, redundant allocations, and inefficient algorithms in parsing logic.
- **Memory Usage**: Verify efficient memory usage, especially when loading large glTF files.
- **I/O Operations**: Ensure efficient file reading and minimize unnecessary disk access.
- **String Operations**: Check for efficient string handling (use of string_view, move semantics).
- **STL Usage**: Verify appropriate use of STL containers and algorithms.
## Documentation
- **Public API**: Ensure all public functions, classes, and methods have clear documentation comments.
- **Parameters**: Verify that function parameters are documented, including expected ranges and constraints.
- **Return Values**: Document return values and possible error conditions.
- **Examples**: Check that complex features include usage examples.
- **Changelog**: Verify that significant changes are documented in release notes or changelog.
## Testing
- **Test Coverage**: Ensure new features include appropriate unit tests or integration tests.
- **Edge Cases**: Verify that tests cover edge cases and error conditions.
- **Cross-Platform Tests**: Check that tests run on all supported platforms.
- **Regression Tests**: Ensure bug fixes include regression tests to prevent recurrence.
- **Sample Files**: Verify that changes are tested with various valid and invalid glTF sample files.
## Code Style Consistency
- **Header-Only Pattern**: Maintain the header-only library structure.
- **Naming Conventions**: Follow existing naming conventions (CamelCase for types, snake_case for functions where applicable).
- **Formatting**: Adhere to the existing code formatting style (check `.clang-format` if available).
- **Include Guards**: Verify proper include guards and header organization.
- **Namespace Usage**: Ensure proper use of the `tinygltf` namespace.
- **Comments**: Maintain consistent comment style with existing code.
- **C++11 Compliance**: Verify that code uses C++11 features appropriately and doesn't require newer standards unless specified.
## Additional Considerations
- **Third-Party Dependencies**: Minimize new dependencies; prefer existing dependencies (json.hpp, stb_image).
- **Warnings**: Ensure code compiles without warnings on supported compilers.
- **const Correctness**: Verify proper use of const for parameters and methods.
- **RAII**: Prefer RAII patterns for resource management over manual cleanup.
- **noexcept**: Use noexcept appropriately for move constructors and move assignment operators.

View File

@@ -14,7 +14,7 @@ jobs:
# steps:
# - name: Checkout
# uses: actions/checkout@v5
# uses: actions/checkout@v1
# - name: Build
# run: |
@@ -40,7 +40,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v2
- name: Build
run: |
@@ -60,7 +60,7 @@ jobs:
# https://help.github.com/en/actions/reference/software-installed-on-github-hosted-runners
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v1
- name: Configure
run: |
mkdir build
@@ -78,7 +78,7 @@ jobs:
name: Buld with gcc
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
- name: build
run: |
g++ -std=c++11 -o loader_example loader_example.cc
@@ -100,17 +100,6 @@ jobs:
./tester_noexcept
cd ..
- name: v3_c_tests
run: |
cd tests
cc -I../ -std=c11 -g -O0 -DTINYGLTF3_ENABLE_FS \
-o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c
./tester_v3_c
cc -I../ -std=c11 -g -O0 -DTINYGLTF3_ENABLE_FS \
-o tester_v3_c_v1port tester_v3_c_v1port.c ../tiny_gltf_v3.c
./tester_v3_c_v1port
cd ..
build-rapidjson-linux:
@@ -118,7 +107,7 @@ jobs:
name: Buld with gcc + rapidjson
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
- name: build
run: |
git clone https://github.com/Tencent/rapidjson
@@ -151,7 +140,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v2
- name: Build
run: |
sudo apt-get update
@@ -169,7 +158,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v1
- name: Build
run: |
clang++ -std=c++11 -g -O0 -o loader_example loader_example.cc

View File

@@ -1,513 +0,0 @@
name: Comprehensive CI
on:
push:
branches:
- master
- release
- devel
pull_request:
branches:
- master
- release
- devel
workflow_dispatch:
permissions:
contents: read
jobs:
# Linux x64 - GCC
linux-gcc-x64:
runs-on: ubuntu-latest
name: Linux x64 (GCC)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Configure
run: cmake -B build -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON
- name: Build
run: cmake --build build
- name: Run loader_example
run: ./build/loader_example models/Cube/Cube.gltf
- name: Run tests
run: ctest --test-dir build --output-on-failure
# Linux x64 - Clang 21
linux-clang-x64:
runs-on: ubuntu-24.04
name: Linux x64 (Clang 21)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install Clang 21
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 21
- name: Configure
run: |
cmake -B build -DCMAKE_C_COMPILER=clang-21 -DCMAKE_CXX_COMPILER=clang++-21 -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON
- name: Build
run: cmake --build build
- name: Run loader_example
run: |
./build/loader_example models/Cube/Cube.gltf
- name: Run tests
run: ctest --test-dir build --output-on-failure
# Linux ARM64 - GCC (native)
linux-arm64:
runs-on: ubuntu-24.04-arm
name: Linux ARM64 (GCC)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Configure
run: cmake -B build -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON
- name: Build
run: cmake --build build
- name: Run loader_example
run: ./build/loader_example models/Cube/Cube.gltf
- name: Run tests
run: ctest --test-dir build --output-on-failure
# macOS ARM64 Apple Silicon
macos-arm64:
runs-on: macos-14
name: macOS ARM64 Apple Silicon (Clang)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Configure
run: cmake -B build -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON
- name: Build
run: cmake --build build
- name: Run loader_example
run: ./build/loader_example models/Cube/Cube.gltf
- name: Run tests
run: ctest --test-dir build --output-on-failure
# Windows x64 - MSVC
windows-msvc-x64:
runs-on: windows-latest
name: Windows x64 (MSVC)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Configure
run: |
mkdir build
cd build
cmake -G "Visual Studio 17 2022" -A x64 -DTINYGLTF_BUILD_LOADER_EXAMPLE=On -DTINYGLTF_BUILD_GL_EXAMPLES=Off -DTINYGLTF_BUILD_VALIDATOR_EXAMPLE=Off ..
- name: Build
run: cmake --build build --config Release
- name: Run loader_example
run: |
.\build\Release\loader_example.exe models\Cube\Cube.gltf
- name: Run tests
run: ctest --test-dir build -C Release --output-on-failure
# Windows x86 - MSVC
windows-msvc-x86:
runs-on: windows-latest
name: Windows x86 (MSVC)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Configure
run: |
mkdir build
cd build
cmake -G "Visual Studio 17 2022" -A Win32 -DTINYGLTF_BUILD_LOADER_EXAMPLE=On -DTINYGLTF_BUILD_GL_EXAMPLES=Off -DTINYGLTF_BUILD_VALIDATOR_EXAMPLE=Off ..
- name: Build
run: cmake --build build --config Release
- name: Run tests
run: ctest --test-dir build -C Release --output-on-failure
# Windows ARM64 - MSVC (cross-compile)
windows-msvc-arm64:
runs-on: windows-latest
name: Windows ARM64 (MSVC) - Cross-compile
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Configure
run: |
mkdir build
cd build
cmake -G "Visual Studio 17 2022" -A ARM64 -DTINYGLTF_BUILD_LOADER_EXAMPLE=On -DTINYGLTF_BUILD_GL_EXAMPLES=Off -DTINYGLTF_BUILD_VALIDATOR_EXAMPLE=Off ..
- name: Build
run: cmake --build build --config Release
# Windows MinGW - MSYS2
windows-mingw-msys2:
runs-on: windows-latest
name: Windows x64 (MinGW MSYS2)
defaults:
run:
shell: msys2 {0}
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
install: base-devel
pacboy: >-
cc:p cmake:p ninja:p
update: true
release: false
- name: Build with CMake
run: |
cmake -G"Ninja" -S . -B build
cmake --build build
- name: Run loader_example
run: |
./build/loader_example models/Cube/Cube.gltf
- name: Run tests
run: ctest --test-dir build --output-on-failure
# Linux -> Windows MinGW Cross-compile
linux-mingw-cross:
runs-on: ubuntu-latest
name: Linux→Windows (MinGW Cross) - Build Only
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install MinGW
run: |
sudo apt-get update
sudo apt-get install -y build-essential mingw-w64
- name: Build
run: |
x86_64-w64-mingw32-g++ -std=c++11 -o loader_example.exe loader_example.cc
# Special Configuration: No Exceptions
linux-noexception:
runs-on: ubuntu-latest
name: Linux x64 (GCC) - No Exceptions
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Build loader_example
run: |
g++ -DTINYGLTF_NOEXCEPTION -std=c++11 -o loader_example loader_example.cc
- name: Run loader_example
run: |
./loader_example models/Cube/Cube.gltf
- name: Build and run unit tests
run: |
cd tests
g++ -DTINYGLTF_NOEXCEPTION -I../ -std=c++11 -g -O0 -o tester_noexcept tester.cc
./tester_noexcept
# Special Configuration: Header-Only Mode
linux-header-only:
runs-on: ubuntu-latest
name: Linux x64 (GCC) - Header-Only Mode
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Build with CMake Header-Only
run: |
mkdir build
cmake -B build -DTINYGLTF_HEADER_ONLY=ON -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON
cmake --build build
- name: Run loader_example
run: |
./build/loader_example models/Cube/Cube.gltf
- name: Run tests
run: ctest --test-dir build --output-on-failure
# Special Configuration: RapidJSON Backend
linux-rapidjson:
runs-on: ubuntu-latest
name: Linux x64 (GCC) - RapidJSON Backend
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Clone RapidJSON
run: |
git clone https://github.com/Tencent/rapidjson
- name: Configure
run: |
cmake -B build -DTINYGLTF_USE_RAPIDJSON=ON -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON -DCMAKE_PREFIX_PATH=$PWD/rapidjson
- name: Build
run: cmake --build build
- name: Run loader_example
run: |
./build/loader_example models/Cube/Cube.gltf
- name: Run tests
run: ctest --test-dir build --output-on-failure
# Special Configuration: AddressSanitizer
linux-asan:
runs-on: ubuntu-latest
name: Linux x64 (Clang) - AddressSanitizer
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Build loader_example with ASan
run: |
clang++ -fsanitize=address -std=c++11 -g -O1 -o loader_example loader_example.cc
- name: Run loader_example
run: |
./loader_example models/Cube/Cube.gltf
- name: Build and run unit tests with ASan
run: |
cd tests
clang++ -fsanitize=address -I../ -std=c++11 -g -O1 -o tester tester.cc
./tester
# Special Configuration: UndefinedBehaviorSanitizer
linux-ubsan:
runs-on: ubuntu-latest
name: Linux x64 (Clang) - UndefinedBehaviorSanitizer
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Build loader_example with UBSan
run: |
clang++ -fsanitize=undefined -std=c++11 -g -O1 -o loader_example loader_example.cc
- name: Run loader_example
run: |
./loader_example models/Cube/Cube.gltf
- name: Build and run unit tests with UBSan
run: |
cd tests
clang++ -fsanitize=undefined -I../ -std=c++11 -g -O1 -o tester tester.cc
./tester
# v3 C runtime: internal security regression tests + ported v1 unit tests.
v3-c-tests:
runs-on: ubuntu-latest
name: v3 C tests (Linux x64, GCC)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Build tester_v3_c
run: |
cd tests
cc -I../ -std=c11 -g -O0 -Wall -Wextra -Wpedantic -Werror -DTINYGLTF3_ENABLE_FS \
-o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c
- name: Build tester_v3_c_v1port
run: |
cd tests
cc -I../ -std=c11 -g -O0 -Wall -Wextra -Wpedantic -Werror -DTINYGLTF3_ENABLE_FS \
-o tester_v3_c_v1port tester_v3_c_v1port.c ../tiny_gltf_v3.c
- name: Run tester_v3_c (security regressions)
run: |
cd tests
./tester_v3_c
- name: Run tester_v3_c_v1port (18 ported unit tests)
run: |
cd tests
./tester_v3_c_v1port
# v3 C runtime under stock Ubuntu clang.
v3-c-tests-clang:
runs-on: ubuntu-latest
name: v3 C tests (Linux x64, Clang)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install clang
run: |
sudo apt-get update
sudo apt-get install -y clang
- name: Build tester_v3_c
run: |
cd tests
clang -I../ -std=c11 -g -O0 -Werror -Weverything \
-Wno-padded -Wno-unsafe-buffer-usage -Wno-switch-default \
-Wno-format-nonliteral -Wno-float-equal -Wno-cast-align \
-Wno-declaration-after-statement -Wno-unknown-warning-option \
-Wno-pre-c11-compat \
-DTINYGLTF3_ENABLE_FS \
-o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c
- name: Build tester_v3_c_v1port
run: |
cd tests
clang -I../ -std=c11 -g -O0 -Werror -Weverything \
-Wno-padded -Wno-unsafe-buffer-usage -Wno-switch-default \
-Wno-format-nonliteral -Wno-float-equal -Wno-cast-align \
-Wno-declaration-after-statement -Wno-unknown-warning-option \
-Wno-pre-c11-compat \
-DTINYGLTF3_ENABLE_FS \
-o tester_v3_c_v1port tester_v3_c_v1port.c ../tiny_gltf_v3.c
- name: Run tester_v3_c
run: |
cd tests
./tester_v3_c
- name: Run tester_v3_c_v1port
run: |
cd tests
./tester_v3_c_v1port
# v3 C runtime under bleeding-edge clang 21 (matches local dev environment).
v3-c-tests-clang21:
runs-on: ubuntu-24.04
name: v3 C tests (Linux x64, Clang 21)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install clang 21
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 21
- name: Build tester_v3_c
run: |
cd tests
clang-21 -I../ -std=c11 -g -O0 -Werror -Weverything \
-Wno-padded -Wno-unsafe-buffer-usage -Wno-switch-default \
-Wno-format-nonliteral -Wno-float-equal -Wno-cast-align \
-Wno-declaration-after-statement -Wno-unknown-warning-option \
-Wno-pre-c11-compat \
-DTINYGLTF3_ENABLE_FS \
-o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c
- name: Build tester_v3_c_v1port
run: |
cd tests
clang-21 -I../ -std=c11 -g -O0 -Werror -Weverything \
-Wno-padded -Wno-unsafe-buffer-usage -Wno-switch-default \
-Wno-format-nonliteral -Wno-float-equal -Wno-cast-align \
-Wno-declaration-after-statement -Wno-unknown-warning-option \
-Wno-pre-c11-compat \
-DTINYGLTF3_ENABLE_FS \
-o tester_v3_c_v1port tester_v3_c_v1port.c ../tiny_gltf_v3.c
- name: Run tester_v3_c
run: |
cd tests
./tester_v3_c
- name: Run tester_v3_c_v1port
run: |
cd tests
./tester_v3_c_v1port
# v3 C runtime built with MSVC at warning-level 4 (/W4 /WX).
v3-c-tests-msvc:
runs-on: windows-latest
name: v3 C tests (Windows x64, MSVC /W4)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Build and run tester_v3_c
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
cd tests
cl /nologo /W4 /WX /std:c11 /Zi /Od /D_CRT_SECURE_NO_WARNINGS /DTINYGLTF3_ENABLE_FS /I.. tester_v3_c.c ..\tiny_gltf_v3.c /Fe:tester_v3_c.exe
tester_v3_c.exe
- name: Build and run tester_v3_c_v1port
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
cd tests
cl /nologo /W4 /WX /std:c11 /Zi /Od /D_CRT_SECURE_NO_WARNINGS /DTINYGLTF3_ENABLE_FS /I.. tester_v3_c_v1port.c ..\tiny_gltf_v3.c /Fe:tester_v3_c_v1port.exe
tester_v3_c_v1port.exe
# v3 C runtime under ASan + UBSan for memory-safety + UB checks.
v3-c-tests-sanitizers:
runs-on: ubuntu-latest
name: v3 C tests (Clang ASan + UBSan)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Build tester_v3_c with ASan + UBSan
run: |
cd tests
clang -I../ -std=c11 -g -O1 -DTINYGLTF3_ENABLE_FS \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c
- name: Build tester_v3_c_v1port with ASan + UBSan
run: |
cd tests
clang -I../ -std=c11 -g -O1 -DTINYGLTF3_ENABLE_FS \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-o tester_v3_c_v1port tester_v3_c_v1port.c ../tiny_gltf_v3.c
- name: Run tester_v3_c
env:
ASAN_OPTIONS: detect_leaks=1:halt_on_error=1
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
run: |
cd tests
./tester_v3_c
- name: Run tester_v3_c_v1port
env:
ASAN_OPTIONS: detect_leaks=1:halt_on_error=1
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
run: |
cd tests
./tester_v3_c_v1port

View File

@@ -38,11 +38,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -69,4 +69,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v2

View File

@@ -1,45 +0,0 @@
name: MSYS2 MinGW-w64 Windows 64bit Build
on:
push:
branches:
- release
- devel
paths:
- 'tiny_gltf.*'
- 'CMakeLists.txt'
- '.github/workflows/mingw-w64-msys2.yml'
pull_request:
workflow_dispatch:
jobs:
mingw-w64-msys2-build:
name: MSYS2 MinGW-w64 Windows Build
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v5
- name: Install core & build dependencies
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
install: base-devel
pacboy: >-
cc:p cmake:p ninja:p
update: true
release: false
- name: Configure
run: |
cmake \
-G"Ninja" \
-S . \
-B build
- name: Build
run: |
cmake --build build

19
.gitignore vendored
View File

@@ -1,5 +1,4 @@
# CMake
/build/
CMakeCache.txt
CMakeFiles
CMakeScripts
@@ -22,13 +21,9 @@ premake5.tar.gz
*.vcxproj*
.vs
# default cmake build dir
build/
#binary directories
bin/
obj/
out/
#runtime gui config
imgui.ini
@@ -72,22 +67,8 @@ imgui.ini
loader_example
tests/tester
tests/tester_noexcept
tests/tester_intensive_customjson
tests/issue-97.gltf
tests/issue-261.gltf
tests/issue-495-external.gltf
# Test-generated output files (written by tester.cc during test run)
tests/Cube.gltf
tests/Cube.bin
tests/Cube.glb
tests/Cube_BaseColor.png
tests/Cube_MetallicRoughness.png
tests/Cube_with_embedded_images.gltf
tests/Cube_with_image_files.gltf
tests/tmp.glb
tests/ issue-236.gltf
tests/ issue-236.bin
tests/ 2x2 image has multiple spaces.png
# unignore
!Makefile

10
.travis-before-install.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
if [[ "$TRAVIS_OS_NAME" == "osx" ]]
then
brew upgrade
curl -o premake5.tar.gz https://github.com/premake/premake-core/releases/download/v5.0.0-alpha12/premake-5.0.0-alpha12-macosx.tar.gz
else
wget https://github.com/premake/premake-core/releases/download/v5.0.0-alpha12/premake-5.0.0-alpha12-linux.tar.gz -O premake5.tar.gz
fi
tar xzf premake5.tar.gz

63
.travis.yml Normal file
View File

@@ -0,0 +1,63 @@
language: cpp
sudo: false
matrix:
include:
- addons: &1
apt:
sources:
- george-edison55-precise-backports
- ubuntu-toolchain-r-test
- llvm-toolchain-trusty-3.9
packages:
- g++-4.9
- clang-3.9
compiler: clang
env: COMPILER_VERSION=3.9 BUILD_TYPE=Debug
- addons: *1
compiler: clang
env: COMPILER_VERSION=3.9 BUILD_TYPE=Release
- addons: &2
apt:
sources:
- george-edison55-precise-backports
- ubuntu-toolchain-r-test
packages:
- g++-4.9
compiler: gcc
env: COMPILER_VERSION=4.9 BUILD_TYPE=Debug EXTRA_CXXFLAGS="-fsanitize=address"
- addons: *2
compiler: gcc
env: COMPILER_VERSION=4.9 BUILD_TYPE=Release EXTRA_CXXFLAGS="-fsanitize=address"
- addons: *1
compiler: clang
env: COMPILER_VERSION=3.9 BUILD_TYPE=Debug CFLAGS="-O0" CXXFLAGS="-O0"
- addons: &3
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
compiler: gcc
env: COMPILER_VERSION=4.8 BUILD_TYPE=Debug
- addons: *3
compiler: gcc
env: COMPILER_VERSION=4.8 BUILD_TYPE=Release
before_install:
- ./.travis-before-install.sh
script:
- export CC="${CC}-${COMPILER_VERSION}"
- export CXX="${CXX}-${COMPILER_VERSION}"
- ${CC} -v
- ${CXX} ${EXTRA_CXXFLAGS} -std=c++11 -Wall -g -o loader_example loader_example.cc
- ./loader_example ./models/Cube/Cube.gltf
- cd tests
- clang++ -v
- make
- ./tester
- ./tester_noexcept
- cd ../examples/raytrace
- ../../premake5 gmake
- make

View File

@@ -1,87 +1,38 @@
cmake_minimum_required(VERSION 3.6)
project(tinygltf)
PROJECT (tinygltf)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED On)
set(CMAKE_CXX_EXTENSIONS Off)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
SET(CMAKE_CXX_STANDARD 11)
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_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)
option(TINYGLTF_INSTALL "Install tinygltf files during install step. Usually set to OFF if you include tinygltf through add_subdirectory()" ON)
option(TINYGLTF_INSTALL_VENDOR "Install vendored nlohmann/json and nothings/stb headers" ON)
option(TINYGLTF_USE_CUSTOM_JSON "Use the built-in fast JSON parser (tinygltf_json.h) instead of nlohmann/json" OFF)
if (TINYGLTF_BUILD_LOADER_EXAMPLE)
add_executable(loader_example
ADD_EXECUTABLE ( loader_example
loader_example.cc
)
endif (TINYGLTF_BUILD_LOADER_EXAMPLE)
if (TINYGLTF_BUILD_GL_EXAMPLES)
add_subdirectory( examples/gltfutil )
add_subdirectory( examples/glview )
ADD_SUBDIRECTORY ( examples/gltfutil )
ADD_SUBDIRECTORY ( examples/glview )
endif (TINYGLTF_BUILD_GL_EXAMPLES)
if (TINYGLTF_BUILD_VALIDATOR_EXAMPLE)
add_subdirectory( examples/validator )
ADD_SUBDIRECTORY ( examples/validator )
endif (TINYGLTF_BUILD_VALIDATOR_EXAMPLE)
if (TINYGLTF_BUILD_BUILDER_EXAMPLE)
add_subdirectory ( examples/build-gltf )
ADD_SUBDIRECTORY ( examples/build-gltf )
endif (TINYGLTF_BUILD_BUILDER_EXAMPLE)
if (TINYGLTF_BUILD_TESTS)
enable_testing()
add_executable(tester tests/tester.cc)
target_include_directories(tester PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
)
add_test(NAME tester COMMAND tester WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests)
# Build and run tests with the custom JSON backend enabled to catch regressions
add_executable(tester_customjson tests/tester.cc)
target_include_directories(tester_customjson PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
)
target_compile_definitions(tester_customjson PRIVATE TINYGLTF_USE_CUSTOM_JSON)
add_test(NAME tester_customjson COMMAND tester_customjson WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests)
# Intensive parser tests for the custom JSON backend
add_executable(tester_intensive_customjson tests/tester_intensive_customjson.cc)
target_include_directories(tester_intensive_customjson PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/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)
add_executable(tester_v3_c
tests/tester_v3_c.c
tiny_gltf_v3.c
)
target_include_directories(tester_v3_c PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
)
set_target_properties(tester_v3_c PROPERTIES
C_STANDARD 11
C_STANDARD_REQUIRED ON
C_EXTENSIONS OFF
)
add_test(NAME tester_v3_c COMMAND tester_v3_c WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests)
endif (TINYGLTF_BUILD_TESTS)
#
# for add_subdirectory and standalone build
#
@@ -105,40 +56,21 @@ else (TINYGLTF_HEADER_ONLY)
)
endif (TINYGLTF_HEADER_ONLY)
if (TINYGLTF_USE_CUSTOM_JSON)
if (TINYGLTF_HEADER_ONLY)
target_compile_definitions(tinygltf INTERFACE TINYGLTF_USE_CUSTOM_JSON)
else ()
target_compile_definitions(tinygltf PUBLIC TINYGLTF_USE_CUSTOM_JSON)
endif ()
endif ()
if (TINYGLTF_INSTALL)
install(TARGETS tinygltf EXPORT tinygltfTargets)
install(EXPORT tinygltfTargets NAMESPACE tinygltf:: FILE TinyGLTFTargets.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/tinygltf)
install(EXPORT tinygltfTargets NAMESPACE tinygltf:: FILE TinyGLTFTargets.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake)
configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/TinyGLTFConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/TinyGLTFConfig.cmake INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/TinyGLTFConfig.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/tinygltf)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/TinyGLTFConfig.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake)
# Do not install .lib even if !TINYGLTF_HEADER_ONLY
INSTALL ( FILES
json.hpp
stb_image.h
stb_image_write.h
tiny_gltf.h
tiny_gltf_v3.h
tiny_gltf_v3.c
tinygltf_json.h
tinygltf_json_c.h
${TINYGLTF_EXTRA_SOUECES}
DESTINATION
include
)
if(TINYGLTF_INSTALL_VENDOR)
INSTALL ( FILES
json.hpp
stb_image.h
stb_image_write.h
DESTINATION
include
)
endif()
endif(TINYGLTF_INSTALL)

121
README.md
View File

@@ -1,92 +1,15 @@
# Header only C++ tiny glTF library(loader/saver).
`TinyGLTF` is a header only C++ glTF 2.0 https://github.com/KhronosGroup/glTF library.
`TinyGLTF` is a header only C++11 glTF 2.0 https://github.com/KhronosGroup/glTF library.
## TinyGLTF v3 (new major release)
**`tiny_gltf_v3.h`** is the new major version of TinyGLTF.
The new C implementation (`tiny_gltf_v3.c` + `tinygltf_json_c.h`) is currently **experimental**.
### What's new in v3
v3 is a ground-up rewrite with a C-centric, low-overhead design:
- **Pure C POD structs** — no STL containers in the public API; easy to bind to other languages.
- **Arena-based memory management** — all parse-time allocations come from a single arena; a single `tg3_model_free()` frees everything.
- **Structured error reporting** — `tg3_error_stack` provides machine-readable errors with severity levels and source locations.
- **Custom JSON backend** — backed by `tinygltf_json_c.h`, a locale-independent pure-C JSON parser/serializer used by the v3 runtime.
- **Streaming callbacks** — opt-in streaming parse/write via user-supplied callbacks.
- **No RTTI, no exceptions required** — suitable for embedded and game-engine use.
- **Opt-in filesystem and image I/O** — `TINYGLTF3_ENABLE_FS` / `TINYGLTF3_ENABLE_STB_IMAGE` are off by default; you control when and how assets are loaded.
- **C++20 coroutine facade** (optional, auto-detected). C17/C++17 default.
- **Hardened against untrusted input** — URI sanitization, post-parse index-bounds validation (default-on, opt-out via `tg3_parse_options.validate_indices = 0`), strict numeric range checks; exercised by a libFuzzer harness and by a cross-version verifier that compares parsed output against the v1 C++ reference loader. See *Security model* below and the `Security Considerations` block at the top of `tiny_gltf_v3.h`.
### Quick start (v3)
Copy `tiny_gltf_v3.h`, `tiny_gltf_v3.c`, and `tinygltf_json_c.h` to your project.
Compile `tiny_gltf_v3.c` as C11 or newer. Define `TINYGLTF3_ENABLE_FS` when
building `tiny_gltf_v3.c` if you want `tg3_parse_file()` to use stdio-backed
filesystem helpers. The legacy `TINYGLTF3_IMPLEMENTATION` include path remains
available for compatibility.
```c
#include "tiny_gltf_v3.h"
```
Loading a glTF file:
```c
tg3_parse_options opts;
tg3_error_stack errors;
tg3_model model;
tg3_parse_options_init(&opts);
tg3_error_stack_init(&errors);
tg3_error_code err = tg3_parse_file(&model, &errors, "scene.gltf", 10, &opts);
if (err != TG3_OK) {
for (uint32_t i = 0; i < errors.count; i++) {
fprintf(stderr, "[%d] %s\n", (int)errors.entries[i].severity,
errors.entries[i].message ? errors.entries[i].message : "(null)");
}
}
// ... use model ...
tg3_model_free(&model);
tg3_error_stack_free(&errors);
```
### Security model (v3 C runtime)
The v3 C runtime is built for processing **untrusted glTF/GLB input** (server-side asset pipelines, user uploads, etc.) and ships hardened by default:
- **URI sanitization** — external buffer/image URIs are rejected before any filesystem call if they are empty, contain NUL bytes, begin with `/` or `\`, look like a Windows drive prefix (`X:`), or contain a `..` segment. Production callers SHOULD still provide a custom `tg3_fs_callbacks.read_file` that confines reads to a known directory (e.g. via `openat` plus a `realpath` prefix check) when the input is attacker-controlled.
- **Index bounds validation** — every `int32_t` index field populated from JSON (accessor.bufferView, primitive.indices/material/attributes, scene.nodes[], skin.joints[], animation channel/sampler refs, KHR_audio + MSFT_lod refs, …) is checked after the structural parse. Out-of-range indices produce `TG3_ERR_INVALID_INDEX`. Default `tg3_parse_options.validate_indices = 1`; set to `0` only when you need raw round-trip and have your own validator.
- **Strict numeric range checks** — JSON numbers feeding integer fields go through finite/round-trip-validated coercion (`tg3__json_number_to_int32` / `_uint64`). `byteStride` is restricted to 0 or [4, 252].
- **Memory budget** — the arena is capped at `TINYGLTF3_MAX_MEMORY_BYTES` (1 GB by default; configurable via `tg3_memory_config`).
- **Image decoding off by default** — the parser does not decode image bytes; use `tg3_parse_options.images_as_is = 1` to skip any decoder entirely when handling untrusted input.
- **Error message lifetime** — error strings on `tg3_error_stack` are arena-allocated and remain valid until `tg3_model_free()`. Read or copy them BEFORE freeing the model.
See the `Security Considerations` block at the top of `tiny_gltf_v3.h` for the authoritative threat-model summary.
### Testing & verification
The v3 C runtime ships with three layers of automated coverage:
- **`tests/tester_v3_c.c`** — internal unit checks plus security regression tests (path traversal, negative `byteStride`, OOB indices, error-message lifetime, …). Build via `make` in `tests/`; run `./tester_v3_c` for the internal suite or `./tester_v3_c <file.gltf|file.glb>` to parse a single asset.
- **`test_runner.py`** — a cross-version verifier that runs the v1 C++ reference loader (`loader_example`) and the v3 C tester against every model in `glTF-Sample-Models/2.0`, then diffs a structured DIGEST block (buffer FNV64 hashes, accessor/bufferView fields, primitive attribute maps, node TRS, material PBR factors, skin/animation/scene topology, …). v1 is the ground truth.
- **`tests/v3/fuzzer/`** — libFuzzer harness with ASan + UBSan (`make run` builds and runs `fuzz_gltf_v3_c`). Crafted regression inputs live in `tests/v3/security/` and are seeded into `tests/v3/fuzzer/corpus/`.
`TinyGLTF` uses Niels Lohmann's json library (https://github.com/nlohmann/json), so now it requires C++11 compiler.
(Also, you can use RadpidJSON as an JSON backend)
If you are looking for old, C++03 version, please use `devel-picojson` branch (but not maintained anymore).
## Status
> ⚠️ **v2 deprecation notice:** `tiny_gltf.h` (v2) remains fully functional and is still supported,
> but it is now in **maintenance mode only** — no new features will be added.
> v2 will be **sunset after mid-2026**. `tiny_gltf_v3.h` is the intended successor, but the new C v3 runtime is still **experimental**.
Currently TinyGLTF is stable and maintenance mode. No drastic changes and feature additions planned.
TinyGLTF v3's C runtime (`tiny_gltf_v3.h` + `tiny_gltf_v3.c`) is available for evaluation and early adoption,
but its API/behavior may still change while the implementation matures.
Currently TinyGLTF v2 is stable and in maintenance mode. No drastic changes and feature additions planned.
- v2.9.0 Various fixes and improvements. Filesystem callback API change.
- v2.8.0 Add URICallbacks for custom URI handling in Buffer and Image. PR#397
- v2.7.0 Change WriteImageDataFunction user callback function signature. PR#393
- v2.6.0 Support serializing sparse accessor(Thanks to @fynv).
@@ -103,6 +26,10 @@ Currently TinyGLTF v2 is stable and in maintenance mode. No drastic changes and
## Builds
[![Build Status](https://travis-ci.org/syoyo/tinygltf.svg?branch=devel)](https://travis-ci.org/syoyo/tinygltf)
[![Build status](https://ci.appveyor.com/api/projects/status/warngenu9wjjhlm8?svg=true)](https://ci.appveyor.com/project/syoyo/tinygltf)
![C/C++ CI](https://github.com/syoyo/tinygltf/workflows/C/C++%20CI/badge.svg)
## Features
@@ -167,6 +94,22 @@ Users who want to run TinyGLTF securely and safely(e.g. need to handle malcious
I recommend to build TinyGLTF for WASM target.
WASI build example is located in [wasm](wasm) .
## Projects using TinyGLTF
* px_render Single header C++ Libraries for Thread Scheduling, Rendering, and so on... https://github.com/pplux/px
* Physical based rendering with Vulkan using glTF 2.0 models https://github.com/SaschaWillems/Vulkan-glTF-PBR
* GLTF loader plugin for OGRE 2.1. Support for PBR materials via HLMS/PBS https://github.com/Ybalrid/Ogre_glTF
* [TinyGltfImporter](http://doc.magnum.graphics/magnum/classMagnum_1_1Trade_1_1TinyGltfImporter.html) plugin for [Magnum](https://github.com/mosra/magnum), a lightweight and modular C++11/C++14 graphics middleware for games and data visualization.
* [Diligent Engine](https://github.com/DiligentGraphics/DiligentEngine) - A modern cross-platform low-level graphics library and rendering framework
* Lighthouse 2: a rendering framework for real-time ray tracing / path tracing experiments. https://github.com/jbikker/lighthouse2
* [QuickLook GLTF](https://github.com/toshiks/glTF-quicklook) - quicklook plugin for macos. Also SceneKit wrapper for tinygltf.
* [GlslViewer](https://github.com/patriciogonzalezvivo/glslViewer) - live GLSL coding for MacOS and Linux
* [Vulkan-Samples](https://github.com/KhronosGroup/Vulkan-Samples) - The Vulkan Samples is collection of resources to help you develop optimized Vulkan applications.
* [TDME2](https://github.com/andreasdr/tdme2) - TDME2 - ThreeDeeMiniEngine2 is a lightweight 3D engine including tools suited for 3D game development using C++11
* [SanityEngine](https://github.com/DethRaid/SanityEngine) - A C++/D3D12 renderer focused on the personal and professional development of its developer
* [Open3D](http://www.open3d.org/) - A Modern Library for 3D Data Processing
* [Supernova Engine](https://github.com/supernovaengine/supernova) - Game engine for 2D and 3D projects with Lua or C++ in data oriented design.
* Your projects here! (Please send PR)
## TODOs
@@ -217,10 +160,9 @@ Model model;
TinyGLTF loader;
std::string err;
std::string warn;
std::string filename = "input.gltf";
bool ret = loader.LoadASCIIFromFile(&model, &err, &warn, filename);
//bool ret = loader.LoadBinaryFromFile(&model, &err, &warn, filename); // for binary glTF(.glb)
bool ret = loader.LoadASCIIFromFile(&model, &err, &warn, argv[1]);
//bool ret = loader.LoadBinaryFromFile(&model, &err, &warn, argv[1]); // for binary glTF(.glb)
if (!warn.empty()) {
printf("Warn: %s\n", warn.c_str());
@@ -231,7 +173,8 @@ if (!err.empty()) {
}
if (!ret) {
printf("Failed to parse glTF: %s\n", filename.c_str());
printf("Failed to parse glTF\n");
return -1;
}
```
@@ -252,6 +195,7 @@ if (!ret) {
* `TINYGLTF_NO_INCLUDE_STB_IMAGE `: Disable including `stb_image.h` from within `tiny_gltf.h` because it has been already included before or you want to include it using custom path before including `tiny_gltf.h`.
* `TINYGLTF_NO_INCLUDE_STB_IMAGE_WRITE `: Disable including `stb_image_write.h` from within `tiny_gltf.h` because it has been already included before or you want to include it using custom path before including `tiny_gltf.h`.
* `TINYGLTF_USE_RAPIDJSON` : Use RapidJSON as a JSON parser/serializer. RapidJSON files are not included in TinyGLTF repo. Please set an include path to RapidJSON if you enable this feature.
* `TINYGLTF_USE_CPP14` : Use C++14 feature(requires C++14 compiler). This may give better performance than C++11.
## CMake options
@@ -268,11 +212,6 @@ set(TINYGLTF_INSTALL OFF CACHE INTERNAL "" FORCE)
add_subdirectory(/path/to/tinygltf)
```
NOTE: Using tinygltf as a submodule doesn't automatically add the headers to your include path (as standard for many libraries). To get this functionality, add the following to the CMakeLists.txt file from above:
```
target_include_directories(${PROJECT_NAME} PRIVATE "/path/to/tinygltf")
```
### Saving gltTF 2.0 model

View File

@@ -1,4 +0,0 @@
# Security Policy
This project manages CVE assignments exclusively through
GitHub Security Advisories.

18
appveyor.yml Normal file
View File

@@ -0,0 +1,18 @@
version: 0.9.{build}
image:
- Visual Studio 2015
# scripts that runs after repo cloning.
install:
- vcsetup.bat
platform: x64
configuration: Release
build:
parallel: true
project: TinyGLTFSolution.sln
after_build:
- examples.bat

View File

@@ -1,70 +0,0 @@
# benchmark/Makefile — Build and run tinygltf v3 benchmarks
#
# Targets:
# make — build gen_synthetic + bench_v3
# make generate — generate synthetic test scenes
# make run — run benchmarks on all generated scenes
# make report — run benchmarks and produce CSV report
# make clean — remove binaries and generated scenes
CXX ?= g++
CXXFLAGS ?= -O2 -std=c++17 -Wall -Wextra -Wno-unused-function
CXXFLAGS += -fno-rtti -fno-exceptions
INCLUDES = -I..
BINDIR = .
GEN = $(BINDIR)/gen_synthetic
BENCH_V3 = $(BINDIR)/bench_v3
# Iteration counts
ITERATIONS ?= 10
WARMUP ?= 2
PREFIX ?= synthetic
.PHONY: all generate run report clean
all: $(GEN) $(BENCH_V3)
$(GEN): gen_synthetic.cpp
$(CXX) $(CXXFLAGS) -o $@ $<
$(BENCH_V3): bench_v3.cpp ../tiny_gltf_v3.h ../tiny_gltf_v3.c ../tinygltf_json_c.h
$(CXX) $(CXXFLAGS) $(INCLUDES) -o $@ $<
# Generate synthetic scenes of varying sizes
generate: $(GEN)
@echo "=== Generating synthetic scenes ==="
./$(GEN) --prefix $(PREFIX)
@echo ""
@echo "Generated files (binary + GLB):"
@ls -lh $(PREFIX)_*.gltf $(PREFIX)_*.glb $(PREFIX)_*.bin 2>/dev/null || true
# Run benchmarks on all generated scenes
run: $(BENCH_V3) generate
@echo ""
@echo "================================================================="
@echo " tinygltf v3 Benchmark"
@echo "================================================================="
@echo ""
@for f in $(PREFIX)_*.glb $(PREFIX)_*.gltf; do \
if [ -f "$$f" ]; then \
./$(BENCH_V3) "$$f" --iterations $(ITERATIONS) --warmup $(WARMUP); \
echo ""; \
fi; \
done
# Run benchmarks and produce CSV report
report: $(BENCH_V3) generate
@echo "file,size_bytes,iterations,parse_min_ms,parse_max_ms,parse_avg_ms,parse_median_ms,throughput_mbs,arena_peak_bytes,meshes,nodes,accessors,materials,animations" > benchmark_report.csv
@for f in $(PREFIX)_*.glb $(PREFIX)_*.gltf; do \
if [ -f "$$f" ]; then \
./$(BENCH_V3) "$$f" --iterations $(ITERATIONS) --warmup $(WARMUP) --csv | tail -1 >> benchmark_report.csv; \
fi; \
done
@echo "=== Report written to benchmark_report.csv ==="
@cat benchmark_report.csv | column -t -s,
clean:
rm -f $(GEN) $(BENCH_V3)
rm -f $(PREFIX)_*.gltf $(PREFIX)_*.glb $(PREFIX)_*.bin
rm -f benchmark_report.csv

View File

@@ -1,396 +0,0 @@
/*
* bench_v3.cpp — Benchmark tinygltf v3 parser: parse speed & memory.
*
* Measures:
* - File read time
* - JSON parse + model build time
* - Peak arena memory usage
* - Throughput (MB/s)
*
* Usage:
* bench_v3 <file.gltf|file.glb> [--iterations N] [--warmup N] [--quiet]
* bench_v3 --batch <file1> <file2> ... [--iterations N]
*/
#define TINYGLTF3_IMPLEMENTATION
#define TINYGLTF3_ENABLE_FS
#include "tiny_gltf_v3.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <vector>
#include <string>
#include <algorithm>
#include <chrono>
#if defined(__linux__)
#include <sys/resource.h>
#endif
/* ------------------------------------------------------------------ */
/* Timing helpers */
/* ------------------------------------------------------------------ */
using Clock = std::chrono::high_resolution_clock;
using TimePoint = Clock::time_point;
static double elapsed_ms(TimePoint start, TimePoint end) {
return std::chrono::duration<double, std::milli>(end - start).count();
}
/* ------------------------------------------------------------------ */
/* Memory tracking allocator */
/* ------------------------------------------------------------------ */
struct MemTracker {
size_t current;
size_t peak;
size_t total_allocs;
size_t total_frees;
};
static void *tracked_alloc(size_t size, void *ud) {
MemTracker *mt = (MemTracker *)ud;
void *ptr = malloc(size);
if (ptr) {
mt->current += size;
if (mt->current > mt->peak) mt->peak = mt->current;
mt->total_allocs++;
}
return ptr;
}
static void *tracked_realloc(void *ptr, size_t old_size, size_t new_size, void *ud) {
MemTracker *mt = (MemTracker *)ud;
void *new_ptr = realloc(ptr, new_size);
if (new_ptr) {
mt->current -= old_size;
mt->current += new_size;
if (mt->current > mt->peak) mt->peak = mt->current;
}
return new_ptr;
}
static void tracked_free(void *ptr, size_t size, void *ud) {
MemTracker *mt = (MemTracker *)ud;
if (ptr) {
mt->current -= size;
mt->total_frees++;
free(ptr);
}
}
/* ------------------------------------------------------------------ */
/* RSS measurement (Linux) */
/* ------------------------------------------------------------------ */
static size_t get_rss_bytes() {
#if defined(__linux__)
FILE *f = fopen("/proc/self/statm", "r");
if (!f) return 0;
long pages = 0;
if (fscanf(f, "%*s %ld", &pages) != 1) pages = 0;
fclose(f);
return (size_t)pages * 4096;
#else
return 0;
#endif
}
/* ------------------------------------------------------------------ */
/* Benchmark result */
/* ------------------------------------------------------------------ */
struct BenchResult {
std::string filename;
uint64_t file_size;
int iterations;
/* Parse timing (ms) */
double parse_min;
double parse_max;
double parse_avg;
double parse_median;
/* Memory */
size_t arena_peak; /* Peak arena allocation */
size_t rss_before;
size_t rss_after;
/* Model stats */
uint32_t meshes;
uint32_t nodes;
uint32_t accessors;
uint32_t materials;
uint32_t animations;
uint32_t buffers;
uint32_t buffer_views;
uint32_t images;
uint32_t textures;
/* Derived */
double throughput_mbs; /* MB/s based on median */
};
/* ------------------------------------------------------------------ */
/* Run benchmark for a single file */
/* ------------------------------------------------------------------ */
static BenchResult bench_file(const char *filename, int iterations, int warmup,
bool quiet, int float32_mode = 0) {
BenchResult r = {};
r.filename = filename;
r.iterations = iterations;
/* Read file into memory */
FILE *f = fopen(filename, "rb");
if (!f) {
fprintf(stderr, "ERROR: Cannot open '%s'\n", filename);
return r;
}
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz <= 0) { fclose(f); return r; }
std::vector<uint8_t> data((size_t)sz);
size_t rd = fread(data.data(), 1, (size_t)sz, f);
fclose(f);
if ((long)rd != sz) { return r; }
r.file_size = (uint64_t)sz;
/* Extract base dir */
std::string path(filename);
std::string base_dir;
size_t sep = path.find_last_of("/\\");
if (sep != std::string::npos) base_dir = path.substr(0, sep);
/* Warmup iterations (not measured) */
for (int i = 0; i < warmup; ++i) {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
tg3_parse_auto(&model, &errors, data.data(), data.size(),
base_dir.c_str(), (uint32_t)base_dir.size(), NULL);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* Benchmark iterations */
std::vector<double> times;
times.reserve(iterations);
MemTracker tracker_best;
memset(&tracker_best, 0, sizeof(tracker_best));
r.rss_before = get_rss_bytes();
for (int i = 0; i < iterations; ++i) {
MemTracker tracker;
memset(&tracker, 0, sizeof(tracker));
tg3_parse_options opts;
tg3_parse_options_init(&opts);
opts.memory.allocator.alloc = tracked_alloc;
opts.memory.allocator.realloc = tracked_realloc;
opts.memory.allocator.free = tracked_free;
opts.memory.allocator.user_data = &tracker;
opts.parse_float32 = float32_mode;
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
TimePoint t0 = Clock::now();
tg3_error_code err = tg3_parse_auto(&model, &errors,
data.data(), data.size(),
base_dir.c_str(),
(uint32_t)base_dir.size(),
&opts);
TimePoint t1 = Clock::now();
double ms = elapsed_ms(t0, t1);
times.push_back(ms);
/* Capture model stats on first successful iteration */
if (i == 0 && err == TG3_OK) {
r.meshes = model.meshes_count;
r.nodes = model.nodes_count;
r.accessors = model.accessors_count;
r.materials = model.materials_count;
r.animations = model.animations_count;
r.buffers = model.buffers_count;
r.buffer_views = model.buffer_views_count;
r.images = model.images_count;
r.textures = model.textures_count;
}
if (tracker.peak > tracker_best.peak) {
tracker_best = tracker;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
if (err != TG3_OK && !quiet) {
fprintf(stderr, " Parse error on iteration %d: code %d\n", i, (int)err);
}
}
r.rss_after = get_rss_bytes();
r.arena_peak = tracker_best.peak;
/* Compute stats */
std::sort(times.begin(), times.end());
r.parse_min = times.front();
r.parse_max = times.back();
double sum = 0;
for (double t : times) sum += t;
r.parse_avg = sum / times.size();
r.parse_median = times[times.size() / 2];
/* Throughput: file_size / median_time */
if (r.parse_median > 0) {
r.throughput_mbs = ((double)r.file_size / (1024.0 * 1024.0)) /
(r.parse_median / 1000.0);
}
return r;
}
/* ------------------------------------------------------------------ */
/* Print results */
/* ------------------------------------------------------------------ */
static const char *human_bytes(size_t bytes, char *buf, size_t buf_sz) {
if (bytes >= 1024ULL * 1024 * 1024)
snprintf(buf, buf_sz, "%.2f GB", (double)bytes / (1024.0 * 1024 * 1024));
else if (bytes >= 1024 * 1024)
snprintf(buf, buf_sz, "%.2f MB", (double)bytes / (1024.0 * 1024));
else if (bytes >= 1024)
snprintf(buf, buf_sz, "%.2f KB", (double)bytes / 1024.0);
else
snprintf(buf, buf_sz, "%zu B", bytes);
return buf;
}
static void print_result(const BenchResult &r) {
char buf1[64], buf2[64];
printf("┌─────────────────────────────────────────────────────────────────┐\n");
printf("│ %-63s │\n", r.filename.c_str());
printf("├─────────────────────────────────────────────────────────────────┤\n");
printf("│ File size: %-47s │\n", human_bytes((size_t)r.file_size, buf1, sizeof(buf1)));
printf("│ Iterations: %-47d │\n", r.iterations);
printf("│ │\n");
printf("│ Parse time (ms): │\n");
printf("│ min: %10.3f │\n", r.parse_min);
printf("│ max: %10.3f │\n", r.parse_max);
printf("│ avg: %10.3f │\n", r.parse_avg);
printf("│ median: %10.3f │\n", r.parse_median);
printf("│ │\n");
printf("│ Throughput: %-47s │\n",
(snprintf(buf1, sizeof(buf1), "%.2f MB/s", r.throughput_mbs), buf1));
printf("│ Arena peak: %-47s │\n", human_bytes(r.arena_peak, buf1, sizeof(buf1)));
if (r.rss_before > 0) {
printf("│ RSS before: %-47s │\n", human_bytes(r.rss_before, buf1, sizeof(buf1)));
printf("│ RSS after: %-47s │\n", human_bytes(r.rss_after, buf2, sizeof(buf2)));
}
printf("│ │\n");
printf("│ Model: %u meshes, %u nodes, %u accessors, %u materials",
r.meshes, r.nodes, r.accessors, r.materials);
printf("\n");
printf("│ %u animations, %u buffers, %u images",
r.animations, r.buffers, r.images);
printf("\n");
printf("└─────────────────────────────────────────────────────────────────┘\n");
}
static void print_csv_header() {
printf("file,size_bytes,iterations,parse_min_ms,parse_max_ms,parse_avg_ms,"
"parse_median_ms,throughput_mbs,arena_peak_bytes,"
"meshes,nodes,accessors,materials,animations\n");
}
static void print_csv_row(const BenchResult &r) {
printf("%s,%lu,%d,%.3f,%.3f,%.3f,%.3f,%.2f,%zu,%u,%u,%u,%u,%u\n",
r.filename.c_str(), (unsigned long)r.file_size, r.iterations,
r.parse_min, r.parse_max, r.parse_avg, r.parse_median,
r.throughput_mbs, r.arena_peak,
r.meshes, r.nodes, r.accessors, r.materials, r.animations);
}
/* ------------------------------------------------------------------ */
/* Main */
/* ------------------------------------------------------------------ */
static void usage() {
fprintf(stderr,
"Usage:\n"
" bench_v3 <file> [--iterations N] [--warmup N] [--csv] [--quiet]\n"
" bench_v3 --batch <file1> [file2] ... [--iterations N] [--csv]\n"
"\n"
"Options:\n"
" --iterations N Number of timed parse iterations (default: 10)\n"
" --warmup N Number of warmup iterations (default: 2)\n"
" --csv Output in CSV format\n"
" --quiet Suppress per-iteration error messages\n"
" --batch Benchmark multiple files\n"
" --float32 Parse JSON floats as float32 (faster, less precise)\n");
}
int main(int argc, char **argv) {
if (argc < 2) { usage(); return 1; }
int iterations = 10;
int warmup = 2;
bool csv = false;
bool quiet = false;
int float32_mode = 0;
std::vector<std::string> files;
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--iterations") == 0 && i + 1 < argc) {
iterations = atoi(argv[++i]);
} else if (strcmp(argv[i], "--warmup") == 0 && i + 1 < argc) {
warmup = atoi(argv[++i]);
} else if (strcmp(argv[i], "--csv") == 0) {
csv = true;
} else if (strcmp(argv[i], "--quiet") == 0) {
quiet = true;
} else if (strcmp(argv[i], "--float32") == 0) {
float32_mode = 1;
} else if (strcmp(argv[i], "--batch") == 0) {
/* batch mode: just collect files */
} else if (argv[i][0] != '-') {
files.push_back(argv[i]);
}
}
if (files.empty()) { usage(); return 1; }
if (csv) print_csv_header();
for (const auto &file : files) {
if (!csv && !quiet) {
printf("Benchmarking: %s (%d iterations, %d warmup%s)\n",
file.c_str(), iterations, warmup,
float32_mode ? ", float32" : "");
}
BenchResult r = bench_file(file.c_str(), iterations, warmup, quiet, float32_mode);
if (csv) {
print_csv_row(r);
} else {
print_result(r);
printf("\n");
}
}
return 0;
}

View File

@@ -1,740 +0,0 @@
/*
* gen_synthetic.cpp — Generate synthetic glTF 2.0 scenes for benchmarking.
*
* Produces .gltf (ASCII) and .glb (binary) files with configurable:
* - Number of meshes, each with N vertices/triangles
* - Number of nodes (flat hierarchy)
* - Number of materials
* - Number of animations with M keyframes
*
* Usage:
* gen_synthetic [--meshes N] [--verts-per-mesh N] [--nodes N]
* [--materials N] [--animations N] [--keyframes N]
* [--prefix NAME]
*
* Outputs: <prefix>_<label>.gltf and <prefix>_<label>.glb
*/
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <string>
#include <vector>
#include <cstdint>
/* ------------------------------------------------------------------ */
/* Tiny JSON writer (no dependencies) */
/* ------------------------------------------------------------------ */
struct JsonWriter {
std::string buf;
int indent;
bool need_comma;
std::vector<bool> stack; /* true = array context */
JsonWriter() : indent(0), need_comma(false) {}
void comma() {
if (need_comma) buf += ",";
buf += "\n";
for (int i = 0; i < indent; ++i) buf += " ";
}
void begin_obj() {
if (!stack.empty()) comma();
buf += "{";
indent++;
need_comma = false;
stack.push_back(false);
}
void end_obj() {
indent--;
buf += "\n";
for (int i = 0; i < indent; ++i) buf += " ";
buf += "}";
stack.pop_back();
need_comma = true;
}
void begin_arr() {
if (!stack.empty() && !need_comma) { /* first elem */ }
buf += "[";
indent++;
need_comma = false;
stack.push_back(true);
}
void end_arr() {
indent--;
buf += "\n";
for (int i = 0; i < indent; ++i) buf += " ";
buf += "]";
stack.pop_back();
need_comma = true;
}
void key(const char *k) {
comma();
buf += "\"";
buf += k;
buf += "\": ";
need_comma = false;
}
void val_str(const char *v) {
if (stack.back()) comma();
buf += "\"";
buf += v;
buf += "\"";
need_comma = true;
}
void val_int(int64_t v) {
if (stack.back()) comma();
buf += std::to_string(v);
need_comma = true;
}
void val_double(double v) {
if (stack.back()) comma();
char tmp[64];
snprintf(tmp, sizeof(tmp), "%.6g", v);
buf += tmp;
need_comma = true;
}
void val_bool(bool v) {
if (stack.back()) comma();
buf += v ? "true" : "false";
need_comma = true;
}
void kv_str(const char *k, const char *v) { key(k); val_str(v); need_comma = true; }
void kv_int(const char *k, int64_t v) { key(k); val_int(v); need_comma = true; }
void kv_double(const char *k, double v) { key(k); val_double(v); need_comma = true; }
void kv_bool(const char *k, bool v) { key(k); val_bool(v); need_comma = true; }
};
/* ------------------------------------------------------------------ */
/* Binary buffer builder */
/* ------------------------------------------------------------------ */
struct BinBuffer {
std::vector<uint8_t> data;
size_t offset() const { return data.size(); }
void push_float(float v) {
const uint8_t *p = reinterpret_cast<const uint8_t*>(&v);
data.insert(data.end(), p, p + 4);
}
void push_u16(uint16_t v) {
const uint8_t *p = reinterpret_cast<const uint8_t*>(&v);
data.insert(data.end(), p, p + 2);
}
void push_u32(uint32_t v) {
const uint8_t *p = reinterpret_cast<const uint8_t*>(&v);
data.insert(data.end(), p, p + 4);
}
void align4() {
while (data.size() % 4 != 0) data.push_back(0);
}
};
/* ------------------------------------------------------------------ */
/* Scene config */
/* ------------------------------------------------------------------ */
struct SceneConfig {
int num_meshes;
int verts_per_mesh;
int num_nodes;
int num_materials;
int num_animations;
int keyframes;
};
/* ------------------------------------------------------------------ */
/* Generate the scene */
/* ------------------------------------------------------------------ */
struct AccessorInfo {
int buffer_view;
int component_type;
int count;
const char *type;
float min_vals[3];
float max_vals[3];
int min_max_components; /* 0 = none, 1 = scalar, 3 = vec3 */
};
static void generate_scene(const SceneConfig &cfg,
std::string &out_json,
std::vector<uint8_t> &out_bin) {
BinBuffer bin;
/* Pre-compute sizes */
int tris_per_mesh = cfg.verts_per_mesh / 3;
if (tris_per_mesh < 1) tris_per_mesh = 1;
int actual_verts = tris_per_mesh * 3;
/*
* For each mesh:
* - positions: actual_verts * 3 floats
* - normals: actual_verts * 3 floats
* - indices: tris_per_mesh * 3 uint16 (or uint32 if >65535)
*
* For each animation:
* - time keys: keyframes floats
* - translation values: keyframes * 3 floats
*/
/* Track buffer views and accessors */
std::vector<size_t> bv_offsets;
std::vector<size_t> bv_lengths;
std::vector<AccessorInfo> accessors;
int bv_idx = 0;
bool use_u32_indices = (actual_verts > 65535);
/* === Mesh data === */
for (int m = 0; m < cfg.num_meshes; ++m) {
float mesh_offset_x = (float)m * 5.0f;
/* Positions */
size_t pos_off = bin.offset();
float pmin[3] = {1e30f, 1e30f, 1e30f};
float pmax[3] = {-1e30f, -1e30f, -1e30f};
for (int v = 0; v < actual_verts; ++v) {
float angle = (float)v / (float)actual_verts * 6.2831853f;
float r = 1.0f + 0.3f * sinf(angle * 5.0f);
float x = mesh_offset_x + r * cosf(angle);
float y = r * sinf(angle);
float z = 0.5f * sinf(angle * 3.0f + (float)m);
bin.push_float(x); bin.push_float(y); bin.push_float(z);
if (x < pmin[0]) pmin[0] = x;
if (x > pmax[0]) pmax[0] = x;
if (y < pmin[1]) pmin[1] = y;
if (y > pmax[1]) pmax[1] = y;
if (z < pmin[2]) pmin[2] = z;
if (z > pmax[2]) pmax[2] = z;
}
size_t pos_len = bin.offset() - pos_off;
bin.align4();
bv_offsets.push_back(pos_off); bv_lengths.push_back(pos_len);
int pos_bv = bv_idx++;
AccessorInfo pos_acc;
pos_acc.buffer_view = pos_bv;
pos_acc.component_type = 5126; /* FLOAT */
pos_acc.count = actual_verts;
pos_acc.type = "VEC3";
memcpy(pos_acc.min_vals, pmin, sizeof(pmin));
memcpy(pos_acc.max_vals, pmax, sizeof(pmax));
pos_acc.min_max_components = 3;
accessors.push_back(pos_acc);
/* Normals */
size_t norm_off = bin.offset();
for (int v = 0; v < actual_verts; ++v) {
float angle = (float)v / (float)actual_verts * 6.2831853f;
float nx = cosf(angle), ny = sinf(angle), nz = 0.0f;
float len = sqrtf(nx*nx + ny*ny + nz*nz);
if (len > 0) { nx /= len; ny /= len; nz /= len; }
bin.push_float(nx); bin.push_float(ny); bin.push_float(nz);
}
size_t norm_len = bin.offset() - norm_off;
bin.align4();
bv_offsets.push_back(norm_off); bv_lengths.push_back(norm_len);
int norm_bv = bv_idx++;
AccessorInfo norm_acc;
norm_acc.buffer_view = norm_bv;
norm_acc.component_type = 5126;
norm_acc.count = actual_verts;
norm_acc.type = "VEC3";
norm_acc.min_max_components = 0;
accessors.push_back(norm_acc);
/* Indices */
size_t idx_off = bin.offset();
for (int t = 0; t < tris_per_mesh; ++t) {
if (use_u32_indices) {
bin.push_u32((uint32_t)(t * 3));
bin.push_u32((uint32_t)(t * 3 + 1));
bin.push_u32((uint32_t)(t * 3 + 2));
} else {
bin.push_u16((uint16_t)(t * 3));
bin.push_u16((uint16_t)(t * 3 + 1));
bin.push_u16((uint16_t)(t * 3 + 2));
}
}
size_t idx_len = bin.offset() - idx_off;
bin.align4();
bv_offsets.push_back(idx_off); bv_lengths.push_back(idx_len);
int idx_bv = bv_idx++;
AccessorInfo idx_acc;
idx_acc.buffer_view = idx_bv;
idx_acc.component_type = use_u32_indices ? 5125 : 5123; /* UINT or USHORT */
idx_acc.count = tris_per_mesh * 3;
idx_acc.type = "SCALAR";
idx_acc.min_max_components = 0;
accessors.push_back(idx_acc);
}
/* === Animation data === */
int anim_time_accessor_start = (int)accessors.size();
for (int a = 0; a < cfg.num_animations; ++a) {
/* Time keys */
size_t time_off = bin.offset();
float tmin = 0.0f, tmax = 0.0f;
for (int k = 0; k < cfg.keyframes; ++k) {
float t = (float)k / (float)(cfg.keyframes - 1) * 10.0f;
bin.push_float(t);
if (k == 0) tmin = t;
tmax = t;
}
size_t time_len = bin.offset() - time_off;
bin.align4();
bv_offsets.push_back(time_off); bv_lengths.push_back(time_len);
int time_bv = bv_idx++;
AccessorInfo time_acc;
time_acc.buffer_view = time_bv;
time_acc.component_type = 5126;
time_acc.count = cfg.keyframes;
time_acc.type = "SCALAR";
time_acc.min_vals[0] = tmin;
time_acc.max_vals[0] = tmax;
time_acc.min_max_components = 1;
accessors.push_back(time_acc);
/* Translation values */
size_t val_off = bin.offset();
for (int k = 0; k < cfg.keyframes; ++k) {
float t = (float)k / (float)(cfg.keyframes - 1) * 10.0f;
float x = sinf(t * 0.5f + (float)a) * 2.0f;
float y = cosf(t * 0.3f) * 1.5f;
float z = sinf(t * 0.7f + (float)a * 0.5f);
bin.push_float(x); bin.push_float(y); bin.push_float(z);
}
size_t val_len = bin.offset() - val_off;
bin.align4();
bv_offsets.push_back(val_off); bv_lengths.push_back(val_len);
int val_bv = bv_idx++;
AccessorInfo val_acc;
val_acc.buffer_view = val_bv;
val_acc.component_type = 5126;
val_acc.count = cfg.keyframes;
val_acc.type = "VEC3";
val_acc.min_max_components = 0;
accessors.push_back(val_acc);
}
size_t total_bin = bin.data.size();
/* === Build JSON === */
JsonWriter w;
w.begin_obj();
/* asset */
w.key("asset"); w.begin_obj();
w.kv_str("version", "2.0");
w.kv_str("generator", "tinygltf_benchmark_gen");
w.end_obj();
/* scene */
w.kv_int("scene", 0);
/* scenes */
w.key("scenes"); w.begin_arr();
w.begin_obj();
w.kv_str("name", "BenchmarkScene");
w.key("nodes"); w.begin_arr();
for (int n = 0; n < cfg.num_nodes; ++n) w.val_int(n);
w.end_arr();
w.end_obj();
w.end_arr();
/* nodes */
w.key("nodes"); w.begin_arr();
for (int n = 0; n < cfg.num_nodes; ++n) {
w.begin_obj();
w.kv_str("name", ("Node_" + std::to_string(n)).c_str());
if (n < cfg.num_meshes) {
w.kv_int("mesh", n);
}
w.key("translation"); w.begin_arr();
w.val_double((double)n * 3.0);
w.val_double(0.0);
w.val_double(0.0);
w.end_arr();
w.end_obj();
}
w.end_arr();
/* meshes */
w.key("meshes"); w.begin_arr();
for (int m = 0; m < cfg.num_meshes; ++m) {
int base_acc = m * 3; /* pos, norm, idx per mesh */
w.begin_obj();
w.kv_str("name", ("Mesh_" + std::to_string(m)).c_str());
w.key("primitives"); w.begin_arr();
w.begin_obj();
w.key("attributes"); w.begin_obj();
w.kv_int("POSITION", base_acc);
w.kv_int("NORMAL", base_acc + 1);
w.end_obj();
w.kv_int("indices", base_acc + 2);
w.kv_int("material", m % cfg.num_materials);
w.kv_int("mode", 4);
w.end_obj();
w.end_arr();
w.end_obj();
}
w.end_arr();
/* materials */
w.key("materials"); w.begin_arr();
for (int m = 0; m < cfg.num_materials; ++m) {
w.begin_obj();
w.kv_str("name", ("Material_" + std::to_string(m)).c_str());
w.key("pbrMetallicRoughness"); w.begin_obj();
w.key("baseColorFactor"); w.begin_arr();
float hue = (float)m / (float)cfg.num_materials;
w.val_double(0.5 + 0.5 * sin(hue * 6.28));
w.val_double(0.5 + 0.5 * sin(hue * 6.28 + 2.09));
w.val_double(0.5 + 0.5 * sin(hue * 6.28 + 4.19));
w.val_double(1.0);
w.end_arr();
w.kv_double("metallicFactor", 0.2 + 0.6 * ((double)m / cfg.num_materials));
w.kv_double("roughnessFactor", 0.3 + 0.5 * ((double)(cfg.num_materials - m) / cfg.num_materials));
w.end_obj();
w.end_obj();
}
w.end_arr();
/* accessors */
w.key("accessors"); w.begin_arr();
for (size_t i = 0; i < accessors.size(); ++i) {
const AccessorInfo &a = accessors[i];
w.begin_obj();
w.kv_int("bufferView", a.buffer_view);
w.kv_int("componentType", a.component_type);
w.kv_int("count", a.count);
w.kv_str("type", a.type);
if (a.min_max_components == 1) {
w.key("min"); w.begin_arr(); w.val_double(a.min_vals[0]); w.end_arr();
w.key("max"); w.begin_arr(); w.val_double(a.max_vals[0]); w.end_arr();
} else if (a.min_max_components == 3) {
w.key("min"); w.begin_arr();
w.val_double(a.min_vals[0]); w.val_double(a.min_vals[1]); w.val_double(a.min_vals[2]);
w.end_arr();
w.key("max"); w.begin_arr();
w.val_double(a.max_vals[0]); w.val_double(a.max_vals[1]); w.val_double(a.max_vals[2]);
w.end_arr();
}
w.end_obj();
}
w.end_arr();
/* bufferViews */
w.key("bufferViews"); w.begin_arr();
for (int i = 0; i < bv_idx; ++i) {
w.begin_obj();
w.kv_int("buffer", 0);
w.kv_int("byteOffset", (int64_t)bv_offsets[i]);
w.kv_int("byteLength", (int64_t)bv_lengths[i]);
w.end_obj();
}
w.end_arr();
/* buffers */
w.key("buffers"); w.begin_arr();
w.begin_obj();
w.kv_int("byteLength", (int64_t)total_bin);
/* URI will be set by caller for .gltf, omitted for .glb */
w.end_obj();
w.end_arr();
/* animations */
if (cfg.num_animations > 0) {
w.key("animations"); w.begin_arr();
for (int a = 0; a < cfg.num_animations; ++a) {
int time_acc = anim_time_accessor_start + a * 2;
int val_acc = time_acc + 1;
/* Target node: cycle through available nodes */
int target_node = a % cfg.num_nodes;
w.begin_obj();
w.kv_str("name", ("Anim_" + std::to_string(a)).c_str());
w.key("channels"); w.begin_arr();
w.begin_obj();
w.kv_int("sampler", 0);
w.key("target"); w.begin_obj();
w.kv_int("node", target_node);
w.kv_str("path", "translation");
w.end_obj();
w.end_obj();
w.end_arr();
w.key("samplers"); w.begin_arr();
w.begin_obj();
w.kv_int("input", time_acc);
w.kv_int("output", val_acc);
w.kv_str("interpolation", "LINEAR");
w.end_obj();
w.end_arr();
w.end_obj();
}
w.end_arr();
}
w.end_obj();
out_json = w.buf;
out_bin = bin.data;
}
/* ------------------------------------------------------------------ */
/* Write .gltf + .bin */
/* ------------------------------------------------------------------ */
static void write_gltf(const std::string &prefix, const std::string &label,
const std::string &json_str,
const std::vector<uint8_t> &bin_data) {
std::string bin_name = prefix + "_" + label + ".bin";
std::string gltf_name = prefix + "_" + label + ".gltf";
/* Inject "uri" into the buffer object in JSON */
std::string json_patched = json_str;
/* Find the buffers array and add uri before the closing } of the buffer */
size_t pos = json_patched.find("\"byteLength\"");
if (pos != std::string::npos) {
/* Find the line end after byteLength value */
size_t line_end = json_patched.find('\n', pos);
if (line_end != std::string::npos) {
/* Extract just the filename for uri */
std::string bin_filename = prefix + "_" + label + ".bin";
std::string uri_line = ",\n \"uri\": \"" + bin_filename + "\"";
json_patched.insert(line_end, uri_line);
}
}
/* Write .bin */
FILE *f = fopen(bin_name.c_str(), "wb");
if (f) {
fwrite(bin_data.data(), 1, bin_data.size(), f);
fclose(f);
}
/* Write .gltf */
f = fopen(gltf_name.c_str(), "w");
if (f) {
fwrite(json_patched.c_str(), 1, json_patched.size(), f);
fclose(f);
}
printf(" Written: %s (%zu bytes JSON) + %s (%zu bytes binary)\n",
gltf_name.c_str(), json_patched.size(),
bin_name.c_str(), bin_data.size());
}
/* ------------------------------------------------------------------ */
/* Write .glb */
/* ------------------------------------------------------------------ */
static void write_glb(const std::string &prefix, const std::string &label,
const std::string &json_str,
const std::vector<uint8_t> &bin_data) {
std::string glb_name = prefix + "_" + label + ".glb";
uint32_t json_len = (uint32_t)json_str.size();
uint32_t json_padded = (json_len + 3) & ~3u;
uint32_t bin_len = (uint32_t)bin_data.size();
uint32_t bin_padded = (bin_len + 3) & ~3u;
uint32_t total = 12 + 8 + json_padded + 8 + bin_padded;
FILE *f = fopen(glb_name.c_str(), "wb");
if (!f) return;
/* Header */
fwrite("glTF", 1, 4, f);
uint32_t version = 2;
fwrite(&version, 4, 1, f);
fwrite(&total, 4, 1, f);
/* JSON chunk */
uint32_t json_type = 0x4E4F534A;
fwrite(&json_padded, 4, 1, f);
fwrite(&json_type, 4, 1, f);
fwrite(json_str.c_str(), 1, json_len, f);
for (uint32_t i = json_len; i < json_padded; ++i) {
char sp = ' ';
fwrite(&sp, 1, 1, f);
}
/* BIN chunk */
uint32_t bin_type = 0x004E4942;
fwrite(&bin_padded, 4, 1, f);
fwrite(&bin_type, 4, 1, f);
fwrite(bin_data.data(), 1, bin_len, f);
for (uint32_t i = bin_len; i < bin_padded; ++i) {
char z = 0;
fwrite(&z, 1, 1, f);
}
fclose(f);
printf(" Written: %s (%u bytes)\n", glb_name.c_str(), total);
}
/* ------------------------------------------------------------------ */
/* Preset configurations */
/* ------------------------------------------------------------------ */
struct Preset {
const char *label;
SceneConfig cfg;
};
static Preset presets[] = {
{"tiny", {1, 100, 2, 1, 0, 0}},
{"small", {5, 1000, 10, 3, 2, 50}},
{"medium", {20, 5000, 50, 10, 5, 200}},
{"large", {100, 10000, 200, 20, 10, 500}},
{"huge", {500, 50000, 1000, 50, 50, 1000}},
};
static const int num_presets = (int)(sizeof(presets) / sizeof(presets[0]));
/* ------------------------------------------------------------------ */
/* Generate float-heavy scene (~500MB of ASCII float values in JSON) */
/* ------------------------------------------------------------------ */
static void generate_float_heavy(const std::string &prefix, size_t target_mb) {
std::string gltf_name = prefix + "_float_heavy.gltf";
FILE *f = fopen(gltf_name.c_str(), "w");
if (!f) {
fprintf(stderr, "Cannot open %s\n", gltf_name.c_str());
return;
}
/* Write minimal valid glTF with massive extras float array */
fprintf(f, "{\n");
fprintf(f, " \"asset\": {\"version\": \"2.0\", \"generator\": \"tinygltf_benchmark_gen\"},\n");
fprintf(f, " \"scene\": 0,\n");
fprintf(f, " \"scenes\": [{\"name\": \"FloatHeavy\", \"nodes\": [0]}],\n");
fprintf(f, " \"nodes\": [{\"name\": \"Root\"}],\n");
fprintf(f, " \"extras\": {\n");
size_t target_bytes = target_mb * 1024ULL * 1024ULL;
size_t total_written = 0;
int num_channels = 10;
size_t per_channel = target_bytes / (size_t)num_channels;
for (int ch = 0; ch < num_channels; ++ch) {
fprintf(f, " \"channel_%d\": [\n ", ch);
size_t ch_written = 0;
size_t count = 0;
uint64_t seed = (uint64_t)ch * 7919ULL + 1;
bool first = true;
while (ch_written < per_channel) {
/* Comma before every value except the first */
if (!first) {
fwrite(",\n ", 1, 8, f);
ch_written += 8;
}
first = false;
/* Generate varied float values: mix of magnitudes and precisions */
seed = seed * 6364136223846793005ULL + 1442695040888963407ULL;
double raw = (double)(int64_t)seed / (double)INT64_MAX;
double val;
int kind = (int)(count % 5);
switch (kind) {
case 0: val = raw * 1000.0; break; /* large: -999.xxx */
case 1: val = raw * 0.001; break; /* small: 0.000xxx */
case 2: val = raw * 3.14159265358979; break; /* medium: -3.14..3.14 */
case 3: val = raw * 1e6; break; /* very large */
case 4: val = raw * 1e-6; break; /* very small */
default: val = raw; break;
}
char buf[64];
int len = snprintf(buf, sizeof(buf), "%.8g", val);
fwrite(buf, 1, (size_t)len, f);
ch_written += (size_t)len;
count++;
}
total_written += ch_written;
if (ch < num_channels - 1) {
fprintf(f, "\n ],\n");
} else {
fprintf(f, "\n ]\n");
}
}
fprintf(f, " }\n");
fprintf(f, "}\n");
fclose(f);
/* Report actual file size */
f = fopen(gltf_name.c_str(), "rb");
if (f) {
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fclose(f);
printf(" Written: %s (%.1f MB, ~%zu float values across %d channels)\n",
gltf_name.c_str(), (double)sz / (1024.0 * 1024.0),
total_written / 12, num_channels);
}
}
/* ------------------------------------------------------------------ */
/* Main */
/* ------------------------------------------------------------------ */
int main(int argc, char **argv) {
std::string prefix = "synthetic";
/* Parse args */
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--prefix") == 0 && i + 1 < argc) {
prefix = argv[++i];
}
}
printf("Generating synthetic glTF benchmark scenes...\n\n");
for (int p = 0; p < num_presets; ++p) {
const Preset &pr = presets[p];
printf("[%s] meshes=%d verts/mesh=%d nodes=%d materials=%d "
"animations=%d keyframes=%d\n",
pr.label, pr.cfg.num_meshes, pr.cfg.verts_per_mesh,
pr.cfg.num_nodes, pr.cfg.num_materials,
pr.cfg.num_animations, pr.cfg.keyframes);
std::string json;
std::vector<uint8_t> bin;
generate_scene(pr.cfg, json, bin);
write_gltf(prefix, pr.label, json, bin);
write_glb(prefix, pr.label, json, bin);
printf("\n");
}
/* Float-heavy scene: ~500MB of ASCII floats in JSON */
printf("[float_heavy] ~500MB of ASCII float values in JSON extras\n");
generate_float_heavy(prefix, 500);
printf("\n");
printf("Done.\n");
return 0;
}

View File

@@ -1,5 +0,0 @@
include_directories(${CMAKE_SOURCE_DIR})
add_executable(create_triangle_gltf create_triangle_gltf.cpp)
target_compile_options(create_triangle_gltf PUBLIC -Wall)
target_link_libraries(create_triangle_gltf )

View File

@@ -788,10 +788,8 @@ static void QuatToAngleAxis(const std::vector<double> quaternion,
return;
}
constexpr double pi = 3.14159265358979323846;
double denom = sqrt(1-qw*qw);
outAngleDegrees = angleRadians * 180.0 / pi;
outAngleDegrees = angleRadians * 180.0 / M_PI;
axis[0] = qx / denom;
axis[1] = qy / denom;
axis[2] = qz / denom;

View File

@@ -6,10 +6,7 @@
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "tiny_gltf.h"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iostream>
@@ -855,206 +852,6 @@ static void Dump(const tinygltf::Model &model) {
}
}
/* ===== Digest helpers (used to compare v1 vs v3 parses) ===================== */
static uint64_t fnv64(const unsigned char *data, size_t n) {
uint64_t h = 0xcbf29ce484222325ULL;
for (size_t i = 0; i < n; ++i) { h ^= data[i]; h *= 0x100000001b3ULL; }
return h;
}
static void d_str(const std::string &s) {
putchar('"');
for (unsigned char c : s) {
if (c == '"' || c == '\\') { putchar('\\'); putchar((char)c); }
else if (c < 0x20 || c >= 0x7f) putchar('?');
else putchar((char)c);
}
putchar('"');
}
static void d_dbl(double v) { printf("%.7g", v); }
static void d_dbl_arr(const double *v, size_t n) {
putchar('[');
for (size_t i = 0; i < n; ++i) { if (i) putchar(','); d_dbl(v[i]); }
putchar(']');
}
static void d_dbl_vec(const std::vector<double> &v) {
d_dbl_arr(v.data(), v.size());
}
static void PrintDigest(const tinygltf::Model &m) {
printf("DIGEST_BEGIN\n");
printf("asset version=");
d_str(m.asset.version);
printf(" generator=");
d_str(m.asset.generator);
printf("\n");
for (size_t i = 0; i < m.buffers.size(); ++i) {
const auto &b = m.buffers[i];
uint64_t h = b.data.empty() ? 0 : fnv64(b.data.data(), b.data.size());
printf("buffer %zu byte_length=%llu fnv64=0x%016llx\n",
i, (unsigned long long)b.data.size(), (unsigned long long)h);
}
for (size_t i = 0; i < m.bufferViews.size(); ++i) {
const auto &bv = m.bufferViews[i];
printf("buffer_view %zu buffer=%d byte_offset=%llu byte_length=%llu byte_stride=%u\n",
i, bv.buffer, (unsigned long long)bv.byteOffset,
(unsigned long long)bv.byteLength, (unsigned)bv.byteStride);
}
for (size_t i = 0; i < m.accessors.size(); ++i) {
const auto &a = m.accessors[i];
printf("accessor %zu buffer_view=%d byte_offset=%llu component_type=%d count=%llu type=%d normalized=%d min=",
i, a.bufferView, (unsigned long long)a.byteOffset, a.componentType,
(unsigned long long)a.count, a.type, a.normalized ? 1 : 0);
d_dbl_vec(a.minValues);
printf(" max=");
d_dbl_vec(a.maxValues);
printf(" sparse=%d\n", a.sparse.isSparse ? 1 : 0);
}
for (size_t i = 0; i < m.meshes.size(); ++i) {
const auto &me = m.meshes[i];
printf("mesh %zu primitives_count=%zu weights_count=%zu\n",
i, me.primitives.size(), me.weights.size());
for (size_t j = 0; j < me.primitives.size(); ++j) {
const auto &p = me.primitives[j];
printf("prim %zu %zu indices=%d material=%d mode=%d attrs=[",
i, j, p.indices, p.material, p.mode);
// attributes is std::map → already sorted by key
bool first = true;
for (const auto &kv : p.attributes) {
if (!first) putchar(',');
printf("%s:%d", kv.first.c_str(), kv.second);
first = false;
}
printf("] targets_count=%zu\n", p.targets.size());
}
}
for (size_t i = 0; i < m.nodes.size(); ++i) {
const auto &n = m.nodes[i];
double t[3] = {0, 0, 0};
double r[4] = {0, 0, 0, 1};
double s[3] = {1, 1, 1};
double mat[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
int has_matrix = (n.matrix.size() == 16) ? 1 : 0;
if (n.translation.size() == 3) std::copy(n.translation.begin(), n.translation.end(), t);
if (n.rotation.size() == 4) std::copy(n.rotation.begin(), n.rotation.end(), r);
if (n.scale.size() == 3) std::copy(n.scale.begin(), n.scale.end(), s);
if (has_matrix) std::copy(n.matrix.begin(), n.matrix.end(), mat);
printf("node %zu mesh=%d skin=%d camera=%d light=%d children_count=%zu has_matrix=%d t=",
i, n.mesh, n.skin, n.camera, n.light, n.children.size(), has_matrix);
d_dbl_arr(t, 3);
printf(" r=");
d_dbl_arr(r, 4);
printf(" s=");
d_dbl_arr(s, 3);
printf(" matrix=");
d_dbl_arr(mat, 16);
printf(" weights_count=%zu\n", n.weights.size());
}
for (size_t i = 0; i < m.materials.size(); ++i) {
const auto &mat = m.materials[i];
double ef[3] = {0, 0, 0};
double bcf[4] = {1, 1, 1, 1};
if (mat.emissiveFactor.size() == 3)
std::copy(mat.emissiveFactor.begin(), mat.emissiveFactor.end(), ef);
if (mat.pbrMetallicRoughness.baseColorFactor.size() == 4)
std::copy(mat.pbrMetallicRoughness.baseColorFactor.begin(),
mat.pbrMetallicRoughness.baseColorFactor.end(), bcf);
printf("material %zu alpha_mode=", i);
d_str(mat.alphaMode);
printf(" alpha_cutoff=");
d_dbl(mat.alphaCutoff);
printf(" double_sided=%d emissive=", mat.doubleSided ? 1 : 0);
d_dbl_arr(ef, 3);
printf(" base_color_factor=");
d_dbl_arr(bcf, 4);
printf(" metallic=");
d_dbl(mat.pbrMetallicRoughness.metallicFactor);
printf(" roughness=");
d_dbl(mat.pbrMetallicRoughness.roughnessFactor);
printf(" base_color_tex=%d normal_tex=%d occlusion_tex=%d emissive_tex=%d\n",
mat.pbrMetallicRoughness.baseColorTexture.index,
mat.normalTexture.index,
mat.occlusionTexture.index,
mat.emissiveTexture.index);
}
for (size_t i = 0; i < m.textures.size(); ++i) {
const auto &t = m.textures[i];
printf("texture %zu source=%d sampler=%d\n", i, t.source, t.sampler);
}
for (size_t i = 0; i < m.samplers.size(); ++i) {
const auto &s = m.samplers[i];
printf("sampler %zu min_filter=%d mag_filter=%d wrap_s=%d wrap_t=%d\n",
i, s.minFilter, s.magFilter, s.wrapS, s.wrapT);
}
for (size_t i = 0; i < m.images.size(); ++i) {
const auto &im = m.images[i];
/* mime_type and uri normalization differ between v1/v3 (data URIs,
extension inference); buffer_view reference is the parse-fidelity bit. */
printf("image %zu buffer_view=%d\n", i, im.bufferView);
}
for (size_t i = 0; i < m.skins.size(); ++i) {
const auto &s = m.skins[i];
printf("skin %zu inverse_bind_matrices=%d skeleton=%d joints_count=%zu\n",
i, s.inverseBindMatrices, s.skeleton, s.joints.size());
}
for (size_t i = 0; i < m.animations.size(); ++i) {
const auto &a = m.animations[i];
printf("animation %zu channels_count=%zu samplers_count=%zu\n",
i, a.channels.size(), a.samplers.size());
for (size_t j = 0; j < a.channels.size(); ++j) {
const auto &c = a.channels[j];
printf("chan %zu %zu sampler=%d target_node=%d target_path=", i, j,
c.sampler, c.target_node);
d_str(c.target_path);
printf("\n");
}
for (size_t j = 0; j < a.samplers.size(); ++j) {
const auto &as = a.samplers[j];
printf("samp %zu %zu input=%d output=%d interpolation=", i, j,
as.input, as.output);
d_str(as.interpolation);
printf("\n");
}
}
for (size_t i = 0; i < m.cameras.size(); ++i) {
const auto &c = m.cameras[i];
bool is_persp = (c.type == "perspective");
printf("camera %zu type=", i);
d_str(c.type);
if (is_persp) {
printf(" yfov=");
d_dbl(c.perspective.yfov);
printf(" znear=");
d_dbl(c.perspective.znear);
printf(" zfar=");
d_dbl(c.perspective.zfar);
printf(" aspect=");
d_dbl(c.perspective.aspectRatio);
} else {
printf(" xmag=");
d_dbl(c.orthographic.xmag);
printf(" ymag=");
d_dbl(c.orthographic.ymag);
printf(" znear=");
d_dbl(c.orthographic.znear);
printf(" zfar=");
d_dbl(c.orthographic.zfar);
}
printf("\n");
}
for (size_t i = 0; i < m.scenes.size(); ++i) {
const auto &s = m.scenes[i];
printf("scene %zu nodes_count=%zu\n", i, s.nodes.size());
}
printf("DIGEST_END\n");
}
int main(int argc, char **argv) {
if (argc < 2) {
printf("Needs input.gltf\n");
@@ -1103,20 +900,6 @@ int main(int argc, char **argv) {
return -1;
}
printf("COUNTS"
" accessors=%zu animations=%zu buffers=%zu bufferViews=%zu"
" cameras=%zu images=%zu materials=%zu meshes=%zu nodes=%zu"
" samplers=%zu scenes=%zu skins=%zu textures=%zu lights=%zu\n",
model.accessors.size(), model.animations.size(),
model.buffers.size(), model.bufferViews.size(),
model.cameras.size(), model.images.size(),
model.materials.size(), model.meshes.size(),
model.nodes.size(), model.samplers.size(),
model.scenes.size(), model.skins.size(),
model.textures.size(), model.lights.size());
PrintDigest(model);
Dump(model);
return 0;

View File

@@ -773,7 +773,7 @@ static int stbi_write_hdr_core(stbi__write_context *s, int x, int y, int comp, f
#ifdef __STDC_LIB_EXT1__
len = sprintf_s(buffer, sizeof(buffer), "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x);
#else
len = snprintf(buffer, sizeof(buffer), "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x);
len = sprintf(buffer, "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x);
#endif
s->func(s->context, buffer, len);

View File

@@ -2,167 +2,63 @@
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.
## Simple test runner.
# -- config -----------------------
sample_model_dir = "/mnt/nfs/syoyo/glTF-Sample-Models"
# Absolute path pointing to your cloned git repo of https://github.com/KhronosGroup/glTF-Sample-Models
sample_model_dir = "/home/syoyo/work/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"]
# Include `glTF-Draco` when you build `loader_example` with draco support.
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)
failed = []
success = []
def run(filename):
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)
cmd = ["./loader_example", filename]
try:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdout, stderr) = p.communicate()
except:
print("Failed to execute: ", cmd)
raise
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)
if p.returncode != 0:
failed.append(filename)
print(stdout)
print(stderr)
else:
success.append(filename)
def test():
for d in sorted(os.listdir(base_model_dir)):
for d in 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)
if os.path.isdir(p):
for k in kinds:
targetDir = os.path.join(p, k)
g = glob.glob(targetDir + "/*.gltf") + glob.glob(targetDir + "/*.glb")
for gltf in g:
run(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)))
print("Success : {0}".format(len(success)))
print("Failed : {0}".format(len(failed)))
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]))
for fail in failed:
print("FAIL: " + fail)
if counts_diff or digest_diff or parse_failed:
sys.exit(1)
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@@ -1,13 +1,6 @@
# Use this for strict compilation check(will work on clang 3.8+)
#EXTRA_CXXFLAGS := -fsanitize=address -Wall -Werror -Weverything -Wno-c++11-long-long -DTINYGLTF_APPLY_CLANG_WEVERYTHING
all: ../tiny_gltf.h tester_v3_c tester_v3_c_v1port
all: ../tiny_gltf.h
clang++ -I../ $(EXTRA_CXXFLAGS) -std=c++11 -g -O0 -o tester tester.cc
clang++ -DTINYGLTF_NOEXCEPTION -I../ $(EXTRA_CXXFLAGS) -std=c++11 -g -O0 -o tester_noexcept tester.cc
clang++ -DTINYGLTF_USE_CUSTOM_JSON -I../ $(EXTRA_CXXFLAGS) -std=c++11 -g -O0 -o tester_intensive_customjson tester_intensive_customjson.cc
tester_v3_c: tester_v3_c.c ../tiny_gltf_v3.h ../tiny_gltf_v3.c ../tinygltf_json_c.h
clang -I../ -std=c11 -g -O0 -DTINYGLTF3_ENABLE_FS -o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c
tester_v3_c_v1port: tester_v3_c_v1port.c ../tiny_gltf_v3.h ../tiny_gltf_v3.c ../tinygltf_json_c.h
clang -I../ -std=c11 -g -O0 -DTINYGLTF3_ENABLE_FS -o tester_v3_c_v1port tester_v3_c_v1port.c ../tiny_gltf_v3.c

View File

@@ -4,14 +4,9 @@ Do fuzzing test for TinyGLTF API.
## Supported API
* [x] LoadASCIIFromString
* [x] LoadASCIIFromMemory
* [ ] LoadBinaryFromMemory
### Custom JSON backend (`tinygltf_json.h`)
* [x] LoadASCIIFromString
* [x] LoadBinaryFromMemory
## Requirements
* meson
@@ -41,17 +36,11 @@ $ cd build
$ ninja
```
This builds two fuzzers:
* `fuzz_gltf` default nlohmann/json backend
* `fuzz_gltf_customjson` custom `tinygltf_json.h` backend (tests both ASCII and binary parsing paths)
## How to run
Increase memory limit. e.g. `-rss_limit_mb=50000`
```
$ ./fuzz_gltf -rss_limit_mb=20000 -jobs 4
$ ./fuzz_gltf_customjson -rss_limit_mb=20000 -jobs 4
```

View File

@@ -1,76 +0,0 @@
/*
* LLVM libFuzzer harness for tinygltf with the custom JSON backend
* (tinygltf_json.h).
*
* Exercises:
* 1. LoadASCIIFromString glTF JSON parsing
* 2. LoadBinaryFromMemory GLB binary parsing
*
* Build (clang with libFuzzer):
* clang++ -std=c++11 -fsanitize=address,fuzzer \
* -DTINYGLTF_USE_CUSTOM_JSON \
* -I../../ fuzz_gltf_customjson.cc \
* -o fuzz_gltf_customjson
*
* Run:
* ./fuzz_gltf_customjson -rss_limit_mb=20000 -jobs 4
*/
#include <cstdint>
#include <cstring>
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define TINYGLTF_IMPLEMENTATION
#ifndef TINYGLTF_USE_CUSTOM_JSON
#define TINYGLTF_USE_CUSTOM_JSON
#endif
#include "tiny_gltf.h"
/* Fuzz the ASCII (JSON) parser path */
static void fuzz_ascii(const uint8_t *data, size_t size) {
tinygltf::Model model;
tinygltf::TinyGLTF ctx;
std::string err;
std::string warn;
const char *str = reinterpret_cast<const char *>(data);
bool ret =
ctx.LoadASCIIFromString(&model, &err, &warn, str,
static_cast<unsigned int>(size), /* base_dir */ "");
(void)ret;
}
/* Fuzz the binary (GLB) parser path */
static void fuzz_binary(const uint8_t *data, size_t size) {
tinygltf::Model model;
tinygltf::TinyGLTF ctx;
std::string err;
std::string warn;
bool ret = ctx.LoadBinaryFromMemory(&model, &err, &warn, data,
static_cast<unsigned int>(size),
/* base_dir */ "");
(void)ret;
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size == 0) return 0;
/* Use the lowest bit of the first byte to select the parse path.
* The remaining bits are left for the fuzzer engine to explore;
* additional paths (e.g. LoadASCIIFromFile, check_sections flags)
* can be added here in the future using more selector bits. */
uint8_t selector = data[0];
const uint8_t *payload = data + 1;
size_t payload_size = size - 1;
if (selector & 1) {
fuzz_binary(payload, payload_size);
} else {
fuzz_ascii(payload, payload_size);
}
return 0;
}

View File

@@ -7,9 +7,3 @@ executable('fuzz_gltf',
cpp_args : '-fsanitize=address,fuzzer',
link_args : '-fsanitize=address,fuzzer' )
executable('fuzz_gltf_customjson',
'fuzz_gltf_customjson.cc',
include_directories : incdirs,
cpp_args : ['-fsanitize=address,fuzzer', '-DTINYGLTF_USE_CUSTOM_JSON'],
link_args : '-fsanitize=address,fuzzer' )

Binary file not shown.

View File

@@ -474,7 +474,7 @@ TEST_CASE("image-uri-spaces", "[issue-236]") {
}
REQUIRE(true == ret);
REQUIRE(err.empty());
REQUIRE(warn.empty());
REQUIRE(!warn.empty()); // relative image path won't exist in tests/
REQUIRE(saved.images.size() == model.images.size());
// The image uri in CubeImageUriMultipleSpaces.gltf is not encoded and
@@ -494,23 +494,25 @@ TEST_CASE("image-uri-spaces", "[issue-236]") {
}
TEST_CASE("serialize-empty-material", "[issue-294]") {
tinygltf::Model m;
// Add default constructed material to model
m.materials.push_back({});
// Serialize model to output stream
tinygltf::Material mat;
mat.pbrMetallicRoughness.baseColorFactor = {1.0f, 1.0f, 1.0f, 1.0f}; // default baseColorFactor
m.materials.push_back(mat);
std::stringstream os;
tinygltf::TinyGLTF ctx;
bool ret = ctx.WriteGltfSceneToStream(&m, os, false, false);
REQUIRE(true == ret);
// Parse serialized model
// use nlohmann json
nlohmann::json j = nlohmann::json::parse(os.str());
// Serialized materials shall hold an empty object that
// represents the default constructed material
REQUIRE(j.find("materials") != j.end());
REQUIRE(j["materials"].is_array());
REQUIRE(1 == j["materials"].size());
CHECK(j["materials"][0].is_object());
CHECK(j["materials"][0].empty());
REQUIRE(j["materials"][0].is_object());
}
TEST_CASE("empty-skeleton-id", "[issue-321]") {
@@ -662,11 +664,10 @@ TEST_CASE("serialize-image-callback", "[issue-394]") {
auto writer = [](const std::string *basepath, const std::string *filename,
const tinygltf::Image *image, bool embedImages,
const tinygltf::FsCallbacks* fs, const tinygltf::URICallbacks *uri_cb,
std::string *out_uri, void *user_pointer) -> bool {
const tinygltf::URICallbacks *uri_cb, std::string *out_uri,
void *user_pointer) -> bool {
(void)basepath;
(void)image;
(void)fs;
(void)uri_cb;
REQUIRE(*filename == "foo");
REQUIRE(embedImages == true);
@@ -700,13 +701,12 @@ TEST_CASE("serialize-image-failure", "[issue-394]") {
auto writer = [](const std::string *basepath, const std::string *filename,
const tinygltf::Image *image, bool embedImages,
const tinygltf::FsCallbacks* fs, const tinygltf::URICallbacks *uri_cb,
std::string *out_uri, void *user_pointer) -> bool {
const tinygltf::URICallbacks *uri_cb, std::string *out_uri,
void *user_pointer) -> bool {
(void)basepath;
(void)filename;
(void)image;
(void)embedImages;
(void)fs;
(void)uri_cb;
(void)out_uri;
(void)user_pointer;
@@ -757,529 +757,3 @@ TEST_CASE("load-issue-416-model", "[issue-416]") {
// external file load fails, but reading glTF itself is ok.
REQUIRE(true == ret);
}
TEST_CASE("serialize-empty-node", "[issue-457]") {
tinygltf::Model m;
// Add default constructed node to model
m.nodes.push_back({});
// Add scene to model
m.scenes.push_back({});
// The scene's only node is the empty node
m.scenes.front().nodes.push_back(0);
// Serialize model to output stream
std::stringstream os;
tinygltf::TinyGLTF ctx;
bool ret = ctx.WriteGltfSceneToStream(&m, os, false, false);
REQUIRE(true == ret);
// Parse serialized model
nlohmann::json j = nlohmann::json::parse(os.str());
// Serialized nodes shall hold an empty object that
// represents the default constructed node
REQUIRE(j.find("nodes") != j.end());
REQUIRE(j["nodes"].is_array());
REQUIRE(1 == j["nodes"].size());
CHECK(j["nodes"][0].is_object());
CHECK(j["nodes"][0].empty());
// We also want to make sure that the serialized scene
// is referencing the empty node.
// There shall be a single serialized scene
auto scenes = j.find("scenes");
REQUIRE(scenes != j.end());
REQUIRE(scenes->is_array());
REQUIRE(1 == scenes->size());
auto scene = scenes->at(0);
REQUIRE(scene.is_object());
// The scene's nodes array shall hold a reference
// to the single node
auto nodes = scene.find("nodes");
REQUIRE(nodes != scene.end());
REQUIRE(nodes->is_array());
REQUIRE(1 == nodes->size());
auto node = nodes->at(0);
CHECK(node.is_number_integer());
int idx = -1;
node.get_to(idx);
CHECK(0 == idx);
}
TEST_CASE("serialize-light-index", "[issue-458]") {
// Create the light
tinygltf::Light light;
light.type = "point";
light.intensity = 0.75;
light.color = std::vector<double>{1.0, 0.8, 0.95};
// Stream to serialize to
std::stringstream os;
{
tinygltf::Model m;
tinygltf::Scene scene;
// Add the light to the model
m.lights.push_back(light);
// Create a node that uses the light
tinygltf::Node node;
node.light = 0;
// Add the node to the model
m.nodes.push_back(node);
// Add the node to the scene
scene.nodes.push_back(0);
// Add the scene to the model
m.scenes.push_back(scene);
// Serialize model to output stream
tinygltf::TinyGLTF ctx;
bool ret = ctx.WriteGltfSceneToStream(&m, os, false, false);
REQUIRE(true == ret);
}
{
tinygltf::Model m;
tinygltf::TinyGLTF ctx;
// Parse the serialized model
bool ok = ctx.LoadASCIIFromString(&m, nullptr, nullptr, os.str().c_str(), os.str().size(), "");
REQUIRE(true == ok);
// Check if the light was correctly serialized
REQUIRE(1 == m.lights.size());
CHECK(m.lights[0] == light);
// Check that the node properly references the light
REQUIRE(1 == m.nodes.size());
CHECK(m.nodes[0].light == 0);
}
}
TEST_CASE("default-material", "[issue-459]") {
const std::vector<double> default_emissive_factor{ 0.0, 0.0, 0.0 };
const std::vector<double> default_base_color_factor{ 1.0, 1.0, 1.0, 1.0 };
const std::string default_alpha_mode = "OPAQUE";
const double default_alpha_cutoff = 0.5;
const bool default_double_sided = false;
const double default_metallic_factor = 1.0;
const double default_roughness_factor = 1.0;
// Check that default constructed material
// holds actual default GLTF material properties
tinygltf::Material mat;
CHECK(mat.alphaMode == default_alpha_mode);
CHECK(mat.alphaCutoff == default_alpha_cutoff);
CHECK(mat.doubleSided == default_double_sided);
CHECK(mat.emissiveFactor == default_emissive_factor);
CHECK(mat.pbrMetallicRoughness.baseColorFactor == default_base_color_factor);
CHECK(mat.pbrMetallicRoughness.metallicFactor == default_metallic_factor);
CHECK(mat.pbrMetallicRoughness.roughnessFactor == default_roughness_factor);
// None of the textures should be set
CHECK(mat.normalTexture.index == -1);
CHECK(mat.occlusionTexture.index == -1);
CHECK(mat.emissiveTexture.index == -1);
}
TEST_CASE("serialize-empty-scene", "[issue-464]") {
// Stream to serialize to
std::stringstream os;
{
tinygltf::Model m;
// Add empty scene to the model
m.scenes.push_back({});
// Serialize model to output stream
tinygltf::TinyGLTF ctx;
bool ret = ctx.WriteGltfSceneToStream(&m, os, false, false);
REQUIRE(true == ret);
}
{
tinygltf::Model m;
tinygltf::TinyGLTF ctx;
// Parse the serialized model
bool ok = ctx.LoadASCIIFromString(&m, nullptr, nullptr, os.str().c_str(), os.str().size(), "");
REQUIRE(true == ok);
// Make sure the empty scene is there
REQUIRE(1 == m.scenes.size());
tinygltf::Scene scene{};
// Check that the scene is empty
CHECK(m.scenes[0] == scene);
}
}
TEST_CASE("zero-sized-bin-chunk-glb", "[issue-440]") {
tinygltf::Model model;
tinygltf::TinyGLTF ctx;
std::string err;
std::string warn;
// Input glb has zero-sized data in bin chunk(8 bytes for BIN chunk, and chunksize == 0)
// The spec https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#binary-buffer says
//
// When the binary buffer is empty or when it is stored by other means, this chunk SHOULD be omitted.
//
// 'SHOULD' mean 'RECOMMENDED', so we'll need to allow such zero-sized bin chunk is NOT omitted.
bool ret = ctx.LoadBinaryFromFile(&model, &err, &warn, "../models/regression/zero-sized-bin-chunk-issue-440.glb");
if (!warn.empty()) {
std::cout << "WARN: " << warn << "\n";
}
if (!err.empty()) {
std::cerr << err << std::endl;
}
REQUIRE(true == ret);
}
TEST_CASE("serialize-node-emitter", "[KHR_audio]") {
// Stream to serialize to
std::stringstream os;
{
tinygltf::Model m;
// Create a default audio emitter
m.audioEmitters.resize(1);
// Create a single node
m.nodes.resize(1);
// The node references the single emitter
m.nodes[0].emitter = 0;
// Create a single scene
m.scenes.resize(1);
// Make the scene reference the single node
m.scenes[0].nodes.push_back(0);
// Serialize model to output stream
tinygltf::TinyGLTF ctx;
bool ret = ctx.WriteGltfSceneToStream(&m, os, false, false);
REQUIRE(true == ret);
}
{
tinygltf::Model m;
tinygltf::TinyGLTF ctx;
// Parse the serialized model
bool ok = ctx.LoadASCIIFromString(&m, nullptr, nullptr, os.str().c_str(), os.str().size(), "");
REQUIRE(true == ok);
// Make sure the single scene is there
REQUIRE(1 == m.scenes.size());
// Make sure all three nodes are there
REQUIRE(1 == m.nodes.size());
// Make sure the single root node of the scene is there
REQUIRE(1 == m.scenes[0].nodes.size());
REQUIRE(0 == m.scenes[0].nodes[0]);
// Retrieve the scene root node
const tinygltf::Node& node = m.nodes[m.scenes[0].nodes[0]];
// Make sure the single root node has both lod nodes
REQUIRE(0 == node.emitter);
}
}
TEST_CASE("serialize-lods", "[lods]") {
// Stream to serialize to
std::stringstream os;
{
tinygltf::Model m;
m.nodes.resize(4);
// Add Node 1 and Node 2 as lods to Node 0
m.nodes[0].lods.push_back(1);
m.nodes[0].lods.push_back(2);
// Add Material 1 and Material 2 as lods to Material 0
m.materials.resize(4);
m.materials[0].lods.push_back(1);
m.materials[0].lods.push_back(2);
tinygltf::Scene scene;
// Scene uses Node 0 and 3 as root node
scene.nodes.push_back(0);
scene.nodes.push_back(3);
// Add scene to the model
m.scenes.push_back(scene);
// Serialize model to output stream
tinygltf::TinyGLTF ctx;
bool ret = ctx.WriteGltfSceneToStream(&m, os, false, false);
REQUIRE(true == ret);
}
{
tinygltf::Model m;
tinygltf::TinyGLTF ctx;
// Parse the serialized model
bool ok = ctx.LoadASCIIFromString(&m, nullptr, nullptr, os.str().c_str(), os.str().size(), "");
REQUIRE(true == ok);
// Make sure the model's used extensions hold MSFT_lod
CHECK(m.extensionsUsed.size() == 1);
CHECK(m.extensionsUsed[0].compare("MSFT_lod") == 0);
// MSFT_lod is not a required extension
CHECK(m.extensionsRequired.size() == 0);
// Make sure all four materials are there
REQUIRE(4 == m.materials.size());
// Make sure the first material has both lod materials
REQUIRE(2 == m.materials[0].lods.size());
// Make sure the order is still the same after serialization and deserialization
CHECK(1 == m.materials[0].lods[0]);
CHECK(2 == m.materials[0].lods[1]);
// Make sure the material with lods exposes the MSFT_lod extension
CHECK(m.materials[0].extensions.size() == 1);
CHECK(m.materials[0].extensions.count("MSFT_lod") == 1);
// Make sure the last material has no lod materials
CHECK(0 == m.materials[3].lods.size());
// Make sure the material without lods does not exposes the MSFT_lod extension
CHECK(m.materials[3].extensions.size() == 0);
CHECK(m.materials[3].extensions.count("MSFT_lod") == 0);
// Make sure the single scene is there
REQUIRE(1 == m.scenes.size());
// Make sure all four nodes are there
REQUIRE(4 == m.nodes.size());
// Make sure the two root nodes of the scene are there
REQUIRE(2 == m.scenes[0].nodes.size());
REQUIRE(0 == m.scenes[0].nodes[0]);
REQUIRE(3 == m.scenes[0].nodes[1]);
// Retrieve the node with lods
const tinygltf::Node& nodeWithLods = m.nodes[m.scenes[0].nodes[0]];
// Make sure the node has both lod nodes
REQUIRE(2 == nodeWithLods.lods.size());
// Make sure the order is still the same after serialization and deserialization
CHECK(1 == nodeWithLods.lods[0]);
CHECK(2 == nodeWithLods.lods[1]);
// Make sure the node with lods exposes the MSFT_lod extension
CHECK(nodeWithLods.extensions.size() == 1);
CHECK(nodeWithLods.extensions.count("MSFT_lod") == 1);
// Retrieve the node without lods
const tinygltf::Node& nodeWithoutLods = m.nodes[m.scenes[0].nodes[1]];
// Make sure the node has no lod nodes
CHECK(0 == nodeWithoutLods.lods.size());
// Make sure the node without lods does not exposes the MSFT_lod extension
CHECK(nodeWithoutLods.extensions.size() == 0);
CHECK(nodeWithoutLods.extensions.count("MSFT_lod") == 0);
}
}
TEST_CASE("write-image-issue", "[issue-473]") {
std::string err;
std::string warn;
tinygltf::Model model;
tinygltf::TinyGLTF ctx;
bool ok = ctx.LoadASCIIFromFile(&model, &err, &warn, "../models/Cube/Cube.gltf");
REQUIRE(ok);
REQUIRE(err.empty());
REQUIRE(warn.empty());
REQUIRE(model.images.size() == 2);
REQUIRE(model.images[0].uri == "Cube_BaseColor.png");
REQUIRE(model.images[1].uri == "Cube_MetallicRoughness.png");
REQUIRE_FALSE(model.images[0].image.empty());
REQUIRE_FALSE(model.images[1].image.empty());
ok = ctx.WriteGltfSceneToFile(&model, "Cube.gltf");
REQUIRE(ok);
for (const auto& image : model.images) {
std::fstream file(image.uri);
CHECK(file.good());
}
}
TEST_CASE("images-as-is", "[issue-487]") {
std::string err;
std::string warn;
tinygltf::Model model;
tinygltf::TinyGLTF ctx;
ctx.SetImagesAsIs(true);
bool ok = ctx.LoadASCIIFromFile(&model, &err, &warn, "../models/Cube/Cube.gltf");
REQUIRE(ok);
REQUIRE(err.empty());
REQUIRE(warn.empty());
for (const auto& image : model.images) {
CHECK(image.as_is == true);
CHECK_FALSE(image.uri.empty());
CHECK_FALSE(image.image.empty());
#ifndef TINYGLTF_NO_STB_IMAGE
// Make sure we can decode the images
int w = -1, h = -1, component = -1;
unsigned char *data = stbi_load_from_memory(image.image.data(), static_cast<int>(image.image.size()), &w, &h, &component, 0);
CHECK(data != nullptr);
CHECK(w == 512);
CHECK(h == 512);
CHECK(component >= 3);
stbi_image_free(data);
#endif
}
// Write glTF model to disk, and images as separate files
{
ok = ctx.WriteGltfSceneToFile(&model, "Cube_with_image_files.gltf");
REQUIRE(ok);
// All the images should have been written to disk with their original data
for (const auto& image : model.images) {
// Make sure the image files exist
{
std::fstream file(image.uri);
CHECK(file.good());
} // Close file before stbi_load (Windows sharing violation fix)
#ifndef TINYGLTF_NO_STB_IMAGE
// Make sure we can load the images
int w = -1, h = -1, component = -1;
unsigned char *data = stbi_load(image.uri.c_str(), &w, &h, &component, 0);
CHECK(data != nullptr);
CHECK(w == 512);
CHECK(h == 512);
CHECK(component >= 3);
stbi_image_free(data);
#endif
}
}
// Write glTF model to disk, and embed images as data URIs
{
ok = ctx.WriteGltfSceneToFile(&model, "Cube_with_embedded_images.gltf", true, false);
REQUIRE(ok);
// Load above model again, and check if the images are loaded properly
tinygltf::Model embeddedImages;
ctx.SetImagesAsIs(false);
bool ok = ctx.LoadASCIIFromFile(&embeddedImages, &err, &warn, "Cube_with_embedded_images.gltf");
REQUIRE(ok);
REQUIRE(err.empty());
REQUIRE(warn.empty());
for (const auto& image : embeddedImages.images) {
CHECK(image.as_is == false);
CHECK_FALSE(image.mimeType.empty());
CHECK_FALSE(image.image.empty());
CHECK(image.width == 512);
CHECK(image.height == 512);
CHECK(image.component >= 3);
}
}
// Write glTF model to disk, as GLB
{
ok = ctx.WriteGltfSceneToFile(&model, "Cube.glb", true, true, true, true);
REQUIRE(ok);
// Load above model again, and check if the images are loaded properly
tinygltf::Model glbModel;
ctx.SetImagesAsIs(false);
bool ok = ctx.LoadBinaryFromFile(&glbModel, &err, &warn, "Cube.glb");
REQUIRE(ok);
REQUIRE(err.empty());
REQUIRE(warn.empty());
for (const auto& image : glbModel.images) {
CHECK(image.as_is == false);
CHECK_FALSE(image.mimeType.empty());
CHECK_FALSE(image.image.empty());
CHECK(image.width == 512);
CHECK(image.height == 512);
CHECK(image.component >= 3);
}
}
}
TEST_CASE("inverse-bind-matrices-optional", "[issue-492]") {
tinygltf::Model model;
tinygltf::TinyGLTF ctx;
std::string err;
std::string warn;
bool ret = ctx.LoadBinaryFromFile(&model, &err, &warn, "issue-492.glb");
if (!warn.empty()) {
std::cout << "WARN:" << warn << std::endl;
}
if (!err.empty()) {
std::cerr << "ERR:" << err << std::endl;
}
REQUIRE(true == ret);
REQUIRE(err.empty());
}
bool LoadImageData(tinygltf::Image * /* image */, const int /* image_idx */, std::string * /* err */,
std::string * /* warn */, int /* req_width */, int /* req_height */,
const unsigned char * /* bytes */, int /* size */, void * /*user_data */) {
return true;
}
bool WriteImageData(const std::string * /* basepath */, const std::string * /* filename */,
const tinygltf::Image *image, bool /* embedImages */,
const tinygltf::FsCallbacks * /* fs_cb */, const tinygltf::URICallbacks * /* uri_cb */,
std::string * /* out_uri */, void * user_pointer) {
REQUIRE(user_pointer != nullptr);
auto counter = static_cast<int*>(user_pointer);
*counter = *counter + 1;
return true;
}
TEST_CASE("empty-images-not-written", "[issue-495]") {
std::string err;
std::string warn;
tinygltf::Model model;
tinygltf::TinyGLTF ctx;
ctx.SetImageLoader(LoadImageData, nullptr);
bool ok = ctx.LoadASCIIFromFile(&model, &err, &warn, "../models/Cube/Cube.gltf");
REQUIRE(ok);
REQUIRE(err.empty());
REQUIRE(warn.empty());
CHECK(model.images.size() == 2);
for (const auto& image : model.images) {
// No data loaded or decoded
CHECK(image.image.empty());
// The URI is kept
CHECK_FALSE(image.uri.empty());
// The URI should not be a data URI
CHECK(image.uri.find("data:") != 0);
}
// Now write the loaded model
int counter = 0;
ctx.SetImageWriter(WriteImageData, &counter);
ok = ctx.WriteGltfSceneToFile(&model, "issue-495-external.gltf");
CHECK(ok);
// WriteImageData should be invoked for both images
CHECK(counter == 2);
}
#ifdef TINYGLTF_USE_CUSTOM_JSON
/* Regression test: in float32_mode, integer-only tokens with more than 9
* digits must still be parsed as integers (is_int == 1), not floats.
* Previously, max_sig=9 was applied to the integer part too, causing excess
* digits to bump exp10, which broke the exp10==0 guard in the integer
* fast-path and mis-classified the value as a float. */
TEST_CASE("cj-float32-long-integer", "[customjson]") {
// Values chosen to cover exactly-at, just-over, and near int64 boundaries.
struct {
const char *text;
int64_t expected;
} cases[] = {
{ "1234567890", 1234567890LL }, /* 10 digits */
{ "12345678901", 12345678901LL }, /* 11 digits */
{ "1000000000000", 1000000000000LL }, /* 13 digits */
{ "9223372036854775807", INT64_MAX }, /* max int64 (19 digits) */
{ "-1234567890", -1234567890LL }, /* negative 10 digits */
{ "-9223372036854775808", INT64_MIN }, /* min int64 */
};
for (auto &tc : cases) {
int is_int = 0;
int64_t ival = 0;
double dval = 0.0;
const char *end = tc.text + strlen(tc.text);
const char *ret = cj_parse_number(tc.text, end, &is_int, &ival, &dval, /*float32_mode=*/1);
CAPTURE(tc.text);
REQUIRE(ret != nullptr);
CHECK(is_int == 1);
CHECK(ival == tc.expected);
}
}
#endif /* TINYGLTF_USE_CUSTOM_JSON */

File diff suppressed because it is too large Load Diff

View File

@@ -1,772 +0,0 @@
#include "tiny_gltf_v3.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ===== Digest helpers (used to compare v1 vs v3 parses) ===================== */
static uint64_t fnv64(const uint8_t *data, uint64_t n) {
uint64_t h = 0xcbf29ce484222325ULL;
uint64_t i;
for (i = 0; i < n; ++i) { h ^= data[i]; h *= 0x100000001b3ULL; }
return h;
}
static void d_str(const tg3_str *s) {
uint32_t i;
putchar('"');
if (s && s->data) {
for (i = 0; i < s->len; ++i) {
unsigned char c = (unsigned char)s->data[i];
if (c == '"' || c == '\\') { putchar('\\'); putchar((char)c); }
else if (c < 0x20 || c >= 0x7f) putchar('?');
else putchar((char)c);
}
}
putchar('"');
}
static void d_dbl(double v) { printf("%.7g", v); }
static void d_dbl_arr(const double *v, uint32_t n) {
uint32_t i;
putchar('[');
for (i = 0; i < n; ++i) { if (i) putchar(','); d_dbl(v[i]); }
putchar(']');
}
static int cmp_str_int_pair(const void *a, const void *b) {
const tg3_str_int_pair *pa = (const tg3_str_int_pair *)a;
const tg3_str_int_pair *pb = (const tg3_str_int_pair *)b;
uint32_t la = pa->key.len, lb = pb->key.len;
uint32_t m = la < lb ? la : lb;
int r = memcmp(pa->key.data, pb->key.data, m);
if (r) return r;
return (la < lb) ? -1 : (la > lb ? 1 : 0);
}
static void d_attrs(const tg3_str_int_pair *attrs, uint32_t n) {
tg3_str_int_pair *sorted;
uint32_t i;
if (n == 0) { fputs("[]", stdout); return; }
sorted = (tg3_str_int_pair *)malloc(n * sizeof(*sorted));
memcpy(sorted, attrs, n * sizeof(*sorted));
qsort(sorted, n, sizeof(*sorted), cmp_str_int_pair);
putchar('[');
for (i = 0; i < n; ++i) {
if (i) putchar(',');
printf("%.*s:%d", (int)sorted[i].key.len, sorted[i].key.data, sorted[i].value);
}
putchar(']');
free(sorted);
}
static void print_digest(const tg3_model *m) {
uint32_t i, j;
printf("DIGEST_BEGIN\n");
printf("asset version=");
d_str(&m->asset.version);
printf(" generator=");
d_str(&m->asset.generator);
printf("\n");
for (i = 0; i < m->buffers_count; ++i) {
const tg3_buffer *b = &m->buffers[i];
uint64_t h = b->data.data ? fnv64(b->data.data, b->data.count) : 0;
printf("buffer %u byte_length=%llu fnv64=0x%016llx\n",
i, (unsigned long long)b->data.count, (unsigned long long)h);
}
for (i = 0; i < m->buffer_views_count; ++i) {
const tg3_buffer_view *bv = &m->buffer_views[i];
printf("buffer_view %u buffer=%d byte_offset=%llu byte_length=%llu byte_stride=%u\n",
i, bv->buffer, (unsigned long long)bv->byte_offset,
(unsigned long long)bv->byte_length, bv->byte_stride);
}
for (i = 0; i < m->accessors_count; ++i) {
const tg3_accessor *a = &m->accessors[i];
printf("accessor %u buffer_view=%d byte_offset=%llu component_type=%d count=%llu type=%d normalized=%d min=",
i, a->buffer_view, (unsigned long long)a->byte_offset, a->component_type,
(unsigned long long)a->count, a->type, a->normalized);
d_dbl_arr(a->min_values, a->min_values_count);
printf(" max=");
d_dbl_arr(a->max_values, a->max_values_count);
printf(" sparse=%d\n", a->sparse.is_sparse);
}
for (i = 0; i < m->meshes_count; ++i) {
const tg3_mesh *me = &m->meshes[i];
printf("mesh %u primitives_count=%u weights_count=%u\n",
i, me->primitives_count, me->weights_count);
for (j = 0; j < me->primitives_count; ++j) {
const tg3_primitive *p = &me->primitives[j];
printf("prim %u %u indices=%d material=%d mode=%d attrs=", i, j,
p->indices, p->material, p->mode);
d_attrs(p->attributes, p->attributes_count);
printf(" targets_count=%u\n", p->targets_count);
}
}
for (i = 0; i < m->nodes_count; ++i) {
const tg3_node *n = &m->nodes[i];
printf("node %u mesh=%d skin=%d camera=%d light=%d children_count=%u has_matrix=%d t=",
i, n->mesh, n->skin, n->camera, n->light, n->children_count, n->has_matrix);
d_dbl_arr(n->translation, 3);
printf(" r=");
d_dbl_arr(n->rotation, 4);
printf(" s=");
d_dbl_arr(n->scale, 3);
printf(" matrix=");
d_dbl_arr(n->matrix, 16);
printf(" weights_count=%u\n", n->weights_count);
}
for (i = 0; i < m->materials_count; ++i) {
const tg3_material *mat = &m->materials[i];
printf("material %u alpha_mode=", i);
d_str(&mat->alpha_mode);
printf(" alpha_cutoff=");
d_dbl(mat->alpha_cutoff);
printf(" double_sided=%d emissive=", mat->double_sided);
d_dbl_arr(mat->emissive_factor, 3);
printf(" base_color_factor=");
d_dbl_arr(mat->pbr_metallic_roughness.base_color_factor, 4);
printf(" metallic=");
d_dbl(mat->pbr_metallic_roughness.metallic_factor);
printf(" roughness=");
d_dbl(mat->pbr_metallic_roughness.roughness_factor);
printf(" base_color_tex=%d normal_tex=%d occlusion_tex=%d emissive_tex=%d\n",
mat->pbr_metallic_roughness.base_color_texture.index,
mat->normal_texture.index,
mat->occlusion_texture.index,
mat->emissive_texture.index);
}
for (i = 0; i < m->textures_count; ++i) {
const tg3_texture *t = &m->textures[i];
printf("texture %u source=%d sampler=%d\n", i, t->source, t->sampler);
}
for (i = 0; i < m->samplers_count; ++i) {
const tg3_sampler *s = &m->samplers[i];
printf("sampler %u min_filter=%d mag_filter=%d wrap_s=%d wrap_t=%d\n",
i, s->min_filter, s->mag_filter, s->wrap_s, s->wrap_t);
}
for (i = 0; i < m->images_count; ++i) {
const tg3_image *im = &m->images[i];
/* mime_type and uri normalization differ between v1/v3 (data URIs,
extension inference); buffer_view reference is the parse-fidelity bit. */
printf("image %u buffer_view=%d\n", i, im->buffer_view);
}
for (i = 0; i < m->skins_count; ++i) {
const tg3_skin *s = &m->skins[i];
printf("skin %u inverse_bind_matrices=%d skeleton=%d joints_count=%u\n",
i, s->inverse_bind_matrices, s->skeleton, s->joints_count);
}
for (i = 0; i < m->animations_count; ++i) {
const tg3_animation *a = &m->animations[i];
printf("animation %u channels_count=%u samplers_count=%u\n",
i, a->channels_count, a->samplers_count);
for (j = 0; j < a->channels_count; ++j) {
const tg3_animation_channel *c = &a->channels[j];
printf("chan %u %u sampler=%d target_node=%d target_path=", i, j,
c->sampler, c->target.node);
d_str(&c->target.path);
printf("\n");
}
for (j = 0; j < a->samplers_count; ++j) {
const tg3_animation_sampler *as = &a->samplers[j];
printf("samp %u %u input=%d output=%d interpolation=", i, j,
as->input, as->output);
d_str(&as->interpolation);
printf("\n");
}
}
for (i = 0; i < m->cameras_count; ++i) {
const tg3_camera *c = &m->cameras[i];
int is_persp = (c->type.len == 11 && memcmp(c->type.data, "perspective", 11) == 0);
printf("camera %u type=", i);
d_str(&c->type);
if (is_persp) {
printf(" yfov=");
d_dbl(c->perspective.yfov);
printf(" znear=");
d_dbl(c->perspective.znear);
printf(" zfar=");
d_dbl(c->perspective.zfar);
printf(" aspect=");
d_dbl(c->perspective.aspect_ratio);
} else {
printf(" xmag=");
d_dbl(c->orthographic.xmag);
printf(" ymag=");
d_dbl(c->orthographic.ymag);
printf(" znear=");
d_dbl(c->orthographic.znear);
printf(" zfar=");
d_dbl(c->orthographic.zfar);
}
printf("\n");
}
for (i = 0; i < m->scenes_count; ++i) {
const tg3_scene *s = &m->scenes[i];
printf("scene %u nodes_count=%u\n", i, s->nodes_count);
}
printf("DIGEST_END\n");
}
/* ============================================================================ */
static int mem_contains(const uint8_t *data, uint64_t size, const char *needle) {
size_t needle_len = strlen(needle);
uint64_t i;
if (needle_len == 0 || size < (uint64_t)needle_len) {
return 0;
}
for (i = 0; i + (uint64_t)needle_len <= size; ++i) {
if (memcmp(data + i, needle, needle_len) == 0) {
return 1;
}
}
return 0;
}
static int check_minimal_parse(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"scene\":0,"
"\"scenes\":[{\"nodes\":[0]}],"
"\"nodes\":[{\"name\":\"root\"}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse_auto(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0,
&opts);
if (err != TG3_OK) {
fprintf(stderr, "tg3_parse_auto failed: %d\n", (int)err);
tg3_error_stack_free(&errors);
return 0;
}
if (model.default_scene != 0 || model.scenes_count != 1 || model.nodes_count != 1) {
fprintf(stderr, "unexpected parsed model shape\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
if (!model.nodes || !model.nodes[0].name.data ||
!tg3_str_equals_cstr(model.nodes[0].name, "root")) {
fprintf(stderr, "node name mismatch\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_minimal_write_roundtrip(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"scene\":0,"
"\"scenes\":[{\"nodes\":[0]}],"
"\"nodes\":[{\"name\":\"root\"}]}";
tg3_model model;
tg3_model roundtrip;
tg3_error_stack errors;
tg3_parse_options parse_opts;
tg3_write_options write_opts;
tg3_error_code err;
uint8_t *out = NULL;
uint64_t out_size = 0;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&parse_opts);
tg3_write_options_init(&write_opts);
err = tg3_parse_auto(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0,
&parse_opts);
if (err != TG3_OK) {
fprintf(stderr, "initial parse failed: %d\n", (int)err);
tg3_error_stack_free(&errors);
return 0;
}
err = tg3_write_to_memory(&model, &errors, &out, &out_size, &write_opts);
if (err != TG3_OK || !out || out_size == 0) {
fprintf(stderr, "tg3_write_to_memory failed: %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
if (!mem_contains(out, out_size, "\"asset\"") ||
!mem_contains(out, out_size, "\"root\"")) {
fprintf(stderr, "serialized JSON missing expected fields\n");
tg3_write_free(out, &write_opts);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
err = tg3_parse_auto(&roundtrip, &errors, out, out_size, "", 0, &parse_opts);
tg3_write_free(out, &write_opts);
if (err != TG3_OK) {
fprintf(stderr, "roundtrip parse failed: %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
if (roundtrip.default_scene != 0 || roundtrip.nodes_count != 1 ||
!roundtrip.nodes || !roundtrip.nodes[0].name.data ||
!tg3_str_equals_cstr(roundtrip.nodes[0].name, "root")) {
fprintf(stderr, "roundtrip model mismatch\n");
tg3_model_free(&roundtrip);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&roundtrip);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_parse_file_failure_initializes_model(void) {
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
memset(&model, 0xA5, sizeof(model));
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse_file(&model, &errors,
"tg3-tester-nonexistent-path.gltf", 32, &opts);
if (err != TG3_ERR_FS_NOT_AVAILABLE && err != TG3_ERR_FILE_NOT_FOUND) {
fprintf(stderr, "tg3_parse_file unexpected error: %d\n", (int)err);
tg3_error_stack_free(&errors);
return 0;
}
if (model.default_scene != -1) {
fprintf(stderr, "tg3_parse_file did not initialize model on failure\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_non_object_root_rejected(void) {
static const uint8_t json[] = "\"not an object\"";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_ERR_JSON_PARSE) {
fprintf(stderr, "non-object root returned unexpected error: %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
if (model.default_scene != -1) {
fprintf(stderr, "non-object root left model in unexpected state\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_huge_integer_field_rejected(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},\"scene\":6.66667e+70}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_ERR_JSON_PARSE) {
fprintf(stderr, "huge integer-like field returned unexpected error: %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
/* ===== Security regression tests ============================================ */
/* fs read_file callback that records calls into *(int *)user_data and never
* succeeds — used to verify path-traversal URIs never reach the filesystem. */
static int32_t recording_read_file(uint8_t **out_data, uint64_t *out_size,
const char *path, uint32_t path_len,
void *user_data) {
(void)out_data; (void)out_size; (void)path; (void)path_len;
if (user_data) *(int *)user_data += 1;
return 0;
}
static int check_path_traversal_rejected(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"buffers\":[{\"uri\":\"../../etc/passwd\",\"byteLength\":4}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
int fs_calls = 0;
uint32_t i;
int saw = 0;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
opts.fs.read_file = recording_read_file;
opts.fs.user_data = &fs_calls;
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1),
"/some/base", 10, &opts);
if (err == TG3_OK) {
fprintf(stderr, "path traversal NOT rejected\n");
goto fail;
}
if (fs_calls != 0) {
fprintf(stderr, "fs.read_file called %d times for traversal URI\n", fs_calls);
goto fail;
}
for (i = 0; i < errors.count; ++i) {
if (errors.entries[i].code == TG3_ERR_INVALID_VALUE) saw = 1;
}
if (!saw) {
fprintf(stderr, "expected TG3_ERR_INVALID_VALUE on traversal\n");
goto fail;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
fail:
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
static int check_absolute_uri_rejected(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"buffers\":[{\"uri\":\"/etc/passwd\",\"byteLength\":4}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
int fs_calls = 0;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
opts.fs.read_file = recording_read_file;
opts.fs.user_data = &fs_calls;
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1),
"/base", 5, &opts);
if (err == TG3_OK || fs_calls != 0) {
fprintf(stderr, "absolute URI not rejected (rc=%d, fs_calls=%d)\n",
(int)err, fs_calls);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_negative_byte_stride_rejected(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"buffers\":[{\"byteLength\":4}],"
"\"bufferViews\":[{\"buffer\":0,\"byteLength\":4,\"byteStride\":-1}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err == TG3_OK) {
fprintf(stderr, "negative byteStride NOT rejected\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_oob_index_rejected(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"buffers\":[{\"byteLength\":4}],"
"\"bufferViews\":[{\"buffer\":0,\"byteLength\":4}],"
"\"accessors\":[{\"bufferView\":1000000,\"componentType\":5121,"
"\"count\":1,\"type\":\"SCALAR\"}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_ERR_INVALID_INDEX) {
fprintf(stderr, "OOB index expected TG3_ERR_INVALID_INDEX, got %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_oob_index_opt_in(void) {
/* When validate_indices=0, parse should accept the same out-of-range index. */
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"buffers\":[{\"byteLength\":4}],"
"\"bufferViews\":[{\"buffer\":0,\"byteLength\":4}],"
"\"accessors\":[{\"bufferView\":1000000,\"componentType\":5121,"
"\"count\":1,\"type\":\"SCALAR\"}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
opts.validate_indices = 0;
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_OK) {
fprintf(stderr, "validate_indices=0 should accept OOB index, got %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
if (model.accessors_count != 1 || model.accessors[0].buffer_view != 1000000) {
fprintf(stderr, "OOB index not preserved when validation off\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_extension_index_oob_rejected(void) {
/* MSFT_lod and KHR_audio index fields must be validated when
* validate_indices=1, otherwise downstream consumers can read OOB. */
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"nodes\":[{\"extensions\":{\"MSFT_lod\":{\"ids\":[99999]}}}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_ERR_INVALID_INDEX) {
fprintf(stderr, "MSFT_lod OOB index expected TG3_ERR_INVALID_INDEX, got %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_error_messages_survive_parse_failure(void) {
/* Regression: parse failure must not invalidate arena-allocated error
* message strings on the user's tg3_error_stack before model_free. */
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"buffers\":[{\"byteLength\":4}],"
"\"bufferViews\":[{\"buffer\":0,\"byteLength\":4,\"byteStride\":-1}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
uint32_t i;
int seen_stride_msg = 0;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err == TG3_OK) goto fail;
for (i = 0; i < errors.count; ++i) {
const char *m = errors.entries[i].message;
if (m && strstr(m, "byteStride")) seen_stride_msg = 1;
}
if (!seen_stride_msg) {
fprintf(stderr, "byteStride error message missing or unreadable after parse\n");
goto fail;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
fail:
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
static int parse_file_arg(const char *path) {
FILE *fp = fopen(path, "rb");
uint8_t *buf;
long sz;
size_t got;
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
int ok;
const char *slash;
size_t base_len;
if (!fp) {
fprintf(stderr, "open failed: %s\n", path);
return 0;
}
if (fseek(fp, 0, SEEK_END) != 0 || (sz = ftell(fp)) < 0 ||
fseek(fp, 0, SEEK_SET) != 0) {
fprintf(stderr, "seek failed: %s\n", path);
fclose(fp);
return 0;
}
buf = (uint8_t *)malloc((size_t)sz);
if (!buf) {
fprintf(stderr, "alloc failed: %s\n", path);
fclose(fp);
return 0;
}
got = fread(buf, 1, (size_t)sz, fp);
fclose(fp);
if (got != (size_t)sz) {
fprintf(stderr, "short read: %s\n", path);
free(buf);
return 0;
}
slash = strrchr(path, '/');
base_len = slash ? (size_t)(slash - path) : 0;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse_auto(&model, &errors, buf, (uint64_t)sz,
path, (uint32_t)base_len, &opts);
ok = (err == TG3_OK);
if (!ok) {
uint32_t i;
fprintf(stderr, "parse failed (%d): %s\n", (int)err, path);
for (i = 0; i < errors.count; ++i) {
fprintf(stderr, " [%u] code=%d sev=%d path=%s msg=%s offset=%lld\n",
i, (int)errors.entries[i].code, (int)errors.entries[i].severity,
errors.entries[i].json_path ? errors.entries[i].json_path : "",
errors.entries[i].message ? errors.entries[i].message : "",
(long long)errors.entries[i].byte_offset);
}
} else {
printf("COUNTS"
" accessors=%u animations=%u buffers=%u bufferViews=%u"
" cameras=%u images=%u materials=%u meshes=%u nodes=%u"
" samplers=%u scenes=%u skins=%u textures=%u lights=%u\n",
model.accessors_count, model.animations_count,
model.buffers_count, model.buffer_views_count,
model.cameras_count, model.images_count,
model.materials_count, model.meshes_count,
model.nodes_count, model.samplers_count,
model.scenes_count, model.skins_count,
model.textures_count, model.lights_count);
print_digest(&model);
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
free(buf);
return ok;
}
int main(int argc, char **argv) {
if (argc > 1) {
int i;
for (i = 1; i < argc; ++i) {
if (!parse_file_arg(argv[i])) return 1;
}
return 0;
}
if (!check_minimal_parse()) {
return 1;
}
if (!check_minimal_write_roundtrip()) {
return 1;
}
if (!check_parse_file_failure_initializes_model()) {
return 1;
}
if (!check_non_object_root_rejected()) {
return 1;
}
if (!check_huge_integer_field_rejected()) {
return 1;
}
if (!check_path_traversal_rejected()) {
return 1;
}
if (!check_absolute_uri_rejected()) {
return 1;
}
if (!check_negative_byte_stride_rejected()) {
return 1;
}
if (!check_oob_index_rejected()) {
return 1;
}
if (!check_oob_index_opt_in()) {
return 1;
}
if (!check_extension_index_oob_rejected()) {
return 1;
}
if (!check_error_messages_survive_parse_failure()) {
return 1;
}
return 0;
}

View File

@@ -1,559 +0,0 @@
/*
* tester_v3_c_v1port.c — Parse/load tests ported from tester.cc (v1) to the
* pure-C v3 runtime. Each PORT_CASE corresponds to a TEST_CASE in tester.cc;
* the comment marks the original test name.
*
* Skipped from tester.cc (require v3 writer or v1-internal helpers, out of
* scope for this port):
* serialize-empty-material, serialize-empty-node, serialize-light-index,
* serialize-empty-scene, serialize-node-emitter, serialize-lods,
* serialize-const-image, serialize-image-callback, serialize-image-failure,
* write-image-issue, empty-images-not-written, empty-bin-buffer,
* parse-integer, parse-unsigned, parse-integer-array,
* expandpath-utf-8, cj-float32-long-integer
*
* Build (run from tests/):
* make tester_v3_c_v1port
* Run:
* ./tester_v3_c_v1port
*/
#include "tiny_gltf_v3.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* --- thin helpers ---------------------------------------------------------- */
static int tg3_str_eq(const tg3_str *s, const char *cstr) {
size_t n = strlen(cstr);
return s->data && s->len == (uint32_t)n &&
memcmp(s->data, cstr, n) == 0;
}
static int tg3_str_contains(const tg3_str *s, char ch) {
uint32_t i;
if (!s->data) return 0;
for (i = 0; i < s->len; ++i) {
if (s->data[i] == ch) return 1;
}
return 0;
}
static int has_extension(const tg3_extras_ext *e, const char *name) {
uint32_t i;
if (!e || !e->extensions) return 0;
for (i = 0; i < e->extensions_count; ++i) {
if (tg3_str_eq(&e->extensions[i].name, name)) return 1;
}
return 0;
}
static const tg3_extension *find_extension(const tg3_extras_ext *e, const char *name) {
uint32_t i;
if (!e || !e->extensions) return NULL;
for (i = 0; i < e->extensions_count; ++i) {
if (tg3_str_eq(&e->extensions[i].name, name)) return &e->extensions[i];
}
return NULL;
}
static const tg3_value *value_get(const tg3_value *v, const char *key) {
uint32_t i;
if (!v || v->type != TG3_VALUE_OBJECT || !v->object_data) return NULL;
for (i = 0; i < v->object_count; ++i) {
if (tg3_str_eq(&v->object_data[i].key, key)) {
return &v->object_data[i].value;
}
}
return NULL;
}
/* Read a glTF/GLB file into a heap buffer; returns 1 on success. */
static int slurp(const char *path, uint8_t **out, size_t *out_len) {
FILE *fp = fopen(path, "rb");
long sz;
size_t got;
uint8_t *buf;
if (!fp) return 0;
if (fseek(fp, 0, SEEK_END) != 0 || (sz = ftell(fp)) < 0 ||
fseek(fp, 0, SEEK_SET) != 0) { fclose(fp); return 0; }
buf = (uint8_t *)malloc((size_t)sz);
if (!buf) { fclose(fp); return 0; }
got = fread(buf, 1, (size_t)sz, fp);
fclose(fp);
if (got != (size_t)sz) { free(buf); return 0; }
*out = buf;
*out_len = (size_t)sz;
return 1;
}
static const char *base_dir_of(const char *path, uint32_t *out_len) {
const char *slash = strrchr(path, '/');
if (slash) {
*out_len = (uint32_t)(slash - path);
} else {
*out_len = 0;
}
return path;
}
static int parse_path(tg3_model *m, tg3_error_stack *e,
tg3_parse_options *opts,
const char *path, tg3_error_code *err_out) {
uint8_t *buf = NULL;
size_t sz = 0;
uint32_t base_len = 0;
const char *base_dir;
if (!slurp(path, &buf, &sz)) {
fprintf(stderr, " slurp failed: %s\n", path);
return 0;
}
base_dir = base_dir_of(path, &base_len);
*err_out = tg3_parse_auto(m, e, buf, (uint64_t)sz,
base_dir, base_len, opts);
free(buf);
return 1;
}
static int errstack_contains(const tg3_error_stack *e, const char *needle) {
uint32_t i;
size_t nl = strlen(needle);
for (i = 0; i < e->count; ++i) {
const char *m = e->entries[i].message;
if (m && strstr(m, needle) != NULL) return 1;
(void)nl;
}
return 0;
}
/* --- test cases ------------------------------------------------------------ */
#define PASS_OR_RET(label) do { \
fprintf(stdout, " [PASS] %s\n", label); \
return 1; \
} while (0)
#define FAIL(...) do { \
fprintf(stderr, " [FAIL] "); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
goto fail; \
} while (0)
/* TEST_CASE("parse-error", "[parse]") */
static int t_parse_error(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
static const uint8_t bad[] = "bora";
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
err = tg3_parse(&m, &e, bad, (uint64_t)(sizeof(bad) - 1), "", 0, &opts);
if (err == TG3_OK) FAIL("expected parse failure on garbage input");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("parse-error");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("datauri-in-glb", "[issue-79]") */
static int t_datauri_in_glb(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts, "../models/box01.glb", &err)) goto fail;
if (err != TG3_OK) FAIL("box01.glb load failed err=%d", (int)err);
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("datauri-in-glb");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("extension-with-empty-object", "[issue-97]") */
static int t_extension_empty_object(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts, "../models/Extensions-issue97/test.gltf", &err)) goto fail;
if (err != TG3_OK) FAIL("load failed err=%d", (int)err);
if (m.extensions_used_count != 1)
FAIL("extensionsUsed.size %u != 1", m.extensions_used_count);
if (!tg3_str_eq(&m.extensions_used[0], "VENDOR_material_some_ext"))
FAIL("unexpected extensionsUsed[0]");
if (m.materials_count != 1) FAIL("materials.size %u != 1", m.materials_count);
if (!has_extension(&m.materials[0].ext, "VENDOR_material_some_ext"))
FAIL("material missing VENDOR_material_some_ext extension");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("extension-with-empty-object");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("extension-overwrite", "[issue-261]") */
static int t_extension_overwrite(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
uint32_t i;
int has_lights = 0;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/Extensions-overwrite-issue261/issue-261.gltf",
&err)) goto fail;
if (err != TG3_OK) FAIL("load failed err=%d", (int)err);
if (m.extensions_used_count != 3)
FAIL("extensionsUsed.size %u != 3", m.extensions_used_count);
for (i = 0; i < m.extensions_used_count; ++i) {
if (tg3_str_eq(&m.extensions_used[i], "KHR_lights_punctual")) {
has_lights = 1; break;
}
}
if (!has_lights) FAIL("KHR_lights_punctual missing in extensionsUsed");
if (!has_extension(&m.ext, "NV_MDL")) FAIL("model.extensions missing NV_MDL");
if (!has_extension(&m.ext, "KHR_lights_punctual"))
FAIL("model.extensions missing KHR_lights_punctual");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("extension-overwrite");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("invalid-primitive-indices", "[bounds-checking]") */
static int t_invalid_primitive_indices(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/BoundsChecking/invalid-primitive-indices.gltf",
&err)) goto fail;
if (err == TG3_OK)
FAIL("invalid-primitive-indices unexpectedly succeeded");
/* v3 reports TG3_ERR_INVALID_INDEX from validate_indices. v1 reports
* "primitive indices accessor out of bounds" at parse time; either is
* acceptable as long as the parser does not crash and rejects the model. */
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("invalid-primitive-indices");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("invalid-buffer-view-index", "[bounds-checking]") */
static int t_invalid_buffer_view_index(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/BoundsChecking/invalid-buffer-view-index.gltf",
&err)) goto fail;
if (err == TG3_OK) FAIL("invalid-buffer-view-index unexpectedly succeeded");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("invalid-buffer-view-index");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("invalid-buffer-index", "[bounds-checking]") */
static int t_invalid_buffer_index(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/BoundsChecking/invalid-buffer-index.gltf",
&err)) goto fail;
if (err == TG3_OK) FAIL("invalid-buffer-index unexpectedly succeeded");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("invalid-buffer-index");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("glb-invalid-length", "[bounds-checking]") */
static int t_glb_invalid_length(void) {
/* 'glTF' magic, version=2, length=0x0000666c (way larger than provided
* data), JSON chunk with empty {}. */
static const unsigned char glb[] = "glTF"
"\x02\x00\x00\x00" "\x6c\x66\x00\x00"
"\x02\x00\x00\x00" "\x4a\x53\x4f\x4e{}";
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
err = tg3_parse_glb(&m, &e, glb, (uint64_t)(sizeof(glb) - 1), "", 0, &opts);
if (err == TG3_OK) FAIL("invalid-length GLB unexpectedly accepted");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("glb-invalid-length");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("integer-out-of-bounds", "[bounds-checking]") */
static int t_integer_out_of_bounds(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/BoundsChecking/integer-out-of-bounds.gltf",
&err)) goto fail;
if (err == TG3_OK) FAIL("integer-out-of-bounds unexpectedly succeeded");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("integer-out-of-bounds");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("pbr-khr-texture-transform", "[material]") */
static int t_pbr_khr_texture_transform(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
const tg3_extension *ext;
const tg3_value *scale, *s0, *s1;
double v0, v1;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/Cube-texture-ext/Cube-textransform.gltf",
&err)) goto fail;
if (err != TG3_OK) FAIL("load failed err=%d", (int)err);
if (m.materials_count != 2) FAIL("materials.size %u != 2", m.materials_count);
ext = find_extension(&m.materials[0].emissive_texture.ext,
"KHR_texture_transform");
if (!ext) FAIL("KHR_texture_transform missing on emissiveTexture");
if (ext->value.type != TG3_VALUE_OBJECT)
FAIL("KHR_texture_transform value not an object");
scale = value_get(&ext->value, "scale");
if (!scale || scale->type != TG3_VALUE_ARRAY || scale->array_count != 2)
FAIL("scale not an array of 2");
s0 = &scale->array_data[0];
s1 = &scale->array_data[1];
v0 = (s0->type == TG3_VALUE_INT) ? (double)s0->int_val : s0->real_val;
v1 = (s1->type == TG3_VALUE_INT) ? (double)s1->int_val : s1->real_val;
if (v0 != 1.0) FAIL("scale[0] %g != 1.0", v0);
if (v1 != -1.0) FAIL("scale[1] %g != -1.0", v1);
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("pbr-khr-texture-transform");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("image-uri-spaces", "[issue-236]") */
static int t_image_uri_spaces(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
/* Single-spaces variant. */
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/CubeImageUriSpaces/CubeImageUriSpaces.gltf",
&err)) goto fail;
if (err != TG3_OK) FAIL("CubeImageUriSpaces load failed err=%d", (int)err);
if (m.images_count != 1) FAIL("images.size %u != 1", m.images_count);
if (!tg3_str_contains(&m.images[0].uri, ' '))
FAIL("image.uri does not contain a space");
tg3_model_free(&m); tg3_error_stack_free(&e);
/* Multiple-spaces variant. */
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/CubeImageUriSpaces/CubeImageUriMultipleSpaces.gltf",
&err)) goto fail2;
if (err != TG3_OK) FAIL("MultipleSpaces load failed err=%d", (int)err);
if (m.images_count != 1) FAIL("images.size %u != 1", m.images_count);
if (m.images[0].uri.len < 2 || m.images[0].uri.data[0] != ' ')
FAIL("image.uri does not start with a space");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("image-uri-spaces");
fail2:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("empty-skeleton-id", "[issue-321]") */
static int t_empty_skeleton_id(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/regression/unassigned-skeleton.gltf",
&err)) goto fail;
if (err != TG3_OK) FAIL("load failed err=%d", (int)err);
if (m.skins_count != 1) FAIL("skins.size %u != 1", m.skins_count);
if (m.skins[0].skeleton != -1)
FAIL("skin.skeleton %d != -1", m.skins[0].skeleton);
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("empty-skeleton-id");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("filesize-check", "[issue-416]") */
static int t_filesize_check(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
opts.max_external_file_size = 10; /* 10 bytes — texture image will exceed */
if (!parse_path(&m, &e, &opts, "../models/Cube/Cube.gltf", &err)) goto fail;
if (err == TG3_OK)
FAIL("expected load failure due to oversized external file");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("filesize-check");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("load-issue-416-model", "[issue-416]") */
static int t_load_issue_416_model(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts, "issue-416.gltf", &err)) goto fail;
/* External file is missing, but the parser should still accept the glTF
* structurally. v1 returns true; v3 returns TG3_OK or a non-fatal error
* about the missing file. */
(void)err; /* tolerate either outcome */
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("load-issue-416-model");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("zero-sized-bin-chunk-glb", "[issue-440]") */
static int t_zero_sized_bin_chunk_glb(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/regression/zero-sized-bin-chunk-issue-440.glb",
&err)) goto fail;
if (err != TG3_OK) FAIL("zero-sized-bin-chunk failed err=%d", (int)err);
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("zero-sized-bin-chunk-glb");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("images-as-is", "[issue-487]") */
static int t_images_as_is(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
uint32_t i;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
opts.images_as_is = 1;
if (!parse_path(&m, &e, &opts, "../models/Cube/Cube.gltf", &err)) goto fail;
if (err != TG3_OK) FAIL("Cube load failed err=%d", (int)err);
if (m.images_count == 0) FAIL("no images parsed");
for (i = 0; i < m.images_count; ++i) {
if (m.images[i].as_is != 1) FAIL("image[%u].as_is != 1", i);
if (m.images[i].uri.len == 0) FAIL("image[%u].uri empty", i);
}
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("images-as-is");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("inverse-bind-matrices-optional", "[issue-492]") */
static int t_ibm_optional(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts, "issue-492.glb", &err)) goto fail;
if (err != TG3_OK) FAIL("issue-492.glb load failed err=%d", (int)err);
/* No "error" entries (warnings are not errors). */
if (errstack_contains(&e, "error"))
FAIL("unexpected error message present");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("inverse-bind-matrices-optional");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("default-material", "[issue-459]") — black-box parse of a
* minimal model with a default material; verify v3 populates the same
* default-PBR field values v1 documents. */
static int t_default_material(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},\"materials\":[{}]}";
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
const tg3_material *mat;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
err = tg3_parse(&m, &e, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_OK) FAIL("minimal default-material parse failed err=%d", (int)err);
if (m.materials_count != 1) FAIL("materials.size %u != 1", m.materials_count);
mat = &m.materials[0];
if (!tg3_str_eq(&mat->alpha_mode, "OPAQUE")) FAIL("alpha_mode default mismatch");
if (mat->alpha_cutoff != 0.5) FAIL("alpha_cutoff default %g != 0.5", mat->alpha_cutoff);
if (mat->double_sided != 0) FAIL("double_sided default != 0");
if (mat->emissive_factor[0] != 0.0 ||
mat->emissive_factor[1] != 0.0 ||
mat->emissive_factor[2] != 0.0) FAIL("emissive_factor default mismatch");
if (mat->pbr_metallic_roughness.base_color_factor[0] != 1.0 ||
mat->pbr_metallic_roughness.base_color_factor[1] != 1.0 ||
mat->pbr_metallic_roughness.base_color_factor[2] != 1.0 ||
mat->pbr_metallic_roughness.base_color_factor[3] != 1.0)
FAIL("base_color_factor default mismatch");
if (mat->pbr_metallic_roughness.metallic_factor != 1.0)
FAIL("metallic_factor default %g != 1.0",
mat->pbr_metallic_roughness.metallic_factor);
if (mat->pbr_metallic_roughness.roughness_factor != 1.0)
FAIL("roughness_factor default %g != 1.0",
mat->pbr_metallic_roughness.roughness_factor);
if (mat->normal_texture.index != -1) FAIL("normal_texture.index != -1");
if (mat->occlusion_texture.index != -1) FAIL("occlusion_texture.index != -1");
if (mat->emissive_texture.index != -1) FAIL("emissive_texture.index != -1");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("default-material");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* --- main ------------------------------------------------------------------ */
int main(void) {
struct { const char *name; int (*fn)(void); } cases[] = {
{"parse-error", t_parse_error},
{"datauri-in-glb", t_datauri_in_glb},
{"extension-with-empty-object", t_extension_empty_object},
{"extension-overwrite", t_extension_overwrite},
{"invalid-primitive-indices", t_invalid_primitive_indices},
{"invalid-buffer-view-index", t_invalid_buffer_view_index},
{"invalid-buffer-index", t_invalid_buffer_index},
{"glb-invalid-length", t_glb_invalid_length},
{"integer-out-of-bounds", t_integer_out_of_bounds},
{"pbr-khr-texture-transform", t_pbr_khr_texture_transform},
{"image-uri-spaces", t_image_uri_spaces},
{"empty-skeleton-id", t_empty_skeleton_id},
{"filesize-check", t_filesize_check},
{"load-issue-416-model", t_load_issue_416_model},
{"zero-sized-bin-chunk-glb", t_zero_sized_bin_chunk_glb},
{"images-as-is", t_images_as_is},
{"inverse-bind-matrices-optional", t_ibm_optional},
{"default-material", t_default_material},
};
size_t total = sizeof(cases) / sizeof(cases[0]);
size_t passed = 0;
size_t i;
for (i = 0; i < total; ++i) {
fprintf(stdout, "RUN %s\n", cases[i].name);
if (cases[i].fn()) ++passed;
}
fprintf(stdout, "\n=== v1 port: %zu/%zu passed ===\n", passed, total);
return passed == total ? 0 : 1;
}

View File

@@ -1,83 +0,0 @@
# tests/v3/fuzzer/Makefile — Build libFuzzer harnesses for tinygltf v3
#
# Requires: clang/clang++ with libFuzzer support
#
# Targets:
# make — build both harnesses with ASan + UBSan
# make run — run the dedicated pure-C v3 harness
# make run-cpp — run the legacy header-implementation harness
# make seed — generate seed corpus from test models
# make clean — remove binaries and corpus
CC = clang
CXX = clang++
CCFLAGS = -g -O1 -std=c11
CXXFLAGS = -g -O1 -std=c++17 -fno-rtti -fno-exceptions
SANITIZE = -fsanitize=fuzzer,address,undefined
INCLUDES = -I../../..
FUZZER = fuzz_gltf_v3
FUZZER_C = fuzz_gltf_v3_c
CORPUS = corpus
ARTIFACTS = artifacts
# Fuzzer runtime options
MAX_LEN ?= 65536
JOBS ?= $(shell nproc 2>/dev/null || echo 4)
MAX_TIME ?= 0
FUZZ_ENV ?= LSAN_OPTIONS=detect_leaks=0
.PHONY: all run run-cpp seed clean
all: $(FUZZER) $(FUZZER_C)
$(FUZZER): fuzz_gltf_v3.cc ../../../tiny_gltf_v3.h ../../../tiny_gltf_v3.c ../../../tinygltf_json_c.h
$(CXX) $(CXXFLAGS) $(SANITIZE) $(INCLUDES) -o $@ $<
$(FUZZER_C): fuzz_gltf_v3_c.c ../../../tiny_gltf_v3.h ../../../tiny_gltf_v3.c ../../../tinygltf_json_c.h
$(CC) $(CCFLAGS) $(SANITIZE) $(INCLUDES) -o $@ $< ../../../tiny_gltf_v3.c
run: $(FUZZER_C) | $(CORPUS) $(ARTIFACTS)
$(FUZZ_ENV) ./$(FUZZER_C) $(CORPUS) \
-artifact_prefix=$(ARTIFACTS)/ \
-max_len=$(MAX_LEN) \
-jobs=$(JOBS) \
-workers=$(JOBS) \
$(if $(filter-out 0,$(MAX_TIME)),-max_total_time=$(MAX_TIME))
run-cpp: $(FUZZER) | $(CORPUS) $(ARTIFACTS)
$(FUZZ_ENV) ./$(FUZZER) $(CORPUS) \
-artifact_prefix=$(ARTIFACTS)/ \
-max_len=$(MAX_LEN) \
-jobs=$(JOBS) \
-workers=$(JOBS) \
$(if $(filter-out 0,$(MAX_TIME)),-max_total_time=$(MAX_TIME))
# Generate seed corpus from existing test models
seed: | $(CORPUS)
@echo "Seeding corpus from test models..."
@for f in ../../../models/Cube/Cube.gltf \
../../../models/Cube/Cube.glb; do \
if [ -f "$$f" ]; then \
cp "$$f" $(CORPUS)/; \
echo " Added: $$f"; \
fi; \
done
@# Add a minimal valid glTF JSON
@echo '{"asset":{"version":"2.0"},"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"name":"n"}]}' > $(CORPUS)/minimal.gltf
@# Add a minimal valid GLB (header + empty JSON chunk)
@printf 'glTF\x02\x00\x00\x00\x1c\x00\x00\x00\x04\x00\x00\x00JSON{} ' > $(CORPUS)/minimal.glb
@# Add edge cases
@echo '{}' > $(CORPUS)/empty_object.gltf
@echo '{"asset":{"version":"2.0"}}' > $(CORPUS)/asset_only.gltf
@echo "Corpus: $$(ls $(CORPUS) | wc -l) files"
$(CORPUS):
mkdir -p $(CORPUS)
$(ARTIFACTS):
mkdir -p $(ARTIFACTS)
clean:
rm -f $(FUZZER) $(FUZZER_C)
rm -rf $(CORPUS) $(ARTIFACTS)

View File

@@ -1,110 +0,0 @@
/*
* fuzz_gltf_v3.cc — libFuzzer harness for tinygltf v3 parser.
*
* Fuzz targets:
* - Auto-detect (GLB or JSON) parse from arbitrary bytes
* - Exercises JSON parser, GLB header parsing, arena allocator,
* error stack, and all glTF entity parsing paths.
*
* Build (clang with libFuzzer):
* clang++ -g -O1 -fsanitize=fuzzer,address,undefined \
* -std=c++17 -fno-rtti -fno-exceptions \
* -I../../.. -o fuzz_gltf_v3 fuzz_gltf_v3.cc
*
* Run:
* ./fuzz_gltf_v3 corpus/ -max_len=65536
*
* Seed corpus: place valid .gltf and .glb files in corpus/
*/
#define TINYGLTF3_IMPLEMENTATION
#include "tiny_gltf_v3.h"
#include <cstdint>
#include <cstddef>
/* Memory budget to prevent OOM during fuzzing */
static const uint64_t FUZZ_MEMORY_BUDGET = 64ULL * 1024 * 1024; /* 64 MB */
static void fuzz_parse_auto(const uint8_t *data, size_t size) {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
tg3_parse_options opts;
tg3_parse_options_init(&opts);
opts.memory.memory_budget = FUZZ_MEMORY_BUDGET;
tg3_parse_auto(&model, &errors, data, (uint64_t)size,
"", 0, &opts);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
static void fuzz_parse_json(const uint8_t *data, size_t size) {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
tg3_parse_options opts;
tg3_parse_options_init(&opts);
opts.memory.memory_budget = FUZZ_MEMORY_BUDGET;
tg3_parse(&model, &errors, data, (uint64_t)size,
"", 0, &opts);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
static void fuzz_parse_glb(const uint8_t *data, size_t size) {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
tg3_parse_options opts;
tg3_parse_options_init(&opts);
opts.memory.memory_budget = FUZZ_MEMORY_BUDGET;
tg3_parse_glb(&model, &errors, data, (uint64_t)size,
"", 0, &opts);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
static void fuzz_parse_float32(const uint8_t *data, size_t size) {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
tg3_parse_options opts;
tg3_parse_options_init(&opts);
opts.memory.memory_budget = FUZZ_MEMORY_BUDGET;
opts.parse_float32 = 1;
tg3_parse_auto(&model, &errors, data, (uint64_t)size,
"", 0, &opts);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size == 0) return 0;
/* Use first byte to select parse path, rest is the payload */
uint8_t selector = data[0] % 4;
const uint8_t *payload = data + 1;
size_t payload_size = size - 1;
switch (selector) {
case 0: fuzz_parse_auto(payload, payload_size); break;
case 1: fuzz_parse_json(payload, payload_size); break;
case 2: fuzz_parse_glb(payload, payload_size); break;
case 3: fuzz_parse_float32(payload, payload_size); break;
}
return 0;
}

View File

@@ -1,39 +0,0 @@
#include "tiny_gltf_v3.h"
#include <stddef.h>
#include <stdint.h>
static const uint64_t FUZZ_MEMORY_BUDGET = 64ULL * 1024 * 1024;
typedef tg3_error_code (*tg3_fuzz_parse_fn)(tg3_model *, tg3_error_stack *,
const uint8_t *, uint64_t, const char *, uint32_t, const tg3_parse_options *);
static void tg3_fuzz_run(tg3_fuzz_parse_fn fn, int parse_float32,
const uint8_t *data, size_t size) {
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
opts.memory.memory_budget = FUZZ_MEMORY_BUDGET;
opts.parse_float32 = parse_float32;
fn(&model, &errors, data, (uint64_t)size, "", 0, &opts);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size == 0) return 0;
switch (data[0] % 4) {
case 0: tg3_fuzz_run(tg3_parse_auto, 0, data + 1, size - 1); break;
case 1: tg3_fuzz_run(tg3_parse, 0, data + 1, size - 1); break;
case 2: tg3_fuzz_run(tg3_parse_glb, 0, data + 1, size - 1); break;
case 3: tg3_fuzz_run(tg3_parse_auto, 1, data + 1, size - 1); break;
}
return 0;
}

View File

@@ -1,8 +0,0 @@
{
"asset": {"version": "2.0"},
"buffers": [{"byteLength": 4}],
"bufferViews": [
{"buffer": 0, "byteOffset": 0, "byteLength": 4, "byteStride": -1}
],
"scenes": [{"nodes": []}]
}

View File

@@ -1,9 +0,0 @@
{
"asset": {"version": "2.0"},
"buffers": [{"byteLength": 4}],
"bufferViews": [{"buffer": 0, "byteOffset": 0, "byteLength": 4}],
"accessors": [
{"bufferView": 1000000, "byteOffset": 0, "componentType": 5121, "count": 1, "type": "SCALAR"}
],
"scenes": [{"nodes": []}]
}

View File

@@ -1,19 +0,0 @@
{
"asset": {"version": "2.0"},
"nodes": [
{"extensions": {"KHR_audio": {"emitter": 2147483647},
"MSFT_lod": {"ids": [-1, 9999]}}}
],
"materials": [
{"extensions": {"MSFT_lod": {"ids": [-5]}}}
],
"scenes": [
{"extensions": {"KHR_audio": {"emitters": [12345]}}}
],
"extensions": {
"KHR_audio": {
"sources": [{"bufferView": -1}],
"emitters": [{"source": 99999}]
}
}
}

View File

@@ -1,14 +0,0 @@
{
"asset": {"version": "2.0"},
"buffers": [
{"uri": "../../../../../../../../tmp/tg3-poc-secret.txt", "byteLength": 32}
],
"bufferViews": [
{"buffer": 0, "byteOffset": 0, "byteLength": 32}
],
"accessors": [
{"bufferView": 0, "byteOffset": 0, "componentType": 5121, "count": 32, "type": "SCALAR"}
],
"scenes": [{"nodes": []}],
"scene": 0
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff