From 8570e29ff8929765b9fcbee6572cab9213cc3875 Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Fri, 22 May 2026 04:15:41 +0900 Subject: [PATCH 1/6] Add optimized parser with multithreading, SIMD, and custom allocator support Squashed take-over of #424 (branch copilot/optimize-parser-for-huge-meshes) so CI runs on a maintainer-pushed branch. Highlights: - New optimized OBJ loader (LoadObjOpt / LoadObjOptTyped) targeting huge meshes, with fast_float-based ASCII float/double parsing. - Optional compile-time features: TINYOBJLOADER_USE_MULTITHREADING (threaded parse + merge), TINYOBJLOADER_USE_SIMD (SSE2/AVX2/NEON newline scanning), and TINYOBJLOADER_ENABLE_EXCEPTION (opt-in C++ exceptions). - ArenaAllocator / arena_adapter for bulk allocation and a TypedArray API. - Experimental stream-based OBJ loader under experimental/stream/. - Fuzzing harnesses (tests/obj-fuzz, tests/llvm-fuzz, tests/fuzz_common.h) and extensive parity tests against the legacy loader (tests/opt/loadobjopt_multithread.inc, tester.cc). Original work by Copilot coding agent; reviewed and validated locally (make check passes under clang++ with -std=c++11 -fsanitize=address). Co-Authored-By: Copilot <198982749+Copilot@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 (1M context) --- experimental/README.md | 1 + experimental/stream/README.md | 52 + experimental/stream/stream_obj_loader.cc | 1300 ++++++ experimental/stream/stream_obj_loader.h | 69 + loader_example.cc | 4 +- tests/Makefile | 13 +- tests/README.md | 6 +- tests/fuzz_common.h | 331 ++ tests/llvm-fuzz/Makefile | 19 + tests/llvm-fuzz/README.md | 46 + tests/llvm-fuzz/corpus/seed_basic.obj | 12 + tests/llvm-fuzz/fuzz_loaders.cc | 105 + tests/obj-fuzz/Makefile | 19 + tests/obj-fuzz/README.md | 47 + tests/obj-fuzz/obj_fuzz.cc | 395 ++ tests/opt/loadobjopt_multithread.inc | 3043 ++++++++++++++ tests/tester.cc | 1036 ++++- tiny_obj_loader.h | 4644 +++++++++++++++++++++- 18 files changed, 11120 insertions(+), 22 deletions(-) create mode 100644 experimental/stream/README.md create mode 100644 experimental/stream/stream_obj_loader.cc create mode 100644 experimental/stream/stream_obj_loader.h create mode 100644 tests/fuzz_common.h create mode 100644 tests/llvm-fuzz/Makefile create mode 100644 tests/llvm-fuzz/README.md create mode 100644 tests/llvm-fuzz/corpus/seed_basic.obj create mode 100644 tests/llvm-fuzz/fuzz_loaders.cc create mode 100644 tests/obj-fuzz/Makefile create mode 100644 tests/obj-fuzz/README.md create mode 100644 tests/obj-fuzz/obj_fuzz.cc create mode 100644 tests/opt/loadobjopt_multithread.inc diff --git a/experimental/README.md b/experimental/README.md index a0603af9..edd96cdf 100644 --- a/experimental/README.md +++ b/experimental/README.md @@ -1,6 +1,7 @@ # Experimental code for .obj loader. * Multi-threaded optimized parser : tinyobj_loader_opt.h +* Streaming experimental parser : stream/stream_obj_loader.h ## Requirements diff --git a/experimental/stream/README.md b/experimental/stream/README.md new file mode 100644 index 00000000..eb9b336f --- /dev/null +++ b/experimental/stream/README.md @@ -0,0 +1,52 @@ +# Experimental Stream OBJ Parser + +This directory contains an experimental line-by-line OBJ parser intended to +reduce peak input buffering compared with the whole-buffer optimized parser. + +The design is split into two layers: + +- `StreamHandler`: callback-style incremental parser interface. +- `LoadObjStreamExperimental(...)`: convenience wrapper that builds + `tinyobj::attrib_t`, `tinyobj::shape_t`, and `tinyobj::material_t`. +- Ordered multithreaded chunk mode: read bounded batches of lines, parse those + chunks in parallel, then replay parsed events in original order. + +## Goals + +- Parse OBJ from `std::istream` without reading the whole file into one buffer. +- Preserve support for relative face indices. +- Allow applications to consume faces incrementally without materializing a + full mesh. + +## Current Scope + +Implemented records: + +- `v` +- `vn` +- `vt` +- `f` +- `g` +- `o` +- `usemtl` +- `mtllib` +- `s` + +Ignored for now: + +- `l` +- `p` +- free-form curves/surfaces +- tags and skinning extensions +- advanced vertex color fallback behavior matching the legacy loader exactly + +## Notes + +This parser is intentionally separate from `LoadObjOpt`. +`LoadObjOpt` is built around random-access whole-buffer processing and +multithreaded partitioning, while this module is focused on low intermediate +buffer usage and incremental consumption. + +The current multithreaded design uses a bounded in-flight chunk window rather +than a true LRU chunk cache. That matches OBJ's mostly sequential access +pattern better and keeps ordering/state management simple. diff --git a/experimental/stream/stream_obj_loader.cc b/experimental/stream/stream_obj_loader.cc new file mode 100644 index 00000000..7b4495b2 --- /dev/null +++ b/experimental/stream/stream_obj_loader.cc @@ -0,0 +1,1300 @@ +#include "stream_obj_loader.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tinyobj { +namespace experimental_stream { +namespace { + +struct RawIndex { + int vertex_index; + int texcoord_index; + int normal_index; + bool has_texcoord_index; + bool has_normal_index; + + RawIndex() + : vertex_index(0), + texcoord_index(0), + normal_index(0), + has_texcoord_index(false), + has_normal_index(false) {} +}; + +enum ParsedEventType { + EVENT_VERTEX, + EVENT_NORMAL, + EVENT_TEXCOORD, + EVENT_FACE, + EVENT_GROUP, + EVENT_OBJECT, + EVENT_USEMTL, + EVENT_MTLLIB, + EVENT_SMOOTHING, + EVENT_WARNING +}; + +struct ParsedEvent { + ParsedEventType type; + size_t line_num; + real_t x, y, z; + real_t vertex_weight; + real_t r, g, b; + real_t w; + bool has_vertex_weight; + bool has_color; + bool has_texcoord_w; + std::vector face; + std::vector filenames; + std::string text; + unsigned int smoothing_group_id; + + ParsedEvent() + : type(EVENT_WARNING), + line_num(0), + x(real_t(0)), + y(real_t(0)), + z(real_t(0)), + vertex_weight(real_t(1)), + r(real_t(1)), + g(real_t(1)), + b(real_t(1)), + w(real_t(0)), + has_vertex_weight(false), + has_color(false), + has_texcoord_w(false), + smoothing_group_id(0) {} +}; + +struct ParsedChunk { + std::vector events; + std::string err; +}; + +static std::string Trim(const std::string &s) { + size_t begin = 0; + while (begin < s.size() && + (s[begin] == ' ' || s[begin] == '\t' || s[begin] == '\r')) { + begin++; + } + + size_t end = s.size(); + while (end > begin && + (s[end - 1] == ' ' || s[end - 1] == '\t' || s[end - 1] == '\r')) { + end--; + } + + return s.substr(begin, end - begin); +} + +static std::string TrimLeading(const std::string &s) { + size_t begin = 0; + while (begin < s.size() && + (s[begin] == ' ' || s[begin] == '\t' || s[begin] == '\r')) { + begin++; + } + return s.substr(begin); +} + +static bool ParseRealToken(const std::string &token, real_t *value) { + if (!value) return false; + + char *end = NULL; + errno = 0; + double v = std::strtod(token.c_str(), &end); + if (end == token.c_str() || (end && *end != '\0') || errno == ERANGE) { + return false; + } + + *value = static_cast(v); + return true; +} + +static bool ParseIntToken(const std::string &token, int *value) { + if (!value) return false; + + char *end = NULL; + errno = 0; + long v = std::strtol(token.c_str(), &end, 10); + if (end == token.c_str() || (end && *end != '\0') || errno == ERANGE) { + return false; + } + if (v < static_cast(std::numeric_limits::min()) || + v > static_cast(std::numeric_limits::max())) { + return false; + } + + *value = static_cast(v); + return true; +} + +template +static int PointInPolygon(int nvert, T *vertx, T *verty, T testx, T testy) { + int c = 0; + for (int i = 0, j = nvert - 1; i < nvert; j = i++) { + if (((verty[i] > testy) != (verty[j] > testy)) && + (testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / + (verty[j] - verty[i]) + + vertx[i])) { + c = !c; + } + } + return c; +} + +static void AppendZeroIndexWarning(std::string *warn, + const std::string &source_name, + size_t line_num) { + if (!warn) return; + + std::stringstream ss; + ss << source_name << ":" << line_num + << ": warning: zero value index found (will have a value of -1 for " + "normal and tex indices)\n"; + (*warn) += ss.str(); +} + +static bool ResolveIndexLikeLegacy(int idx, int n, int *ret, bool allow_zero, + const std::string &source_name, + size_t line_num, std::string *warn) { + if (!ret) return false; + if (idx > 0) { + (*ret) = idx - 1; + return true; + } + if (idx == 0) { + AppendZeroIndexWarning(warn, source_name, line_num); + (*ret) = -1; + return allow_zero; + } + + (*ret) = n + idx; + return ((*ret) >= 0); +} + +static void UpdateGreatestIndex(int idx, int *greatest) { + if (!greatest) return; + if (idx > *greatest) { + *greatest = idx; + } +} + +static void AppendOutOfBoundsWarnings(std::string *warn, + int greatest_v_idx, + int greatest_vn_idx, + int greatest_vt_idx, + int num_vertices, + int num_normals, + int num_texcoords, + size_t line_num) { + if (!warn) return; + + if (greatest_v_idx >= num_vertices) { + std::stringstream ss; + ss << "Vertex indices out of bounds (line " << line_num << ".)\n\n"; + (*warn) += ss.str(); + } + if (greatest_vn_idx >= num_normals) { + std::stringstream ss; + ss << "Vertex normal indices out of bounds (line " << line_num << ".)\n\n"; + (*warn) += ss.str(); + } + if (greatest_vt_idx >= num_texcoords) { + std::stringstream ss; + ss << "Vertex texcoord indices out of bounds (line " << line_num + << ".)\n\n"; + (*warn) += ss.str(); + } +} + +static bool IsValidFaceVertex(const std::vector &vertices, + const index_t &idx) { + if (idx.vertex_index < 0) return false; + const size_t vi = static_cast(idx.vertex_index); + return ((3 * vi + 2) < vertices.size()); +} + +static size_t TriangulateFaceLikeLegacy(const std::vector &vertices, + const index_t *face, size_t face_count, + index_t *dst) { + if (face_count < 3) return 0; + if (face_count == 3) { + dst[0] = face[0]; + dst[1] = face[1]; + dst[2] = face[2]; + return 3; + } + + for (size_t i = 0; i < face_count; i++) { + if (!IsValidFaceVertex(vertices, face[i])) { + return 0; + } + } + + if (face_count == 4) { + const size_t vi0 = static_cast(face[0].vertex_index); + const size_t vi1 = static_cast(face[1].vertex_index); + const size_t vi2 = static_cast(face[2].vertex_index); + const size_t vi3 = static_cast(face[3].vertex_index); + const real_t v0x = vertices[vi0 * 3 + 0]; + const real_t v0y = vertices[vi0 * 3 + 1]; + const real_t v0z = vertices[vi0 * 3 + 2]; + const real_t v1x = vertices[vi1 * 3 + 0]; + const real_t v1y = vertices[vi1 * 3 + 1]; + const real_t v1z = vertices[vi1 * 3 + 2]; + const real_t v2x = vertices[vi2 * 3 + 0]; + const real_t v2y = vertices[vi2 * 3 + 1]; + const real_t v2z = vertices[vi2 * 3 + 2]; + const real_t v3x = vertices[vi3 * 3 + 0]; + const real_t v3y = vertices[vi3 * 3 + 1]; + const real_t v3z = vertices[vi3 * 3 + 2]; + const real_t e02x = v2x - v0x; + const real_t e02y = v2y - v0y; + const real_t e02z = v2z - v0z; + const real_t e13x = v3x - v1x; + const real_t e13y = v3y - v1y; + const real_t e13z = v3z - v1z; + const real_t sqr02 = e02x * e02x + e02y * e02y + e02z * e02z; + const real_t sqr13 = e13x * e13x + e13y * e13y + e13z * e13z; + if (sqr02 < sqr13) { + dst[0] = face[0]; + dst[1] = face[1]; + dst[2] = face[2]; + dst[3] = face[0]; + dst[4] = face[2]; + dst[5] = face[3]; + } else { + dst[0] = face[0]; + dst[1] = face[1]; + dst[2] = face[3]; + dst[3] = face[1]; + dst[4] = face[2]; + dst[5] = face[3]; + } + return 6; + } + + std::vector remaining(face, face + face_count); + size_t axes[2] = {1, 2}; + for (size_t k = 0; k < face_count; ++k) { + const size_t vi0 = static_cast(face[(k + 0) % face_count].vertex_index); + const size_t vi1 = static_cast(face[(k + 1) % face_count].vertex_index); + const size_t vi2 = static_cast(face[(k + 2) % face_count].vertex_index); + const real_t v0x = vertices[vi0 * 3 + 0]; + const real_t v0y = vertices[vi0 * 3 + 1]; + const real_t v0z = vertices[vi0 * 3 + 2]; + const real_t v1x = vertices[vi1 * 3 + 0]; + const real_t v1y = vertices[vi1 * 3 + 1]; + const real_t v1z = vertices[vi1 * 3 + 2]; + const real_t v2x = vertices[vi2 * 3 + 0]; + const real_t v2y = vertices[vi2 * 3 + 1]; + const real_t v2z = vertices[vi2 * 3 + 2]; + const real_t e0x = v1x - v0x; + const real_t e0y = v1y - v0y; + const real_t e0z = v1z - v0z; + const real_t e1x = v2x - v1x; + const real_t e1y = v2y - v1y; + const real_t e1z = v2z - v1z; + const real_t cx = std::fabs(e0y * e1z - e0z * e1y); + const real_t cy = std::fabs(e0z * e1x - e0x * e1z); + const real_t cz = std::fabs(e0x * e1y - e0y * e1x); + const real_t epsilon = std::numeric_limits::epsilon(); + if (cx > epsilon || cy > epsilon || cz > epsilon) { + if (!(cx > cy && cx > cz)) { + axes[0] = 0; + if (cz > cx && cz > cy) { + axes[1] = 1; + } + } + break; + } + } + + size_t out = 0; + size_t guess_vert = 0; + size_t remaining_iterations = remaining.size(); + size_t previous_remaining_vertices = remaining.size(); + while (remaining.size() > 3 && remaining_iterations > 0) { + const size_t npolys = remaining.size(); + if (guess_vert >= npolys) { + guess_vert -= npolys; + } + if (previous_remaining_vertices != npolys) { + previous_remaining_vertices = npolys; + remaining_iterations = npolys; + } else { + remaining_iterations--; + } + + index_t ind[3]; + real_t vx[3]; + real_t vy[3]; + for (size_t k = 0; k < 3; k++) { + ind[k] = remaining[(guess_vert + k) % npolys]; + const size_t vi = static_cast(ind[k].vertex_index); + vx[k] = vertices[vi * 3 + axes[0]]; + vy[k] = vertices[vi * 3 + axes[1]]; + } + + const real_t e0x = vx[1] - vx[0]; + const real_t e0y = vy[1] - vy[0]; + const real_t e1x = vx[2] - vx[1]; + const real_t e1y = vy[2] - vy[1]; + const real_t cross_val = e0x * e1y - e0y * e1x; + const real_t area = + (vx[0] * vy[1] - vy[0] * vx[1]) * static_cast(0.5); + if (cross_val * area < static_cast(0.0)) { + guess_vert += 1; + continue; + } + + bool overlap = false; + for (size_t other_vert = 3; other_vert < npolys; ++other_vert) { + const size_t idx = (guess_vert + other_vert) % npolys; + const size_t ovi = static_cast(remaining[idx].vertex_index); + const real_t tx = vertices[ovi * 3 + axes[0]]; + const real_t ty = vertices[ovi * 3 + axes[1]]; + if (PointInPolygon(3, vx, vy, tx, ty)) { + overlap = true; + break; + } + } + + if (overlap) { + guess_vert += 1; + continue; + } + + dst[out++] = ind[0]; + dst[out++] = ind[1]; + dst[out++] = ind[2]; + remaining.erase(remaining.begin() + + static_cast((guess_vert + 1) % npolys)); + } + + if (remaining.size() == 3) { + dst[out++] = remaining[0]; + dst[out++] = remaining[1]; + dst[out++] = remaining[2]; + } + + return out; +} + +static bool ParseRawTripleToken(const std::string &token, RawIndex *out) { + if (!out) return false; + + out->vertex_index = 0; + out->texcoord_index = 0; + out->normal_index = 0; + + size_t first = token.find('/'); + if (first == std::string::npos) { + return ParseIntToken(token, &out->vertex_index); + } + + std::string v_str = token.substr(0, first); + size_t second = token.find('/', first + 1); + + if (v_str.empty() || !ParseIntToken(v_str, &out->vertex_index)) return false; + + if (second == std::string::npos) { + std::string vt_str = token.substr(first + 1); + if (!vt_str.empty()) { + out->has_texcoord_index = true; + if (!ParseIntToken(vt_str, &out->texcoord_index)) return false; + } + return true; + } + + std::string vt_str = token.substr(first + 1, second - first - 1); + std::string vn_str = token.substr(second + 1); + + if (!vt_str.empty()) { + out->has_texcoord_index = true; + if (!ParseIntToken(vt_str, &out->texcoord_index)) return false; + } + + if (!vn_str.empty()) { + out->has_normal_index = true; + if (!ParseIntToken(vn_str, &out->normal_index)) return false; + } + + return true; +} + +static void SplitFilenames(const std::string &s, + std::vector *filenames) { + if (!filenames) return; + filenames->clear(); + std::string token; + token.reserve(s.size()); + bool escaped = false; + for (size_t i = 0; i < s.size(); i++) { + const char c = s[i]; + if (escaped) { + token.push_back(c); + escaped = false; + continue; + } + if (c == '\\') { + escaped = true; + continue; + } + if (c == ' ' || c == '\t') { + if (!token.empty()) { + filenames->push_back(token); + token.clear(); + } + continue; + } + token.push_back(c); + } + if (escaped) { + token.push_back('\\'); + } + if (!token.empty()) { + filenames->push_back(token); + } +} + +static std::string ParseLegacyGroupName(std::istringstream *iss) { + std::string name; + std::string token; + while ((*iss) >> token) { + if (!token.empty() && token[0] == '#') { + break; + } + if (!name.empty()) { + name.push_back(' '); + } + name += token; + } + return name; +} + +class MeshBuilderHandler : public StreamHandler { + public: + MeshBuilderHandler(attrib_t *attrib, std::vector *shapes, + std::vector *materials, MaterialReader *reader, + std::string *warn, std::string *err, + const StreamLoadConfig &config) + : attrib_(attrib), + shapes_(shapes), + materials_(materials), + material_reader_(reader), + warn_(warn), + err_(err), + config_(config), + current_material_id_(-1), + current_smoothing_group_id_(0), + saw_explicit_color_(false), + saw_missing_color_(false), + current_shape_from_group_(false), + current_shape_has_face_record_(false), + current_shape_degenerate_face_count_(0) { + assert(attrib_); + assert(shapes_); + attrib_->vertices.clear(); + attrib_->vertex_weights.clear(); + attrib_->normals.clear(); + attrib_->texcoords.clear(); + attrib_->texcoord_ws.clear(); + attrib_->colors.clear(); + attrib_->skin_weights.clear(); + shapes_->clear(); + if (materials_) materials_->clear(); + } + + void Finish() { + FlushShape(true); + if (!config_.default_vcols_fallback && saw_explicit_color_ && + saw_missing_color_) { + attrib_->colors.clear(); + } + if (config_.default_vcols_fallback && !saw_explicit_color_ && + attrib_->colors.empty() && !attrib_->vertices.empty()) { + attrib_->colors.assign(attrib_->vertices.size(), real_t(1.0)); + } + } + + virtual void OnVertex(real_t x, real_t y, real_t z, bool has_weight, real_t w, + bool has_color, real_t r, real_t g, real_t b) { + attrib_->vertices.push_back(x); + attrib_->vertices.push_back(y); + attrib_->vertices.push_back(z); + attrib_->vertex_weights.push_back(has_weight ? w : real_t(1.0)); + + if (has_color) { + if (!saw_explicit_color_ && attrib_->colors.empty() && + attrib_->vertices.size() > 3) { + const size_t prior_vertex_count = attrib_->vertices.size() / 3 - 1; + attrib_->colors.assign(prior_vertex_count * 3, real_t(1.0)); + } + saw_explicit_color_ = true; + attrib_->colors.push_back(r); + attrib_->colors.push_back(g); + attrib_->colors.push_back(b); + } else if (saw_explicit_color_) { + saw_missing_color_ = true; + attrib_->colors.push_back(real_t(1.0)); + attrib_->colors.push_back(real_t(1.0)); + attrib_->colors.push_back(real_t(1.0)); + } else { + saw_missing_color_ = true; + } + } + + virtual void OnNormal(real_t x, real_t y, real_t z) { + attrib_->normals.push_back(x); + attrib_->normals.push_back(y); + attrib_->normals.push_back(z); + } + + virtual void OnTexcoord(real_t u, real_t v, bool has_w, real_t w) { + attrib_->texcoords.push_back(u); + attrib_->texcoords.push_back(v); + attrib_->texcoord_ws.push_back(has_w ? w : real_t(0.0)); + } + + virtual void OnFace(const index_t *indices, size_t num_indices) { + current_shape_has_face_record_ = true; + for (size_t i = 0; i < num_indices; i++) { + current_shape_.mesh.indices.push_back(indices[i]); + } + current_shape_.mesh.num_face_vertices.push_back( + static_cast(num_indices)); + current_shape_.mesh.material_ids.push_back(current_material_id_); + current_shape_.mesh.smoothing_group_ids.push_back( + current_smoothing_group_id_); + } + + virtual void OnDegenerateFace() { + current_shape_has_face_record_ = true; + current_shape_degenerate_face_count_++; + } + + virtual void OnGroup(const std::string &name) { + SwitchShape(name, true); + } + + virtual void OnObject(const std::string &name) { + SwitchShape(name, false); + } + + virtual void OnUsemtl(const std::string &name) { + current_material_name_ = name; + std::map::const_iterator it = material_map_.find(name); + if (it != material_map_.end()) { + current_material_id_ = it->second; + } else { + current_material_id_ = -1; + if (warn_) { + (*warn_) += "material [ '" + name + "' ] not found in .mtl\n"; + } + } + } + + virtual void OnMtllib(const std::vector &filenames) { + HandleMtllib(filenames, 0); + } + + virtual void OnMtllibWithLine(const std::vector &filenames, + size_t line_num) { + HandleMtllib(filenames, line_num); + } + + private: + void HandleMtllib(const std::vector &filenames, + size_t line_num) { + if (!material_reader_) return; + + std::vector *material_dst = + materials_ ? materials_ : &scratch_materials_; + + if (filenames.empty()) { + if (warn_) { + if (line_num != 0) { + std::stringstream ss; + ss << "Looks like empty filename for mtllib. Use default material " + "(line " + << line_num << ".)\n"; + (*warn_) += ss.str(); + } else { + (*warn_) += + "Looks like empty filename for mtllib. Use default material.\n"; + } + } + return; + } + + bool found = false; + for (size_t i = 0; i < filenames.size(); i++) { + if (loaded_material_filenames_.count(filenames[i]) > 0) { + found = true; + continue; + } + + std::string warn_mtl; + std::string err_mtl; + bool ok = (*material_reader_)(filenames[i], material_dst, &material_map_, + &warn_mtl, &err_mtl); + if (warn_ && !warn_mtl.empty()) { + (*warn_) += warn_mtl; + } + if (err_ && !err_mtl.empty()) { + (*err_) += err_mtl; + } + + if (ok) { + found = true; + loaded_material_filenames_.insert(filenames[i]); + break; + } + } + + if (!found && warn_) { + (*warn_) += "Failed to load material file(s). Use default material.\n"; + } + } + + public: + virtual void OnSmoothingGroup(unsigned int smoothing_group_id) { + current_smoothing_group_id_ = smoothing_group_id; + } + + private: + void SwitchShape(const std::string &name, bool from_group) { + if (!current_shape_.mesh.indices.empty() || current_shape_has_face_record_) { + FlushShape(false); + } else { + current_shape_ = shape_t(); + current_shape_has_face_record_ = false; + current_shape_degenerate_face_count_ = 0; + } + current_shape_.name = name; + current_shape_from_group_ = from_group; + } + + void FlushShape(bool at_eof) { + EmitDegenerateFaceWarnings(); + + if (current_shape_.mesh.indices.empty() && + !(at_eof && current_shape_has_face_record_)) { + current_shape_has_face_record_ = false; + current_shape_degenerate_face_count_ = 0; + if (at_eof) { + current_shape_from_group_ = false; + } + return; + } + + shapes_->push_back(current_shape_); + current_shape_ = shape_t(); + current_shape_.name.clear(); + current_shape_from_group_ = false; + current_shape_has_face_record_ = false; + current_shape_degenerate_face_count_ = 0; + } + + void EmitDegenerateFaceWarnings() { + if (!warn_) { + current_shape_degenerate_face_count_ = 0; + return; + } + + for (size_t i = 0; i < current_shape_degenerate_face_count_; i++) { + (*warn_) += "Degenerated face found\n."; + } + current_shape_degenerate_face_count_ = 0; + } + + attrib_t *attrib_; + std::vector *shapes_; + std::vector *materials_; + std::vector scratch_materials_; + MaterialReader *material_reader_; + std::string *warn_; + std::string *err_; + StreamLoadConfig config_; + shape_t current_shape_; + std::map material_map_; + std::set loaded_material_filenames_; + std::string current_material_name_; + int current_material_id_; + unsigned int current_smoothing_group_id_; + bool saw_explicit_color_; + bool saw_missing_color_; + bool current_shape_from_group_; + bool current_shape_has_face_record_; + size_t current_shape_degenerate_face_count_; +}; + +static bool ParseLineToEvent(size_t line_num, const std::string &line, + ParsedChunk *chunk) { + std::string work = line; + const size_t nul_pos = work.find('\0'); + if (nul_pos != std::string::npos) { + work.resize(nul_pos); + } + for (size_t i = 0; i < work.size(); i++) { + if (work[i] == '\r') { + work[i] = ' '; + } + } + if (Trim(work).empty()) { + return true; + } + + work = TrimLeading(work); + std::istringstream iss(work); + std::string tag; + iss >> tag; + if (!tag.empty() && tag[0] == '#') { + return true; + } + + ParsedEvent event; + event.line_num = line_num; + + if (tag == "v") { + std::vector tokens; + std::string token; + while (iss >> token) { + if (!token.empty() && token[0] == '#') { + break; + } + tokens.push_back(token); + } + if (tokens.size() < 3) { + chunk->err = "line " + std::to_string(line_num) + + ": malformed vertex record\n"; + return false; + } + + event.type = EVENT_VERTEX; + if (!ParseRealToken(tokens[0], &event.x) || + !ParseRealToken(tokens[1], &event.y) || + !ParseRealToken(tokens[2], &event.z)) { + chunk->err = "line " + std::to_string(line_num) + + ": malformed vertex coordinates\n"; + return false; + } + + if (tokens.size() >= 4) { + real_t maybe_r = real_t(1.0); + if (ParseRealToken(tokens[3], &maybe_r)) { + if (tokens.size() == 4) { + event.has_vertex_weight = true; + event.vertex_weight = maybe_r; + } else { + real_t maybe_g = real_t(1.0); + if (!ParseRealToken(tokens[4], &maybe_g)) { + event.has_vertex_weight = true; + event.vertex_weight = maybe_r; + } else if (tokens.size() >= 6) { + real_t maybe_b = real_t(1.0); + if (ParseRealToken(tokens[5], &maybe_b)) { + event.has_vertex_weight = true; + event.vertex_weight = maybe_r; + event.has_color = true; + event.r = maybe_r; + event.g = maybe_g; + event.b = maybe_b; + } + } + } + } + } + + chunk->events.push_back(event); + return true; + } + + if (tag == "vn") { + std::string sx, sy, sz; + if (!(iss >> sx >> sy >> sz)) { + chunk->err = "line " + std::to_string(line_num) + + ": malformed normal record\n"; + return false; + } + event.type = EVENT_NORMAL; + if (!ParseRealToken(sx, &event.x) || !ParseRealToken(sy, &event.y) || + !ParseRealToken(sz, &event.z)) { + chunk->err = "line " + std::to_string(line_num) + + ": malformed normal record\n"; + return false; + } + chunk->events.push_back(event); + return true; + } + + if (tag == "vt") { + std::string su, sv, sw; + if (!(iss >> su)) { + chunk->err = "line " + std::to_string(line_num) + + ": malformed texcoord record\n"; + return false; + } + event.type = EVENT_TEXCOORD; + event.y = real_t(0.0); + if (!ParseRealToken(su, &event.x)) { + chunk->err = "line " + std::to_string(line_num) + + ": malformed texcoord record\n"; + return false; + } + if (iss >> sv) { + if (sv[0] == '#') { + chunk->events.push_back(event); + return true; + } + if (!ParseRealToken(sv, &event.y)) { + chunk->err = "line " + std::to_string(line_num) + + ": malformed texcoord record\n"; + return false; + } + } + if (iss >> sw) { + if (sw[0] != '#') { + event.has_texcoord_w = true; + if (!ParseRealToken(sw, &event.w)) { + chunk->err = "line " + std::to_string(line_num) + + ": malformed texcoord record\n"; + return false; + } + } + } + chunk->events.push_back(event); + return true; + } + + if (tag == "f") { + event.type = EVENT_FACE; + std::string tok; + while (iss >> tok) { + if (!tok.empty() && tok[0] == '#') { + break; + } + RawIndex idx; + if (!ParseRawTripleToken(tok, &idx)) { + chunk->err = "line " + std::to_string(line_num) + + ": malformed face record\n"; + return false; + } + event.face.push_back(idx); + } + chunk->events.push_back(event); + return true; + } + + if (tag == "g") { + event.type = EVENT_GROUP; + event.text = ParseLegacyGroupName(&iss); + chunk->events.push_back(event); + if (event.text.empty()) { + ParsedEvent warn_event; + warn_event.type = EVENT_WARNING; + warn_event.text = "Empty group name. line: " + std::to_string(line_num) + + "\n"; + chunk->events.push_back(warn_event); + } + return true; + } + + if (tag == "o") { + event.type = EVENT_OBJECT; + if (iss.peek() == ' ' || iss.peek() == '\t') { + iss.get(); + } + std::getline(iss, event.text); + chunk->events.push_back(event); + return true; + } + + if (tag == "usemtl") { + event.type = EVENT_USEMTL; + iss >> event.text; + chunk->events.push_back(event); + return true; + } + + if (tag == "mtllib") { + event.type = EVENT_MTLLIB; + std::string rest; + std::getline(iss, rest); + SplitFilenames(Trim(rest), &event.filenames); + chunk->events.push_back(event); + return true; + } + + if (tag == "s") { + event.type = EVENT_SMOOTHING; + std::string value; + iss >> value; + if (value == "off" || value == "0") { + event.smoothing_group_id = 0; + } else { + int smoothing = 0; + if (ParseIntToken(value, &smoothing) && smoothing > 0) { + event.smoothing_group_id = static_cast(smoothing); + } else { + event.smoothing_group_id = 0; + } + } + chunk->events.push_back(event); + return true; + } + + event.type = EVENT_WARNING; + event.text = "line " + std::to_string(line_num) + ": ignoring `" + tag + + "` in experimental stream parser\n"; + chunk->events.push_back(event); + return true; +} + +static bool ReplayChunk(const ParsedChunk &chunk, StreamHandler *handler, + std::string *warn, std::string *err, + int *num_vertices, int *num_normals, + int *num_texcoords, int *greatest_v_idx, + int *greatest_vn_idx, int *greatest_vt_idx, + std::vector *vertex_positions, + const std::string &source_name, + const StreamLoadConfig &config) { + for (size_t i = 0; i < chunk.events.size(); i++) { + const ParsedEvent &event = chunk.events[i]; + switch (event.type) { + case EVENT_VERTEX: + handler->OnVertex(event.x, event.y, event.z, event.has_vertex_weight, + event.vertex_weight, event.has_color, event.r, + event.g, event.b); + if (vertex_positions) { + vertex_positions->push_back(event.x); + vertex_positions->push_back(event.y); + vertex_positions->push_back(event.z); + } + (*num_vertices)++; + break; + case EVENT_NORMAL: + handler->OnNormal(event.x, event.y, event.z); + (*num_normals)++; + break; + case EVENT_TEXCOORD: + handler->OnTexcoord(event.x, event.y, event.has_texcoord_w, event.w); + (*num_texcoords)++; + break; + case EVENT_FACE: + if (event.face.size() < 3) { + handler->OnDegenerateFace(); + break; + } + { + std::vector face(event.face.size()); + for (size_t k = 0; k < event.face.size(); k++) { + if (!ResolveIndexLikeLegacy(event.face[k].vertex_index, + *num_vertices, &face[k].vertex_index, + false, source_name, event.line_num, + warn)) { + if (err) { + (*err) += "line " + std::to_string(event.line_num) + + ": malformed face record\n"; + } + return false; + } + UpdateGreatestIndex(face[k].vertex_index, greatest_v_idx); + + if (event.face[k].has_texcoord_index) { + if (!ResolveIndexLikeLegacy(event.face[k].texcoord_index, + *num_texcoords, + &face[k].texcoord_index, true, + source_name, event.line_num, warn)) { + if (err) { + (*err) += "line " + std::to_string(event.line_num) + + ": malformed face record\n"; + } + return false; + } + if (face[k].texcoord_index >= 0) { + UpdateGreatestIndex(face[k].texcoord_index, greatest_vt_idx); + } + } else { + face[k].texcoord_index = -1; + } + + if (event.face[k].has_normal_index) { + if (!ResolveIndexLikeLegacy(event.face[k].normal_index, + *num_normals, &face[k].normal_index, + true, source_name, event.line_num, + warn)) { + if (err) { + (*err) += "line " + std::to_string(event.line_num) + + ": malformed face record\n"; + } + return false; + } + if (face[k].normal_index >= 0) { + UpdateGreatestIndex(face[k].normal_index, greatest_vn_idx); + } + } else { + face[k].normal_index = -1; + } + } + + if (config.triangulate && face.size() > 3) { + std::vector tris((face.size() - 2) * 3); + const size_t tri_count = TriangulateFaceLikeLegacy( + *vertex_positions, face.data(), face.size(), tris.data()); + for (size_t k = 0; k + 2 < tri_count; k += 3) { + handler->OnFace(&tris[k], 3); + } + } else { + handler->OnFace(face.data(), face.size()); + } + } + break; + case EVENT_GROUP: + handler->OnGroup(event.text); + break; + case EVENT_OBJECT: + handler->OnObject(event.text); + break; + case EVENT_USEMTL: + handler->OnUsemtl(event.text); + break; + case EVENT_MTLLIB: + handler->OnMtllibWithLine(event.filenames, event.line_num); + break; + case EVENT_SMOOTHING: + handler->OnSmoothingGroup(event.smoothing_group_id); + break; + case EVENT_WARNING: + if (warn) { + (*warn) += event.text; + } + break; + } + } + + return true; +} + +} // namespace + +bool ParseObjStream(std::istream *input, StreamHandler *handler, + std::string *warn, std::string *err, + const std::string &source_name, + const StreamLoadConfig &config) { + if (!input || !handler) { + if (err) { + (*err) += "input stream and handler must not be null.\n"; + } + return false; + } + + std::string line; + size_t line_num = 0; + int num_vertices = 0; + int num_normals = 0; + int num_texcoords = 0; + int greatest_v_idx = -1; + int greatest_vn_idx = -1; + int greatest_vt_idx = -1; + std::vector vertex_positions; + + int num_threads = config.num_threads; + if (num_threads < 1) { + num_threads = 1; + } + + size_t chunk_line_count = config.chunk_line_count; + if (chunk_line_count < 1) { + chunk_line_count = 1; + } + + while (true) { + std::vector > > chunk_inputs; + chunk_inputs.reserve(static_cast(num_threads)); + + for (int t = 0; t < num_threads; t++) { + std::vector > lines; + lines.reserve(chunk_line_count); + while (lines.size() < chunk_line_count && std::getline(*input, line)) { + line_num++; + if (!line.empty() && line[line.size() - 1] == '\r') { + line.resize(line.size() - 1); + } + lines.push_back(std::make_pair(line_num, line)); + } + if (!lines.empty()) { + chunk_inputs.push_back(lines); + } + if (!(*input)) { + break; + } + } + + if (chunk_inputs.empty()) { + break; + } + + std::vector chunks(chunk_inputs.size()); + size_t error_chunk_index = chunks.size(); + if (chunk_inputs.size() == 1) { + for (size_t i = 0; i < chunk_inputs[0].size(); i++) { + if (!ParseLineToEvent(chunk_inputs[0][i].first, chunk_inputs[0][i].second, + &chunks[0])) { + error_chunk_index = 0; + break; + } + } + } else { + std::vector workers; + workers.reserve(chunk_inputs.size()); + for (size_t c = 0; c < chunk_inputs.size(); c++) { + workers.push_back(std::thread([&, c]() { + for (size_t i = 0; i < chunk_inputs[c].size(); i++) { + if (!ParseLineToEvent(chunk_inputs[c][i].first, + chunk_inputs[c][i].second, &chunks[c])) { + return; + } + } + })); + } + for (size_t c = 0; c < workers.size(); c++) { + workers[c].join(); + } + for (size_t c = 0; c < chunks.size(); c++) { + if (!chunks[c].err.empty()) { + error_chunk_index = c; + break; + } + } + } + + const size_t replay_chunk_count = + (error_chunk_index < chunks.size()) ? (error_chunk_index + 1) + : chunks.size(); + for (size_t c = 0; c < replay_chunk_count; c++) { + if (!ReplayChunk(chunks[c], handler, warn, err, &num_vertices, + &num_normals, &num_texcoords, &greatest_v_idx, + &greatest_vn_idx, &greatest_vt_idx, &vertex_positions, + source_name, config)) { + return false; + } + } + + if (error_chunk_index < chunks.size()) { + if (err) { + (*err) += chunks[error_chunk_index].err; + } + return false; + } + } + + AppendOutOfBoundsWarnings(warn, greatest_v_idx, greatest_vn_idx, + greatest_vt_idx, num_vertices, num_normals, + num_texcoords, line_num + 1); + return true; +} + +bool LoadObjStreamExperimental( + attrib_t *attrib, std::vector *shapes, + std::vector *materials, std::string *warn, std::string *err, + std::istream *input, MaterialReader *readMatFn, + const StreamLoadConfig &config) { + if (!attrib || !shapes || !input) { + if (err) { + (*err) += "attrib, shapes and input stream must not be null.\n"; + } + return false; + } + + MeshBuilderHandler builder(attrib, shapes, materials, readMatFn, warn, err, + config); + bool ok = ParseObjStream(input, &builder, warn, err, "", config); + if (!ok) { + attrib->vertices.clear(); + attrib->vertex_weights.clear(); + attrib->normals.clear(); + attrib->texcoords.clear(); + attrib->texcoord_ws.clear(); + attrib->colors.clear(); + attrib->skin_weights.clear(); + shapes->clear(); + if (materials) materials->clear(); + return false; + } + builder.Finish(); + return ok; +} + +bool LoadObjStreamExperimental( + attrib_t *attrib, std::vector *shapes, + std::vector *materials, std::string *warn, std::string *err, + const char *filename, const char *mtl_basedir, + const StreamLoadConfig &config) { + if (!filename) { + if (err) { + (*err) += "filename must not be null.\n"; + } + return false; + } + + std::ifstream ifs(filename); + if (!ifs) { + if (err) { + (*err) += "Cannot open file: " + std::string(filename) + "\n"; + } + return false; + } + + std::string base_dir; + if (mtl_basedir) { + base_dir = mtl_basedir; + } else { + std::string path(filename); + size_t pos = path.find_last_of("/\\"); + if (pos != std::string::npos) { + base_dir = path.substr(0, pos + 1); + } + } + + MaterialFileReader mat_reader(base_dir); + if (!attrib || !shapes) { + if (err) { + (*err) += "attrib and shapes must not be null.\n"; + } + return false; + } + + MeshBuilderHandler builder(attrib, shapes, materials, &mat_reader, warn, err, + config); + bool ok = ParseObjStream(&ifs, &builder, warn, err, filename, config); + if (!ok) { + attrib->vertices.clear(); + attrib->vertex_weights.clear(); + attrib->normals.clear(); + attrib->texcoords.clear(); + attrib->texcoord_ws.clear(); + attrib->colors.clear(); + attrib->skin_weights.clear(); + shapes->clear(); + if (materials) materials->clear(); + return false; + } + + builder.Finish(); + return true; +} + +} // namespace experimental_stream +} // namespace tinyobj diff --git a/experimental/stream/stream_obj_loader.h b/experimental/stream/stream_obj_loader.h new file mode 100644 index 00000000..ff6ec33d --- /dev/null +++ b/experimental/stream/stream_obj_loader.h @@ -0,0 +1,69 @@ +#ifndef TINYOBJ_EXPERIMENTAL_STREAM_OBJ_LOADER_H_ +#define TINYOBJ_EXPERIMENTAL_STREAM_OBJ_LOADER_H_ + +#include +#include +#include + +#include "tiny_obj_loader.h" + +namespace tinyobj { +namespace experimental_stream { + +struct StreamLoadConfig { + bool triangulate; + bool default_vcols_fallback; + int num_threads; + size_t chunk_line_count; + + StreamLoadConfig() + : triangulate(true), + default_vcols_fallback(false), + num_threads(1), + chunk_line_count(4096) {} +}; + +class StreamHandler { + public: + virtual ~StreamHandler() {} + + virtual void OnVertex(real_t x, real_t y, real_t z, bool has_weight, + real_t w, bool has_color, real_t r, real_t g, + real_t b) = 0; + virtual void OnNormal(real_t x, real_t y, real_t z) = 0; + virtual void OnTexcoord(real_t u, real_t v, bool has_w, real_t w) = 0; + virtual void OnFace(const index_t *indices, size_t num_indices) = 0; + virtual void OnDegenerateFace() {} + virtual void OnGroup(const std::string &name) = 0; + virtual void OnObject(const std::string &name) = 0; + virtual void OnUsemtl(const std::string &name) = 0; + virtual void OnMtllib(const std::vector &filenames) = 0; + virtual void OnMtllibWithLine(const std::vector &filenames, + size_t line_num) { + (void)line_num; + OnMtllib(filenames); + } + virtual void OnSmoothingGroup(unsigned int smoothing_group_id) = 0; +}; + +bool ParseObjStream(std::istream *input, StreamHandler *handler, + std::string *warn, std::string *err, + const std::string &source_name, + const StreamLoadConfig &config = StreamLoadConfig()); + +bool LoadObjStreamExperimental( + attrib_t *attrib, std::vector *shapes, + std::vector *materials, std::string *warn, std::string *err, + std::istream *input, MaterialReader *readMatFn = NULL, + const StreamLoadConfig &config = StreamLoadConfig()); + +bool LoadObjStreamExperimental( + attrib_t *attrib, std::vector *shapes, + std::vector *materials, std::string *warn, std::string *err, + const char *filename, const char *mtl_basedir = NULL, + const StreamLoadConfig &config = StreamLoadConfig()); + +} // namespace experimental_stream +} // namespace tinyobj + +#endif // TINYOBJ_EXPERIMENTAL_STREAM_OBJ_LOADER_H_ diff --git a/loader_example.cc b/loader_example.cc index 21feb684..2ce6d58f 100644 --- a/loader_example.cc +++ b/loader_example.cc @@ -379,12 +379,12 @@ static bool TestStreamLoadObj() { public: MaterialStringStreamReader(const std::string& matSStream) : m_matSStream(matSStream) {} - virtual ~MaterialStringStreamReader() TINYOBJ_OVERRIDE {} + virtual ~MaterialStringStreamReader() override {} virtual bool operator()(const std::string& matId, std::vector* materials, std::map* matMap, std::string* warn, - std::string* err) TINYOBJ_OVERRIDE { + std::string* err) override { (void)err; (void)matId; LoadMtl(matMap, materials, &m_matSStream, warn, err); diff --git a/tests/Makefile b/tests/Makefile index 4a557a4e..7427352c 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -4,14 +4,21 @@ CXX ?= clang++ CXXFLAGS ?= -g -O1 EXTRA_CXXFLAGS ?= -std=c++11 -fsanitize=address -tester: tester.cc ../tiny_obj_loader.h - $(CXX) $(CXXFLAGS) $(EXTRA_CXXFLAGS) -o tester tester.cc +tester: tester.cc ../tiny_obj_loader.h ../experimental/stream/stream_obj_loader.cc ../experimental/stream/stream_obj_loader.h opt/loadobjopt_multithread.inc + $(CXX) $(CXXFLAGS) $(EXTRA_CXXFLAGS) -pthread -I.. -o tester tester.cc ../experimental/stream/stream_obj_loader.cc all: tester +obj-fuzz: + $(MAKE) -C obj-fuzz + +llvm-fuzz: + $(MAKE) -C llvm-fuzz + check: tester ./tester clean: rm -rf tester - + $(MAKE) -C obj-fuzz clean + $(MAKE) -C llvm-fuzz clean diff --git a/tests/README.md b/tests/README.md index 1b0b43d1..7c76a523 100644 --- a/tests/README.md +++ b/tests/README.md @@ -4,6 +4,11 @@ $ make check +Additional fuzz targets: + + $ make obj-fuzz + $ make llvm-fuzz + ## Use ninja + kuroga Assume @@ -34,4 +39,3 @@ Or on msys2 bash, $ cmd //c vcbuild.bat - diff --git a/tests/fuzz_common.h b/tests/fuzz_common.h new file mode 100644 index 00000000..2059f1cd --- /dev/null +++ b/tests/fuzz_common.h @@ -0,0 +1,331 @@ +#ifndef TINYOBJ_TESTS_FUZZ_COMMON_H_ +#define TINYOBJ_TESTS_FUZZ_COMMON_H_ + +#include +#include +#include +#include +#include +#include +#include + +// Include tiny_obj_loader.h (or stream_obj_loader.h, which includes it) +// before including this helper. We intentionally avoid including it here +// because TINYOBJLOADER_IMPLEMENTATION lives outside the header guard. + +namespace tinyobj_fuzz { + +class InMemoryMaterialReader : public tinyobj::MaterialReader { + public: + explicit InMemoryMaterialReader(const std::string &mtl_text) + : mtl_text_(mtl_text) {} + + virtual bool operator()(const std::string &mat_id, + std::vector *materials, + std::map *mat_map, + std::string *warn, std::string *err) { + (void)mat_id; + std::istringstream stream(mtl_text_); + tinyobj::MaterialStreamReader reader(stream); + return reader("", materials, mat_map, warn, err); + } + + private: + std::string mtl_text_; +}; + +inline void SplitInput(const uint8_t *data, size_t size, std::string *obj_text, + std::string *mtl_text) { + obj_text->clear(); + mtl_text->clear(); + if (!data || size == 0) { + return; + } + + size_t split = size; + for (size_t i = 0; i < size; i++) { + if (data[i] == 0) { + split = i; + break; + } + } + + obj_text->assign(reinterpret_cast(data), split); + if (split < size) { + mtl_text->assign(reinterpret_cast(data + split + 1), + size - split - 1); + } +} + +inline std::string FirstToken(const std::string &line) { + size_t pos = 0; + while (pos < line.size() && + std::isspace(static_cast(line[pos]))) { + pos++; + } + if (pos >= line.size() || line[pos] == '#') { + return std::string(); + } + size_t end = pos; + while (end < line.size() && + !std::isspace(static_cast(line[end]))) { + end++; + } + return line.substr(pos, end - pos); +} + +inline bool IsSharedSubsetForCrossCheck(const std::string &obj_text) { + std::istringstream stream(obj_text); + std::string line; + while (std::getline(stream, line)) { + const std::string token = FirstToken(line); + if (token.empty()) { + continue; + } + if (token == "mtllib" || token == "vw" || token == "l" || token == "p" || + token == "t") { + return false; + } + } + return true; +} + +inline bool IsTextLikeObj(const std::string &obj_text) { + for (size_t i = 0; i < obj_text.size(); i++) { + const unsigned char c = static_cast(obj_text[i]); + if (c == '\n' || c == '\r' || c == '\t') { + continue; + } + if (c < 0x20 || c > 0x7e) { + return false; + } + } + return true; +} + +inline bool IndexEquals(const tinyobj::index_t &lhs, + const tinyobj::index_t &rhs) { + return lhs.vertex_index == rhs.vertex_index && + lhs.texcoord_index == rhs.texcoord_index && + lhs.normal_index == rhs.normal_index; +} + +inline bool TagEquals(const tinyobj::tag_t &lhs, const tinyobj::tag_t &rhs) { + return lhs.name == rhs.name && lhs.intValues == rhs.intValues && + lhs.floatValues == rhs.floatValues && + lhs.stringValues == rhs.stringValues; +} + +template +inline bool SkinWeightEquals(const SkinWeightT &lhs, const SkinWeightT &rhs) { + if (lhs.vertex_id != rhs.vertex_id || + lhs.weightValues.size() != rhs.weightValues.size()) { + return false; + } + for (size_t i = 0; i < lhs.weightValues.size(); i++) { + if (lhs.weightValues[i].joint_id != rhs.weightValues[i].joint_id || + lhs.weightValues[i].weight != rhs.weightValues[i].weight) { + return false; + } + } + return true; +} + +inline bool LegacyMeshEquals(const tinyobj::mesh_t &lhs, + const tinyobj::mesh_t &rhs) { + if (lhs.indices.size() != rhs.indices.size()) { + return false; + } + for (size_t i = 0; i < lhs.indices.size(); i++) { + if (!IndexEquals(lhs.indices[i], rhs.indices[i])) { + return false; + } + } + if (lhs.num_face_vertices != rhs.num_face_vertices || + lhs.material_ids != rhs.material_ids || + lhs.smoothing_group_ids != rhs.smoothing_group_ids || + lhs.tags.size() != rhs.tags.size()) { + return false; + } + for (size_t i = 0; i < lhs.tags.size(); i++) { + if (!TagEquals(lhs.tags[i], rhs.tags[i])) { + return false; + } + } + return true; +} + +inline bool LegacyLinesEquals(const tinyobj::lines_t &lhs, + const tinyobj::lines_t &rhs) { + if (lhs.indices.size() != rhs.indices.size()) { + return false; + } + for (size_t i = 0; i < lhs.indices.size(); i++) { + if (!IndexEquals(lhs.indices[i], rhs.indices[i])) { + return false; + } + } + return lhs.num_line_vertices == rhs.num_line_vertices; +} + +inline bool LegacyPointsEquals(const tinyobj::points_t &lhs, + const tinyobj::points_t &rhs) { + if (lhs.indices.size() != rhs.indices.size()) { + return false; + } + for (size_t i = 0; i < lhs.indices.size(); i++) { + if (!IndexEquals(lhs.indices[i], rhs.indices[i])) { + return false; + } + } + return true; +} + +inline bool LegacyShapeEquals(const tinyobj::shape_t &lhs, + const tinyobj::shape_t &rhs) { + return lhs.name == rhs.name && LegacyMeshEquals(lhs.mesh, rhs.mesh) && + LegacyLinesEquals(lhs.lines, rhs.lines) && + LegacyPointsEquals(lhs.points, rhs.points); +} + +inline bool LegacyShapesEqual(const std::vector &lhs, + const std::vector &rhs) { + if (lhs.size() != rhs.size()) { + return false; + } + for (size_t i = 0; i < lhs.size(); i++) { + if (!LegacyShapeEquals(lhs[i], rhs[i])) { + return false; + } + } + return true; +} + +inline bool LegacyMaterialsEqual( + const std::vector &lhs, + const std::vector &rhs) { + if (lhs.size() != rhs.size()) { + return false; + } + for (size_t i = 0; i < lhs.size(); i++) { + if (lhs[i].name != rhs[i].name) { + return false; + } + } + return true; +} + +inline size_t ShapeIndexCount(const std::vector &shapes) { + size_t count = 0; + for (size_t i = 0; i < shapes.size(); i++) { + count += shapes[i].mesh.indices.size(); + } + return count; +} + +inline bool LegacyAttribEquals(const tinyobj::attrib_t &lhs, + const tinyobj::attrib_t &rhs) { + if (lhs.vertices != rhs.vertices || lhs.vertex_weights != rhs.vertex_weights || + lhs.normals != rhs.normals || lhs.texcoords != rhs.texcoords || + lhs.texcoord_ws != rhs.texcoord_ws || lhs.colors != rhs.colors || + lhs.skin_weights.size() != rhs.skin_weights.size()) { + return false; + } + for (size_t i = 0; i < lhs.skin_weights.size(); i++) { + if (!SkinWeightEquals(lhs.skin_weights[i], rhs.skin_weights[i])) { + return false; + } + } + return true; +} + +inline bool LegacyMeshEqualsOpt(const tinyobj::mesh_t &lhs, + const tinyobj::basic_mesh_t<> &rhs) { + if (lhs.indices.size() != rhs.indices.size()) { + return false; + } + for (size_t i = 0; i < lhs.indices.size(); i++) { + if (!IndexEquals(lhs.indices[i], rhs.indices[i])) { + return false; + } + } + if (lhs.num_face_vertices != rhs.num_face_vertices || + lhs.material_ids != rhs.material_ids || + lhs.smoothing_group_ids != rhs.smoothing_group_ids || + lhs.tags.size() != rhs.tags.size()) { + return false; + } + for (size_t i = 0; i < lhs.tags.size(); i++) { + if (!TagEquals(lhs.tags[i], rhs.tags[i])) { + return false; + } + } + return true; +} + +inline bool LegacyLinesEqualsOpt(const tinyobj::lines_t &lhs, + const tinyobj::basic_lines_t<> &rhs) { + if (lhs.indices.size() != rhs.indices.size()) { + return false; + } + for (size_t i = 0; i < lhs.indices.size(); i++) { + if (!IndexEquals(lhs.indices[i], rhs.indices[i])) { + return false; + } + } + return lhs.num_line_vertices == rhs.num_line_vertices; +} + +inline bool LegacyPointsEqualsOpt(const tinyobj::points_t &lhs, + const tinyobj::basic_points_t<> &rhs) { + if (lhs.indices.size() != rhs.indices.size()) { + return false; + } + for (size_t i = 0; i < lhs.indices.size(); i++) { + if (!IndexEquals(lhs.indices[i], rhs.indices[i])) { + return false; + } + } + return true; +} + +inline bool LegacyShapeEqualsOpt(const tinyobj::shape_t &lhs, + const tinyobj::basic_shape_t<> &rhs) { + return lhs.name == rhs.name && LegacyMeshEqualsOpt(lhs.mesh, rhs.mesh) && + LegacyLinesEqualsOpt(lhs.lines, rhs.lines) && + LegacyPointsEqualsOpt(lhs.points, rhs.points); +} + +inline bool LegacyShapesEqualOpt( + const std::vector &lhs, + const std::vector > &rhs) { + if (lhs.size() != rhs.size()) { + return false; + } + for (size_t i = 0; i < lhs.size(); i++) { + if (!LegacyShapeEqualsOpt(lhs[i], rhs[i])) { + return false; + } + } + return true; +} + +inline bool LegacyAttribEqualsOpt(const tinyobj::attrib_t &lhs, + const tinyobj::basic_attrib_t<> &rhs) { + if (lhs.vertices != rhs.vertices || lhs.vertex_weights != rhs.vertex_weights || + lhs.normals != rhs.normals || lhs.texcoords != rhs.texcoords || + lhs.texcoord_ws != rhs.texcoord_ws || lhs.colors != rhs.colors || + lhs.skin_weights.size() != rhs.skin_weights.size()) { + return false; + } + for (size_t i = 0; i < lhs.skin_weights.size(); i++) { + if (!SkinWeightEquals(lhs.skin_weights[i], rhs.skin_weights[i])) { + return false; + } + } + return true; +} + +} // namespace tinyobj_fuzz + +#endif // TINYOBJ_TESTS_FUZZ_COMMON_H_ diff --git a/tests/llvm-fuzz/Makefile b/tests/llvm-fuzz/Makefile new file mode 100644 index 00000000..6889a557 --- /dev/null +++ b/tests/llvm-fuzz/Makefile @@ -0,0 +1,19 @@ +.PHONY: all clean run + +CXX := clang++ +CXXFLAGS ?= -g -O1 -std=c++11 +SANITIZERS ?= -fsanitize=fuzzer,address,undefined +CPPFLAGS ?= -DTINYOBJLOADER_USE_MULTITHREADING + +TARGET := fuzz_loaders + +all: $(TARGET) + +$(TARGET): fuzz_loaders.cc ../fuzz_common.h ../../tiny_obj_loader.h ../../tiny_obj_loader.cc ../../experimental/stream/stream_obj_loader.cc ../../experimental/stream/stream_obj_loader.h + $(CXX) $(CXXFLAGS) $(SANITIZERS) $(CPPFLAGS) -pthread -I../.. -I.. -o $(TARGET) fuzz_loaders.cc ../../tiny_obj_loader.cc ../../experimental/stream/stream_obj_loader.cc + +run: $(TARGET) + ./$(TARGET) corpus -max_total_time=600 -rss_limit_mb=2048 -max_len=4096 + +clean: + rm -f $(TARGET) diff --git a/tests/llvm-fuzz/README.md b/tests/llvm-fuzz/README.md new file mode 100644 index 00000000..65fa7f1a --- /dev/null +++ b/tests/llvm-fuzz/README.md @@ -0,0 +1,46 @@ +# LLVM libFuzzer Harness + +This directory contains a libFuzzer entry point for the main parser paths: + +- `tinyobj::ObjReader::ParseFromString` +- `tinyobj::LoadObj` +- `tinyobj::LoadObjOpt` +- `tinyobj::experimental_stream::LoadObjStreamExperimental` + +The harness accepts a single mutated byte stream. + +- Bytes before the first `NUL` are treated as OBJ text. +- Bytes after the first `NUL` are treated as MTL text. +- Parser configs are varied from the first few bytes to cover different + thread counts and parser settings. + +For inputs in the shared supported subset (`v`, `vn`, `vt`, `f`, `g`, `o`, +`usemtl`, `s`, comments), the harness cross-checks successful parses between +the legacy and stream loaders. `LoadObjOpt` is still exercised on every input +for stability coverage, but is not the default differential oracle here. + +## Build + +```bash +cd tests/llvm-fuzz +make +``` + +## Run + +```bash +cd tests/llvm-fuzz +./fuzz_loaders corpus -rss_limit_mb=2048 +``` + +## Seed corpus + +`corpus/` contains small valid OBJ seeds so the fuzzer starts from structured +inputs instead of pure random bytes. + +## Other applicable fuzzing + +- Differential fuzzing against another OBJ parser implementation. +- Sanitizer matrix runs with `address`, `undefined`, `thread`, and `memory`. +- Structure-aware mutation based on OBJ grammar instead of raw-byte mutation. +- File-path fuzzing for mmap/file overloads and material search path handling. diff --git a/tests/llvm-fuzz/corpus/seed_basic.obj b/tests/llvm-fuzz/corpus/seed_basic.obj new file mode 100644 index 00000000..f52c4a56 --- /dev/null +++ b/tests/llvm-fuzz/corpus/seed_basic.obj @@ -0,0 +1,12 @@ +o seed +g seed_group +v 0.0 0.0 0.0 +v 1.0 0.0 0.0 +v 0.0 1.0 0.0 +vt 0.0 0.0 +vt 1.0 0.0 +vt 0.0 1.0 +vn 0.0 0.0 1.0 +usemtl default +s 1 +f 1/1/1 2/2/1 3/3/1 diff --git a/tests/llvm-fuzz/fuzz_loaders.cc b/tests/llvm-fuzz/fuzz_loaders.cc new file mode 100644 index 00000000..0e6c3928 --- /dev/null +++ b/tests/llvm-fuzz/fuzz_loaders.cc @@ -0,0 +1,105 @@ +#include +#include +#include + +#include "../../experimental/stream/stream_obj_loader.h" + +#include +#include +#include + +#include "../fuzz_common.h" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + // Cap input size to prevent per-iteration memory explosion from running + // 5 parsers (ObjReader, LoadObj, LoadObjOpt, StreamLoader, LoadObjOptTyped) + // simultaneously. Inputs above this threshold are unlikely to find new + // coverage but can push RSS past the limit. + if (size > 4096) return 0; + + std::string obj_text; + std::string mtl_text; + tinyobj_fuzz::SplitInput(data, size, &obj_text, &mtl_text); + + tinyobj::ObjReaderConfig reader_config; + reader_config.triangulate = (size == 0) ? true : ((data[0] & 1u) != 0u); + reader_config.vertex_color = (size > 1) ? ((data[1] & 1u) != 0u) : false; + + tinyobj::ObjReader reader; + reader.ParseFromString(obj_text, mtl_text, reader_config); + + tinyobj_fuzz::InMemoryMaterialReader material_reader(mtl_text); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn; + std::string legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, + &legacy_err, &legacy_stream, + &material_reader, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector > opt_shapes; + std::vector opt_materials; + std::string opt_warn; + std::string opt_err; + tinyobj::OptLoadConfig opt_config; + opt_config.triangulate = true; + opt_config.num_threads = (size > 2) ? static_cast((data[2] % 4u) + 1u) : 2; + tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, &opt_warn, + &opt_err, obj_text.data(), obj_text.size(), opt_config); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn; + std::string stream_err; + std::istringstream stream_input(obj_text); + tinyobj::experimental_stream::StreamLoadConfig stream_config; + stream_config.triangulate = true; + stream_config.num_threads = + (size > 3) ? static_cast((data[3] % 4u) + 1u) : 2; + stream_config.chunk_line_count = 32u + (size > 4 ? data[4] : 64u); + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream_input, &material_reader, stream_config); + + const bool can_cross_check = + tinyobj_fuzz::IsTextLikeObj(obj_text) && + tinyobj_fuzz::IsSharedSubsetForCrossCheck(obj_text); + const bool both_have_faces = + tinyobj_fuzz::ShapeIndexCount(legacy_shapes) > 0 && + tinyobj_fuzz::ShapeIndexCount(stream_shapes) > 0; + const bool clean_parse = legacy_err.empty() && stream_err.empty() && + legacy_warn.empty() && stream_warn.empty(); + + if (can_cross_check && both_have_faces && clean_parse && legacy_ok && stream_ok) { + assert(tinyobj_fuzz::LegacyAttribEquals(legacy_attrib, stream_attrib)); + assert(tinyobj_fuzz::LegacyShapesEqual(legacy_shapes, stream_shapes)); + assert( + tinyobj_fuzz::LegacyMaterialsEqual(legacy_materials, stream_materials)); + } + + (void)opt_attrib; + (void)opt_shapes; + (void)opt_materials; + + // Exercise LoadObjOptTyped (TypedArray/arena path) + { + std::string typed_warn; + std::string typed_err; + tinyobj::OptLoadConfig typed_config; + typed_config.triangulate = true; + typed_config.num_threads = 1; + // Toggle cache based on input byte to exercise both paths + typed_config.float_cache = (size > 5) ? ((data[5] & 1u) != 0u) : false; + tinyobj::OptResult typed_result = tinyobj::LoadObjOptTyped( + obj_text.data(), obj_text.size(), &typed_warn, &typed_err, typed_config); + (void)typed_result; + } + + return 0; +} diff --git a/tests/obj-fuzz/Makefile b/tests/obj-fuzz/Makefile new file mode 100644 index 00000000..e77157e4 --- /dev/null +++ b/tests/obj-fuzz/Makefile @@ -0,0 +1,19 @@ +.PHONY: all clean run + +CXX ?= clang++ +CXXFLAGS ?= -g -O1 -std=c++11 +SANITIZERS ?= -fsanitize=address,undefined +CPPFLAGS ?= -DTINYOBJLOADER_USE_MULTITHREADING + +TARGET := obj_fuzz + +all: $(TARGET) + +$(TARGET): obj_fuzz.cc ../fuzz_common.h ../../tiny_obj_loader.h ../../tiny_obj_loader.cc ../../experimental/stream/stream_obj_loader.cc ../../experimental/stream/stream_obj_loader.h + $(CXX) $(CXXFLAGS) $(SANITIZERS) $(CPPFLAGS) -pthread -I../.. -I.. -o $(TARGET) obj_fuzz.cc ../../tiny_obj_loader.cc ../../experimental/stream/stream_obj_loader.cc + +run: $(TARGET) + ./$(TARGET) --iterations 1000 --seed 1 + +clean: + rm -f $(TARGET) diff --git a/tests/obj-fuzz/README.md b/tests/obj-fuzz/README.md new file mode 100644 index 00000000..3356d788 --- /dev/null +++ b/tests/obj-fuzz/README.md @@ -0,0 +1,47 @@ +# Structured OBJ Fuzzer + +This directory contains a standalone randomized OBJ generator and verifier. + +The goal is different from libFuzzer: + +- generate reproducible, somewhat coherent OBJ text +- exercise faces, groups, objects, smoothing groups, vertex colors, weights, + texcoord `w`, comments, and relative indices +- compare successful parses across `LoadObj`, `ObjReader`, + `LoadObjOpt`, and the experimental stream loader + +The runner is deterministic for a given seed and prints the exact failing OBJ +text when a mismatch is found. + +By default the differential checks are: + +- `LoadObj` vs `ObjReader` +- `LoadObj` vs experimental stream loader + +`LoadObjOpt` is always executed for stability coverage, and can be promoted to a +strict differential target with `--strict-opt`. + +## Build + +```bash +cd tests/obj-fuzz +make +``` + +## Run + +```bash +cd tests/obj-fuzz +./obj_fuzz --iterations 5000 --seed 12345 +./obj_fuzz --iterations 1000 --seed 12345 --strict-opt +``` + +Optional material coverage: + +```bash +./obj_fuzz --iterations 2000 --seed 99 --allow-mtllib +``` + +`--allow-mtllib` keeps the run useful for the legacy, `ObjReader`, and stream +paths, but the standalone cross-check against `LoadObjOpt` is skipped because +the optimized in-memory API does not accept a custom material reader. diff --git a/tests/obj-fuzz/obj_fuzz.cc b/tests/obj-fuzz/obj_fuzz.cc new file mode 100644 index 00000000..5a1edf3e --- /dev/null +++ b/tests/obj-fuzz/obj_fuzz.cc @@ -0,0 +1,395 @@ +#include + +#include +#include "../../experimental/stream/stream_obj_loader.h" + +#include +#include +#include +#include +#include + +#include "../fuzz_common.h" + +namespace { + +struct Options { + uint64_t seed; + size_t iterations; + size_t max_vertices; + size_t max_faces; + bool allow_mtllib; + bool strict_opt; + + Options() + : seed(1), + iterations(1000), + max_vertices(128), + max_faces(128), + allow_mtllib(false), + strict_opt(false) {} +}; + +struct GeneratedCase { + std::string obj_text; + std::string mtl_text; +}; + +class Rng { + public: + explicit Rng(uint64_t seed) : state_(seed ? seed : 0x9e3779b97f4a7c15ULL) {} + + uint64_t NextU64() { + uint64_t x = state_; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + state_ = x; + return x * 0x2545F4914F6CDD1DULL; + } + + uint32_t NextU32() { + return static_cast(NextU64() >> 32); + } + + size_t Uniform(size_t upper_bound) { + if (upper_bound == 0) { + return 0; + } + return static_cast(NextU64() % upper_bound); + } + + bool OneIn(size_t n) { + return Uniform(n) == 0; + } + + bool Bool() { + return (NextU32() & 1u) != 0u; + } + + double Real(double min_value, double max_value) { + const double unit = + static_cast(NextU64() & 0x1fffffffffffffULL) / + static_cast(0x1fffffffffffffULL); + return min_value + (max_value - min_value) * unit; + } + + private: + uint64_t state_; +}; + +std::string FormatReal(double value) { + std::ostringstream os; + os.setf(std::ios::fixed); + os.precision(6); + os << value; + return os.str(); +} + +std::string MakeIndexToken(Rng *rng, size_t v_count, size_t vt_count, + size_t vn_count) { + const bool use_negative = (v_count > 0) && rng->OneIn(3); + const int v_idx = use_negative + ? -static_cast(rng->Uniform(v_count) + 1) + : static_cast(rng->Uniform(v_count) + 1); + const int vt_idx = rng->OneIn(3) + ? -static_cast(rng->Uniform(vt_count) + 1) + : static_cast(rng->Uniform(vt_count) + 1); + const int vn_idx = rng->OneIn(3) + ? -static_cast(rng->Uniform(vn_count) + 1) + : static_cast(rng->Uniform(vn_count) + 1); + + std::ostringstream os; + os << v_idx << "/" << vt_idx << "/" << vn_idx; + return os.str(); +} + +GeneratedCase BuildCase(Rng *rng, const Options &options) { + GeneratedCase out; + const bool has_mtllib = options.allow_mtllib && rng->OneIn(4); + const size_t material_count = 1 + rng->Uniform(4); + const size_t vertex_count = 3 + rng->Uniform(options.max_vertices); + const size_t texcoord_count = 1 + rng->Uniform(options.max_vertices); + const size_t normal_count = 1 + rng->Uniform(options.max_vertices); + const size_t face_count = 1 + rng->Uniform(options.max_faces); + + std::vector materials; + for (size_t i = 0; i < material_count; i++) { + std::ostringstream name; + name << "mat_" << i; + materials.push_back(name.str()); + out.mtl_text += "newmtl " + name.str() + "\n"; + out.mtl_text += "Kd " + FormatReal(rng->Real(0.0, 1.0)) + " " + + FormatReal(rng->Real(0.0, 1.0)) + " " + + FormatReal(rng->Real(0.0, 1.0)) + "\n"; + } + + if (has_mtllib) { + out.obj_text += "mtllib fuzz.mtl\n"; + } + out.obj_text += "o fuzz_object\n"; + out.obj_text += "g fuzz_group_0\n"; + + for (size_t i = 0; i < vertex_count; i++) { + out.obj_text += "v " + FormatReal(rng->Real(-10.0, 10.0)) + " " + + FormatReal(rng->Real(-10.0, 10.0)) + " " + + FormatReal(rng->Real(-10.0, 10.0)); + if (rng->OneIn(10)) { + out.obj_text += " # vertex"; + } + out.obj_text += "\n"; + } + + for (size_t i = 0; i < texcoord_count; i++) { + out.obj_text += "vt " + FormatReal(rng->Real(-2.0, 2.0)) + " " + + FormatReal(rng->Real(-2.0, 2.0)) + "\n"; + } + + for (size_t i = 0; i < normal_count; i++) { + out.obj_text += "vn " + FormatReal(rng->Real(-1.0, 1.0)) + " " + + FormatReal(rng->Real(-1.0, 1.0)) + " " + + FormatReal(rng->Real(-1.0, 1.0)) + "\n"; + } + + out.obj_text += "usemtl " + materials[rng->Uniform(materials.size())] + "\n"; + out.obj_text += rng->Bool() ? "s 1\n" : "s off\n"; + + for (size_t i = 0; i < face_count; i++) { + if (rng->OneIn(7)) { + std::ostringstream group_name; + group_name << "g fuzz_group_" << i; + out.obj_text += group_name.str() + "\n"; + } + if (rng->OneIn(9)) { + std::ostringstream object_name; + object_name << "o fuzz_object_" << i; + out.obj_text += object_name.str() + "\n"; + } + if (rng->OneIn(5)) { + out.obj_text += "usemtl " + materials[rng->Uniform(materials.size())] + + "\n"; + } + if (rng->OneIn(6)) { + out.obj_text += rng->Bool() ? "s 2\n" : "s off\n"; + } + + const size_t verts_in_face = 3; + out.obj_text += "f"; + for (size_t v = 0; v < verts_in_face; v++) { + out.obj_text += " " + + MakeIndexToken(rng, vertex_count, texcoord_count, + normal_count); + } + if (rng->OneIn(6)) { + out.obj_text += " # face"; + } + out.obj_text += "\n"; + } + + return out; +} + +void PrintUsage(const char *argv0) { + std::cerr << "Usage: " << argv0 + << " [--iterations N] [--seed N] [--max-vertices N]" + " [--max-faces N] [--allow-mtllib] [--strict-opt]\n"; +} + +bool ParseSizeArg(const char *value, size_t *out) { + if (!value || !out) { + return false; + } + char *end = NULL; + unsigned long long parsed = strtoull(value, &end, 10); + if (!end || *end != '\0') { + return false; + } + if (parsed > static_cast( + (std::numeric_limits::max)())) { + return false; + } + *out = static_cast(parsed); + return true; +} + +bool ParseU64Arg(const char *value, uint64_t *out) { + if (!value || !out) { + return false; + } + char *end = NULL; + unsigned long long parsed = strtoull(value, &end, 10); + if (!end || *end != '\0') { + return false; + } + *out = static_cast(parsed); + return true; +} + +} // namespace + +int main(int argc, char **argv) { + Options options; + for (int i = 1; i < argc; i++) { + const std::string arg(argv[i]); + if (arg == "--iterations" && (i + 1) < argc) { + if (!ParseSizeArg(argv[++i], &options.iterations)) { + PrintUsage(argv[0]); + return 2; + } + } else if (arg == "--seed" && (i + 1) < argc) { + if (!ParseU64Arg(argv[++i], &options.seed)) { + PrintUsage(argv[0]); + return 2; + } + } else if (arg == "--max-vertices" && (i + 1) < argc) { + if (!ParseSizeArg(argv[++i], &options.max_vertices)) { + PrintUsage(argv[0]); + return 2; + } + } else if (arg == "--max-faces" && (i + 1) < argc) { + if (!ParseSizeArg(argv[++i], &options.max_faces)) { + PrintUsage(argv[0]); + return 2; + } + } else if (arg == "--allow-mtllib") { + options.allow_mtllib = true; + } else if (arg == "--strict-opt") { + options.strict_opt = true; + } else if (arg == "--help") { + PrintUsage(argv[0]); + return 0; + } else { + PrintUsage(argv[0]); + return 2; + } + } + + Rng rng(options.seed); + for (size_t iter = 0; iter < options.iterations; iter++) { + const GeneratedCase generated = BuildCase(&rng, options); + tinyobj_fuzz::InMemoryMaterialReader material_reader(generated.mtl_text); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn; + std::string legacy_err; + std::istringstream legacy_stream(generated.obj_text); + const bool legacy_ok = tinyobj::LoadObj( + &legacy_attrib, &legacy_shapes, &legacy_materials, &legacy_warn, + &legacy_err, &legacy_stream, &material_reader, true, false); + + tinyobj::ObjReaderConfig reader_config; + reader_config.triangulate = true; + reader_config.vertex_color = false; + tinyobj::ObjReader reader; + reader.ParseFromString(generated.obj_text, generated.mtl_text, reader_config); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector > opt_shapes; + std::vector opt_materials; + std::string opt_warn; + std::string opt_err; + tinyobj::OptLoadConfig opt_config; + opt_config.triangulate = true; + opt_config.num_threads = 4; + const bool opt_ok = + tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, generated.obj_text.data(), + generated.obj_text.size(), opt_config); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn; + std::string stream_err; + std::istringstream stream_input(generated.obj_text); + tinyobj::experimental_stream::StreamLoadConfig stream_config; + stream_config.triangulate = true; + stream_config.num_threads = 4; + stream_config.chunk_line_count = 64; + const bool stream_ok = + tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream_input, &material_reader, stream_config); + + if (legacy_ok != stream_ok) { + std::cerr << "legacy/stream success mismatch at iteration " << iter + << " seed " << options.seed << "\n"; + std::cerr << generated.obj_text << "\n"; + return 1; + } + + if (options.strict_opt && !options.allow_mtllib && legacy_ok != opt_ok) { + std::cerr << "legacy/opt success mismatch at iteration " << iter + << " seed " << options.seed << "\n"; + std::cerr << generated.obj_text << "\n"; + return 1; + } + + if (legacy_ok && !tinyobj_fuzz::LegacyAttribEquals( + legacy_attrib, reader.GetAttrib())) { + std::cerr << "legacy/ObjReader attrib mismatch at iteration " << iter + << " seed " << options.seed << "\n"; + std::cerr << generated.obj_text << "\n"; + return 1; + } + + if (legacy_ok && !tinyobj_fuzz::LegacyShapesEqual(legacy_shapes, + reader.GetShapes())) { + std::cerr << "legacy/ObjReader shape mismatch at iteration " << iter + << " seed " << options.seed << "\n"; + std::cerr << generated.obj_text << "\n"; + return 1; + } + + if (legacy_ok && !tinyobj_fuzz::LegacyAttribEquals(legacy_attrib, + stream_attrib)) { + std::cerr << "legacy/stream attrib mismatch at iteration " << iter + << " seed " << options.seed << "\n"; + std::cerr << generated.obj_text << "\n"; + return 1; + } + + if (legacy_ok && + !tinyobj_fuzz::LegacyShapesEqual(legacy_shapes, stream_shapes)) { + std::cerr << "legacy/stream shape mismatch at iteration " << iter + << " seed " << options.seed << "\n"; + std::cerr << generated.obj_text << "\n"; + return 1; + } + + if (options.strict_opt && !options.allow_mtllib && legacy_ok && + !tinyobj_fuzz::LegacyAttribEqualsOpt(legacy_attrib, opt_attrib)) { + std::cerr << "legacy/opt attrib mismatch at iteration " << iter + << " seed " << options.seed << "\n"; + std::cerr << generated.obj_text << "\n"; + return 1; + } + + if (options.strict_opt && !options.allow_mtllib && legacy_ok && + !tinyobj_fuzz::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)) { + std::cerr << "legacy/opt shape mismatch at iteration " << iter + << " seed " << options.seed << "\n"; + std::cerr << generated.obj_text << "\n"; + return 1; + } + + // Exercise LoadObjOptTyped (TypedArray/arena path) — crash/ASAN check + { + std::string typed_warn, typed_err; + tinyobj::OptLoadConfig typed_config; + typed_config.triangulate = true; + typed_config.num_threads = 1; + typed_config.float_cache = (iter & 1u) != 0u; // alternate cache on/off + tinyobj::OptResult typed_result = tinyobj::LoadObjOptTyped( + generated.obj_text.data(), generated.obj_text.size(), + &typed_warn, &typed_err, typed_config); + (void)typed_result; + } + } + + std::cout << "obj_fuzz: completed " << options.iterations + << " iterations with seed " << options.seed << "\n"; + return 0; +} diff --git a/tests/opt/loadobjopt_multithread.inc b/tests/opt/loadobjopt_multithread.inc new file mode 100644 index 00000000..43219c6b --- /dev/null +++ b/tests/opt/loadobjopt_multithread.inc @@ -0,0 +1,3043 @@ +namespace opt_test { + +static bool IndexEquals(const tinyobj::index_t &lhs, const tinyobj::index_t &rhs) { + return lhs.vertex_index == rhs.vertex_index && + lhs.normal_index == rhs.normal_index && + lhs.texcoord_index == rhs.texcoord_index; +} + +template +static bool TagEquals(const LhsTag &lhs, const RhsTag &rhs) { + if (!(lhs.name == rhs.name) || lhs.intValues != rhs.intValues || + lhs.floatValues != rhs.floatValues || + lhs.stringValues.size() != rhs.stringValues.size()) { + return false; + } + for (size_t i = 0; i < lhs.stringValues.size(); i++) { + if (!(lhs.stringValues[i] == rhs.stringValues[i])) return false; + } + return true; +} + +template +static bool TagsEqual(const std::vector &lhs, + const std::vector &rhs) { + if (lhs.size() != rhs.size()) return false; + for (size_t i = 0; i < lhs.size(); i++) { + if (!TagEquals(lhs[i], rhs[i])) return false; + } + return true; +} + +template +static bool SkinWeightEquals(const LhsSkin &lhs, const RhsSkin &rhs) { + if (lhs.vertex_id != rhs.vertex_id) return false; + if (lhs.weightValues.size() != rhs.weightValues.size()) return false; + for (size_t i = 0; i < lhs.weightValues.size(); i++) { + if (lhs.weightValues[i].joint_id != rhs.weightValues[i].joint_id) { + return false; + } + if (!FloatEquals(static_cast(lhs.weightValues[i].weight), + static_cast(rhs.weightValues[i].weight))) { + return false; + } + } + return true; +} + +static bool MeshEquals(const tinyobj::basic_mesh_t<> &lhs, + const tinyobj::basic_mesh_t<> &rhs) { + if (lhs.indices.size() != rhs.indices.size()) return false; + for (size_t i = 0; i < lhs.indices.size(); i++) { + if (!IndexEquals(lhs.indices[i], rhs.indices[i])) return false; + } + + return lhs.num_face_vertices == rhs.num_face_vertices && + lhs.material_ids == rhs.material_ids && + lhs.smoothing_group_ids == rhs.smoothing_group_ids && + TagsEqual(lhs.tags, rhs.tags); +} + +static bool LinesEquals(const tinyobj::basic_lines_t<> &lhs, + const tinyobj::basic_lines_t<> &rhs) { + if (lhs.indices.size() != rhs.indices.size()) return false; + for (size_t i = 0; i < lhs.indices.size(); i++) { + if (!IndexEquals(lhs.indices[i], rhs.indices[i])) return false; + } + return lhs.num_line_vertices == rhs.num_line_vertices; +} + +static bool PointsEquals(const tinyobj::basic_points_t<> &lhs, + const tinyobj::basic_points_t<> &rhs) { + if (lhs.indices.size() != rhs.indices.size()) return false; + for (size_t i = 0; i < lhs.indices.size(); i++) { + if (!IndexEquals(lhs.indices[i], rhs.indices[i])) return false; + } + return true; +} + +static bool ShapeEquals(const tinyobj::basic_shape_t<> &lhs, + const tinyobj::basic_shape_t<> &rhs) { + return lhs.name == rhs.name && MeshEquals(lhs.mesh, rhs.mesh) && + LinesEquals(lhs.lines, rhs.lines) && PointsEquals(lhs.points, rhs.points); +} + +static bool AttribEquals(const tinyobj::basic_attrib_t<> &lhs, + const tinyobj::basic_attrib_t<> &rhs) { + if (lhs.vertices != rhs.vertices) return false; + if (lhs.vertex_weights != rhs.vertex_weights) return false; + if (lhs.normals != rhs.normals) return false; + if (lhs.texcoords != rhs.texcoords) return false; + if (lhs.texcoord_ws != rhs.texcoord_ws) return false; + if (lhs.colors != rhs.colors) return false; + if (lhs.skin_weights.size() != rhs.skin_weights.size()) return false; + for (size_t i = 0; i < lhs.skin_weights.size(); i++) { + if (!SkinWeightEquals(lhs.skin_weights[i], rhs.skin_weights[i])) return false; + } + if (lhs.face_num_verts != rhs.face_num_verts) return false; + if (lhs.material_ids != rhs.material_ids) return false; + if (lhs.indices.size() != rhs.indices.size()) return false; + + for (size_t i = 0; i < lhs.indices.size(); i++) { + if (!IndexEquals(lhs.indices[i], rhs.indices[i])) return false; + } + + return true; +} + +static bool ShapesEquals(const std::vector> &lhs, + const std::vector> &rhs) { + if (lhs.size() != rhs.size()) return false; + for (size_t i = 0; i < lhs.size(); i++) { + if (!ShapeEquals(lhs[i], rhs[i])) return false; + } + return true; +} + +static bool LegacyMeshEquals(const tinyobj::mesh_t &lhs, + const tinyobj::basic_mesh_t<> &rhs) { + if (lhs.indices.size() != rhs.indices.size()) return false; + for (size_t i = 0; i < lhs.indices.size(); i++) { + if (!IndexEquals(lhs.indices[i], rhs.indices[i])) return false; + } + + return lhs.num_face_vertices == rhs.num_face_vertices && + lhs.material_ids == rhs.material_ids && + lhs.smoothing_group_ids == rhs.smoothing_group_ids && + TagsEqual(lhs.tags, rhs.tags); +} + +static bool LegacyLinesEquals(const tinyobj::lines_t &lhs, + const tinyobj::basic_lines_t<> &rhs) { + if (lhs.indices.size() != rhs.indices.size()) return false; + for (size_t i = 0; i < lhs.indices.size(); i++) { + if (!IndexEquals(lhs.indices[i], rhs.indices[i])) return false; + } + return lhs.num_line_vertices == rhs.num_line_vertices; +} + +static bool LegacyPointsEquals(const tinyobj::points_t &lhs, + const tinyobj::basic_points_t<> &rhs) { + if (lhs.indices.size() != rhs.indices.size()) return false; + for (size_t i = 0; i < lhs.indices.size(); i++) { + if (!IndexEquals(lhs.indices[i], rhs.indices[i])) return false; + } + return true; +} + +static bool LegacyShapeEquals(const tinyobj::shape_t &lhs, + const tinyobj::basic_shape_t<> &rhs) { + return lhs.name == rhs.name && LegacyMeshEquals(lhs.mesh, rhs.mesh) && + LegacyLinesEquals(lhs.lines, rhs.lines) && + LegacyPointsEquals(lhs.points, rhs.points); +} + +static bool LegacyShapesEqualOpt( + const std::vector &lhs, + const std::vector> &rhs) { + if (lhs.size() != rhs.size()) return false; + for (size_t i = 0; i < lhs.size(); i++) { + if (!LegacyShapeEquals(lhs[i], rhs[i])) return false; + } + return true; +} + +static bool LegacyAttribEqualsOpt(const tinyobj::attrib_t &lhs, + const std::vector &lhs_shapes, + const tinyobj::basic_attrib_t<> &rhs) { + if (lhs.vertices != rhs.vertices) return false; + if (lhs.vertex_weights != rhs.vertex_weights) return false; + if (lhs.normals != rhs.normals) return false; + if (lhs.texcoords != rhs.texcoords) return false; + if (lhs.texcoord_ws != rhs.texcoord_ws) return false; + if (lhs.colors != rhs.colors) return false; + if (lhs.skin_weights.size() != rhs.skin_weights.size()) return false; + for (size_t i = 0; i < lhs.skin_weights.size(); i++) { + if (!SkinWeightEquals(lhs.skin_weights[i], rhs.skin_weights[i])) return false; + } + + std::vector legacy_indices; + std::vector legacy_face_num_verts; + std::vector legacy_material_ids; + for (size_t i = 0; i < lhs_shapes.size(); i++) { + legacy_indices.insert(legacy_indices.end(), lhs_shapes[i].mesh.indices.begin(), + lhs_shapes[i].mesh.indices.end()); + legacy_face_num_verts.insert(legacy_face_num_verts.end(), + lhs_shapes[i].mesh.num_face_vertices.begin(), + lhs_shapes[i].mesh.num_face_vertices.end()); + legacy_material_ids.insert(legacy_material_ids.end(), + lhs_shapes[i].mesh.material_ids.begin(), + lhs_shapes[i].mesh.material_ids.end()); + } + + if (legacy_face_num_verts != rhs.face_num_verts) return false; + if (legacy_material_ids != rhs.material_ids) return false; + if (legacy_indices.size() != rhs.indices.size()) return false; + for (size_t i = 0; i < legacy_indices.size(); i++) { + if (!IndexEquals(legacy_indices[i], rhs.indices[i])) return false; + } + + return true; +} + +static bool LegacyMaterialsEqualOpt( + const std::vector &lhs, + const std::vector &rhs) { + if (lhs.size() != rhs.size()) return false; + for (size_t i = 0; i < lhs.size(); i++) { + if (lhs[i].name != rhs[i].name) return false; + } + return true; +} + +static std::string BuildChunkedSyntheticObj(size_t group_count, + size_t faces_per_group) { + std::ostringstream os; + size_t vertex_index = 1; + size_t texcoord_index = 1; + size_t normal_index = 1; + + for (size_t group = 0; group < group_count; group++) { + os << "o object_" << group << "\n"; + os << "g group_" << group << "\n"; + + for (size_t face = 0; face < faces_per_group; face++) { + const float base = static_cast(group * faces_per_group + face); + os << "v " << base << " 0.0 0.0\n"; + os << "v " << base << " 1.0 0.0\n"; + os << "v " << base << " 0.0 1.0\n"; + + os << "vt 0.0 0.0\n"; + os << "vt 1.0 0.0\n"; + os << "vt 0.0 1.0\n"; + + os << "vn 0.0 0.0 1.0\n"; + os << "vn 0.0 0.0 1.0\n"; + os << "vn 0.0 0.0 1.0\n"; + + os << "f " + << vertex_index << "/" << texcoord_index << "/" << normal_index << " " + << (vertex_index + 1) << "/" << (texcoord_index + 1) << "/" + << (normal_index + 1) << " " + << (vertex_index + 2) << "/" << (texcoord_index + 2) << "/" + << (normal_index + 2) << "\n"; + + vertex_index += 3; + texcoord_index += 3; + normal_index += 3; + } + } + + return os.str(); +} + +static std::string BuildRelativeIndexObj(size_t face_count) { + std::ostringstream os; + for (size_t face = 0; face < face_count; face++) { + const float base = static_cast(face); + os << "v " << base << " 0.0 0.0\n"; + os << "v " << base << " 1.0 0.0\n"; + os << "v " << base << " 0.0 1.0\n"; + os << "f -3 -2 -1\n"; + } + return os.str(); +} + +static void RunOptLoad(const std::string &obj_text, int num_threads, + tinyobj::basic_attrib_t<> *attrib, + std::vector> *shapes, + std::vector *materials, + std::string *warn, std::string *err) { + tinyobj::OptLoadConfig config; + config.num_threads = num_threads; + config.triangulate = true; + + bool ok = tinyobj::LoadObjOpt(attrib, shapes, materials, warn, err, + obj_text.c_str(), obj_text.size(), config); + if (!ok && !err->empty()) { + std::cerr << "LoadObjOpt failed: " << *err << "\n"; + } + TEST_CHECK(ok == true); +} + +static bool WriteTextFile(const std::string &path, const std::string &content) { +#ifdef _WIN32 + std::ofstream out(LongPathW(UTF8ToWchar(path)).c_str(), std::ios::binary); +#else + std::ofstream out(path.c_str(), std::ios::binary); +#endif + if (!out) return false; + out << content; + return !out.fail(); +} + +static std::string BuildTriangleSoupObj(size_t triangle_count) { + std::ostringstream os; + size_t vertex_index = 1; + + for (size_t i = 0; i < triangle_count; i++) { + const float base = static_cast(i); + os << "v " << base << " 0.0 0.0\n"; + os << "v " << base << " 1.0 0.0\n"; + os << "v " << base << " 0.0 1.0\n"; + os << "f " << vertex_index << " " << (vertex_index + 1) << " " + << (vertex_index + 2) << "\n"; + vertex_index += 3; + } + + return os.str(); +} + +static std::string BuildPointsOnlyObj(size_t vertex_count) { + std::ostringstream os; + for (size_t i = 0; i < vertex_count; i++) { + const float base = static_cast(i); + os << "v " << base << " " << (base + 0.5f) << " 0.0\n"; + os << "p " << (i + 1) << "\n"; + } + return os.str(); +} + +static std::string BuildFacesOnlyObj(size_t face_count) { + std::ostringstream os; + for (size_t i = 0; i < face_count; i++) { + const size_t base = i * 3 + 1; + os << "f " << base << " " << (base + 1) << " " << (base + 2) << "\n"; + } + return os.str(); +} + +static std::string BuildInvalidFaceTokenObj() { + return + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 bad 3\n"; +} + +static std::string BuildTexcoordWObj() { + return + "vt 0.0 0.0 0.5\n" + "vt 1.0 0.0\n" + "vt 0.0 1.0 0.25\n"; +} + +static std::string BuildExtendedFeatureObj() { + return + "o extended\n" + "v 0.0 0.0 0.0 0.5\n" + "v 1.0 0.0 0.0 0.75\n" + "v 0.0 1.0 0.0 1.0\n" + "vt 0.0 0.0 0.25\n" + "vt 1.0 0.0 0.0\n" + "vt 0.0 1.0 0.5\n" + "vw 0 1 0.75 2 0.25\n" + "l 1/1 2/2 3/3\n" + "p 3\n" + "t crease 1/1/1 7 0.5 hard\n" + "f 1/1 2/2 3/3\n"; +} + +static std::string BuildWeightedVertexTexcoordObj() { + return + "v 0.0 0.0 0.0 0.5\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0 0.75\n" + "vt 0.0 0.0 0.25\n" + "vt 1.0 0.0\n" + "vt 0.0 1.0 0.5\n" + "f 1/1 2/2 3/3\n"; +} + +static std::string BuildSingleComponentTexcoordObj() { + return "vt 0.5\n"; +} + +static std::string BuildEmbeddedCarriageReturnObj() { + return + "o seed\n" + "g s\reed_group\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "vt 0.0 0.0\n" + "vt 1.0 0.0\n" + "vt 0.0 1.0\n" + "usemtl default\n" + "s 1\n" + "f 1/1/1 2/2/1 3/8/1\n"; +} + +static std::string BuildEmbeddedNulEmptyShapeObj() { + std::string obj = + "o seed\n" + "g seed_grou\n" + "vn 0.0 0.0 1.0\n" + "usemtl default\n" + "s 1\n" + "f 1/1\n" + "usemtl defau_"; + obj.append(7, '\0'); + obj += " 1/1/1 2/2/1 3/3/1\n"; + return obj; +} + +static std::string BuildObjectOnlyObj() { + return "o seedo\n"; +} + +static std::string BuildGroupOnlyObj() { + return "g group_only\n"; +} + +static std::string BuildDegenerateFaceOnlyObj() { + return + "o obj\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "f 1 2\n"; +} + +static std::string BuildDegenerateFaceThenMissingUsemtlObj() { + return + "g grp\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "f 1 2\n" + "usemtl missing\n"; +} + +static std::string BuildDegenerateFaceThenInvalidFaceObj() { + return + "g grp\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "f 1 2\n" + "f 1 bad 3\n"; +} + +static std::string BuildInitialNamedShapeThenBoundaryObj() { + return + "o root_object\n" + "g first_group\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "v 0.0 0.0 1.0\n" + "f 1 2 3\n" + "g second_group\n" + "f 1 3 4\n"; +} + +static std::string BuildInvalidRelativeVertexIndexObj() { + return + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f -4 -3 -2\n"; +} + +static std::string BuildInvalidRelativeTexcoordNormalIndexObj() { + return + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "vt 0.0 0.0\n" + "vt 1.0 0.0\n" + "vt 0.0 1.0\n" + "vn 0.0 0.0 1.0\n" + "f 1/-4/-2 2/2/1 3/3/1\n"; +} + +static std::string BuildAcceptedOutOfBoundsFaceObj() { + return + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "vt 0.0 0.0\n" + "vn 0.0 0.0 1.0\n" + "f 4/2/2 1/1/1 2/1/1\n"; +} + +static std::string BuildZeroVertexIndexThenMissingMaterialObj() { + return + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 0 1 2\n" + "usemtl later_missing\n"; +} + +static std::string BuildZeroTexcoordNormalIndexObj() { + return + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "vt 0.0 0.0\n" + "vn 0.0 0.0 1.0\n" + "f 1/0/0 2/1/1 3/1/1\n"; +} + +static size_t ParseBenchmarkTriCount() { + const char *env = std::getenv("TINYOBJ_OPT_BENCH_TRIS"); + if (!env || env[0] == '\0') { + return 100000; // sane default for normal test runs + } + + char *end = NULL; + unsigned long long parsed = strtoull(env, &end, 10); + if (end == env || (end && *end != '\0')) { + return 100000; + } + + // Empirical slope from local runs is ~0.93 KB / triangle for the + // optimized synthetic benchmark binary built without ASAN. + // Clamp to a conservative ~48 GiB budget with some headroom. + const unsigned long long max_triangles = 45000000ULL; + if (parsed > max_triangles) { + parsed = max_triangles; + } + + if (parsed == 0) { + parsed = 1; + } + + return static_cast(parsed); +} + +static size_t ParseCorrectnessTriCount() { + const char *env = std::getenv("TINYOBJ_OPT_COMPARE_TRIS"); + if (!env || env[0] == '\0') { + return 100000; + } + + char *end = NULL; + unsigned long long parsed = strtoull(env, &end, 10); + if (end == env || (end && *end != '\0')) { + return 100000; + } + + if (parsed == 0) { + parsed = 1; + } + + if (parsed > 100000ULL) { + parsed = 100000ULL; + } + + return static_cast(parsed); +} + +static std::string BuildThreadedMaterialObj(size_t face_count_per_material) { + std::ostringstream os; + size_t vertex_index = 1; + os << "mtllib materials.mtl\n"; + os << "usemtl mat_a\n"; + + for (size_t i = 0; i < face_count_per_material; i++) { + const float base = static_cast(i); + os << "v " << base << " 0.0 0.0\n"; + os << "v " << base << " 1.0 0.0\n"; + os << "v " << base << " 0.0 1.0\n"; + os << "f " << vertex_index << " " << (vertex_index + 1) << " " + << (vertex_index + 2) << "\n"; + vertex_index += 3; + } + + os << "usemtl mat_b\n"; + for (size_t i = 0; i < face_count_per_material; i++) { + const float base = static_cast(face_count_per_material + i); + os << "v " << base << " 0.0 0.0\n"; + os << "v " << base << " 1.0 0.0\n"; + os << "v " << base << " 0.0 1.0\n"; + os << "f " << vertex_index << " " << (vertex_index + 1) << " " + << (vertex_index + 2) << "\n"; + vertex_index += 3; + } + + return os.str(); +} + +static std::string BuildColorAndSmoothingObj() { + std::ostringstream os; + os << "mtllib mat.mtl\n"; + os << "usemtl mat_a # trailing comment must be ignored\n"; + os << "v 0.0 0.0 0.0 1.0 0.0 0.0\n"; + os << "v 1.0 0.0 0.0 0.0 1.0 0.0\n"; + os << "v 0.0 1.0 0.0 0.0 0.0 1.0\n"; + os << "s 7\n"; + os << "f 1 2 3\n"; + os << "s off\n"; + os << "g second\n"; + os << "v 0.0 0.0 1.0 0.5 0.5 0.5\n"; + os << "v 1.0 0.0 1.0 0.2 0.2 0.2\n"; + os << "v 0.0 1.0 1.0 0.8 0.8 0.8\n"; + os << "f 4 5 6\n"; + return os.str(); +} + +static std::string BuildMixedColorObj() { + std::ostringstream os; + os << "v 0.0 0.0 0.0\n"; + os << "v 1.0 0.0 0.0 0.1 0.2 0.3\n"; + os << "v 0.0 1.0 0.0\n"; + os << "f 1 2 3\n"; + return os.str(); +} + +static std::string BuildFaceCommentObj() { + std::ostringstream os; + os << "v 0.0 0.0 0.0\n"; + os << "v 1.0 0.0 0.0\n"; + os << "v 0.0 1.0 0.0\n"; + os << "f 1 2 3 # trailing comment should be ignored\n"; + return os.str(); +} + +static std::string BuildConcavePolygonObj() { + std::ostringstream os; + os << "v 0.0 0.0 0.0\n"; + os << "v 2.0 0.0 0.0\n"; + os << "v 2.0 2.0 0.0\n"; + os << "v 1.0 1.0 0.0\n"; + os << "v 0.0 2.0 0.0\n"; + os << "f 1 2 3 4 5\n"; + return os.str(); +} + +static std::string BuildInvalidPolygonObj() { + std::ostringstream os; + os << "v 0.0 0.0 0.0\n"; + os << "v 1.0 0.0 0.0\n"; + os << "v 1.0 1.0 0.0\n"; + os << "v 0.0 1.0 0.0\n"; + os << "f 1 2 3 4 5\n"; + return os.str(); +} + +static std::string BuildUsemtlBeforeMtllibObj() { + return + "usemtl mat_early\n" + "mtllib mats.mtl\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; +} + +static std::string BuildGroupCommentObj() { + return + "g foo # trailing comment should be ignored\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; +} + +static std::string BuildWeightedVertexCommentObj() { + return + "v 0.0 0.0 0.0 0.5 # trailing comment must be ignored\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; +} + +static std::string BuildEmptyGroupObj() { + return + "g # empty group should warn and reset the current group\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; +} + +static std::string BuildOptionalVertexFallbackObj() { + return + "v 0.0 0.0 0.0 foo\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; +} + +static std::string BuildObjectSpacingObj() { + return + "o foo \n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; +} + +static std::string BuildEmptyMtllibObj() { + return + "mtllib \n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; +} + +static std::string BuildInvalidVertexRecordObj() { + return "v a 0.0 0.0\n"; +} + +} // namespace opt_test + +void test_loadobjopt_multithread_matches_single_thread() { + const std::string obj_text = opt_test::BuildChunkedSyntheticObj(6, 24); + + tinyobj::basic_attrib_t<> single_attrib; + tinyobj::basic_attrib_t<> multi_attrib; + std::vector> single_shapes; + std::vector> multi_shapes; + std::vector single_materials; + std::vector multi_materials; + std::string single_warn; + std::string single_err; + std::string multi_warn; + std::string multi_err; + + opt_test::RunOptLoad(obj_text, 1, &single_attrib, &single_shapes, + &single_materials, &single_warn, &single_err); + opt_test::RunOptLoad(obj_text, 4, &multi_attrib, &multi_shapes, + &multi_materials, &multi_warn, &multi_err); + + TEST_CHECK(single_warn == multi_warn); + TEST_CHECK(single_materials.size() == multi_materials.size()); + TEST_CHECK(opt_test::AttribEquals(single_attrib, multi_attrib)); + TEST_CHECK(opt_test::ShapesEquals(single_shapes, multi_shapes)); + TEST_CHECK(multi_shapes.size() == 6); +} + +void test_loadobjopt_multithread_relative_indices_match_single_thread() { + const std::string obj_text = opt_test::BuildRelativeIndexObj(256); + + tinyobj::basic_attrib_t<> single_attrib; + tinyobj::basic_attrib_t<> multi_attrib; + std::vector> single_shapes; + std::vector> multi_shapes; + std::vector single_materials; + std::vector multi_materials; + std::string single_warn; + std::string single_err; + std::string multi_warn; + std::string multi_err; + + opt_test::RunOptLoad(obj_text, 1, &single_attrib, &single_shapes, + &single_materials, &single_warn, &single_err); + opt_test::RunOptLoad(obj_text, 8, &multi_attrib, &multi_shapes, + &multi_materials, &multi_warn, &multi_err); + + TEST_CHECK(single_warn == multi_warn); + TEST_CHECK(single_materials.size() == multi_materials.size()); + TEST_CHECK(opt_test::AttribEquals(single_attrib, multi_attrib)); + TEST_CHECK(opt_test::ShapesEquals(single_shapes, multi_shapes)); + TEST_CHECK(multi_attrib.face_num_verts.size() == 256); +} + +void test_loadobjopt_mtllib_multiple_filenames() { +#ifdef _WIN32 + const char path_sep = '\\'; + std::string test_dir = WcharToUTF8(L"") + std::string(); + wchar_t wtmpbuf[MAX_PATH]; + GetTempPathW(MAX_PATH, wtmpbuf); + test_dir = WcharToUTF8(wtmpbuf) + "tinyobj_opt_mtllib_multi\\"; +#else + const char path_sep = '/'; + std::string test_dir = "/tmp/tinyobj_opt_mtllib_multi/"; +#endif + + RemoveTestDir(test_dir); + TEST_CHECK(MakeDir(test_dir)); + + const std::string obj_path = test_dir + "scene.obj"; + const std::string mtl_path = test_dir + "good.mtl"; + const std::string obj_text = + "mtllib missing.mtl good.mtl\n" + "usemtl mat_ok\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; + const std::string mtl_text = + "newmtl mat_ok\n" + "Kd 1.0 0.0 0.0\n"; + + TEST_CHECK(opt_test::WriteTextFile(obj_path, obj_text)); + TEST_CHECK(opt_test::WriteTextFile(mtl_path, mtl_text)); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.num_threads = 1; + config.triangulate = true; + + bool ok = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_path.c_str(), + static_cast(NULL), config); + RemoveTestDir(test_dir); + + TEST_CHECK(ok == true); + TEST_CHECK(err.empty()); + TEST_CHECK(materials.size() == 1); + TEST_CHECK(materials[0].name == "mat_ok"); + TEST_CHECK(attrib.material_ids.size() == 1); + TEST_CHECK(attrib.material_ids[0] == 0); + TEST_CHECK(warn.find("Failed to load material file(s)") == std::string::npos); +} + +void test_loadobjopt_mtllib_repeated_lines() { +#ifdef _WIN32 + std::string test_dir; + wchar_t wtmpbuf[MAX_PATH]; + GetTempPathW(MAX_PATH, wtmpbuf); + test_dir = WcharToUTF8(wtmpbuf) + "tinyobj_opt_mtllib_repeat\\"; +#else + std::string test_dir = "/tmp/tinyobj_opt_mtllib_repeat/"; +#endif + + RemoveTestDir(test_dir); + TEST_CHECK(MakeDir(test_dir)); + + const std::string obj_path = test_dir + "scene.obj"; + const std::string mtl_a_path = test_dir + "a.mtl"; + const std::string mtl_b_path = test_dir + "b.mtl"; + const std::string obj_text = + "mtllib a.mtl\n" + "mtllib b.mtl\n" + "usemtl mat_b\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; + const std::string mtl_a_text = + "newmtl mat_a\n" + "Kd 1.0 0.0 0.0\n"; + const std::string mtl_b_text = + "newmtl mat_b\n" + "Kd 0.0 1.0 0.0\n"; + + TEST_CHECK(opt_test::WriteTextFile(obj_path, obj_text)); + TEST_CHECK(opt_test::WriteTextFile(mtl_a_path, mtl_a_text)); + TEST_CHECK(opt_test::WriteTextFile(mtl_b_path, mtl_b_text)); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.num_threads = 1; + config.triangulate = true; + + bool ok = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_path.c_str(), + static_cast(NULL), config); + RemoveTestDir(test_dir); + + TEST_CHECK(ok == true); + TEST_CHECK(err.empty()); + TEST_CHECK(materials.size() == 2); + TEST_CHECK(attrib.material_ids.size() == 1); + TEST_CHECK(attrib.material_ids[0] == 1); +} + +void test_loadobjopt_points_only_input() { + const std::string obj_text = opt_test::BuildPointsOnlyObj(256); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, + &legacy_err, &legacy_stream, NULL, true, + false); + if (!legacy_ok && !legacy_err.empty()) { + std::cerr << "Legacy LoadObj failed: " << legacy_err << "\n"; + } + TEST_CHECK(legacy_ok == true); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + + tinyobj::OptLoadConfig config; + config.num_threads = 4; + config.triangulate = true; + + bool ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(ok == true); + TEST_CHECK(opt_err.empty()); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); + TEST_CHECK(opt_materials.size() == legacy_materials.size()); +} + +void test_loadobjopt_faces_only_input() { + const std::string obj_text = opt_test::BuildFacesOnlyObj(128); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.num_threads = 4; + config.triangulate = true; + + bool ok = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text.c_str(), obj_text.size(), config); + + TEST_CHECK(ok == true); + TEST_CHECK(err.empty()); + TEST_CHECK(attrib.vertices.empty()); + TEST_CHECK(attrib.indices.size() == 128 * 3); + TEST_CHECK(attrib.face_num_verts.size() == 128); + TEST_CHECK(!shapes.empty()); +} + +void test_loadobjopt_synthetic_benchmark() { + const size_t triangle_count = opt_test::ParseBenchmarkTriCount(); + const std::string obj_text = opt_test::BuildTriangleSoupObj(triangle_count); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.num_threads = 4; + config.triangulate = true; + + const clock_t begin = clock(); + bool ok = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text.c_str(), obj_text.size(), config); + const double elapsed_sec = + static_cast(clock() - begin) / static_cast(CLOCKS_PER_SEC); + + TEST_CHECK(ok == true); + TEST_CHECK(err.empty()); + TEST_CHECK(attrib.vertices.size() == triangle_count * 9); + TEST_CHECK(attrib.indices.size() == triangle_count * 3); + TEST_CHECK(attrib.face_num_verts.size() == triangle_count); + TEST_MSG("LoadObjOpt benchmark: %lu triangles in %.3f sec", + static_cast(triangle_count), elapsed_sec); +} + +void test_loadobjopt_matches_legacy_parser_triangle_soup() { + const size_t triangle_count = opt_test::ParseCorrectnessTriCount(); + const std::string obj_text = opt_test::BuildTriangleSoupObj(triangle_count); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + if (!legacy_ok && !legacy_err.empty()) { + std::cerr << "Legacy LoadObj failed: " << legacy_err << "\n"; + } + TEST_CHECK(legacy_ok == true); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + + tinyobj::OptLoadConfig config; + config.num_threads = 4; + config.triangulate = true; + + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + if (!opt_ok && !opt_err.empty()) { + std::cerr << "LoadObjOpt failed: " << opt_err << "\n"; + } + TEST_CHECK(opt_ok == true); + + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_materials.size() == opt_materials.size()); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); + TEST_CHECK(opt_attrib.face_num_verts.size() == triangle_count); +} + +void test_loadobjopt_matches_legacy_parser_mtllib_threaded() { +#ifdef _WIN32 + std::string test_dir; + wchar_t wtmpbuf[MAX_PATH]; + GetTempPathW(MAX_PATH, wtmpbuf); + test_dir = WcharToUTF8(wtmpbuf) + "tinyobj_opt_compare_mtllib\\"; +#else + std::string test_dir = "/tmp/tinyobj_opt_compare_mtllib/"; +#endif + + RemoveTestDir(test_dir); + TEST_CHECK(MakeDir(test_dir)); + + const std::string obj_path = test_dir + "scene.obj"; + const std::string mtl_path = test_dir + "materials.mtl"; + const std::string obj_text = opt_test::BuildThreadedMaterialObj(256); + const std::string mtl_text = + "newmtl mat_a\n" + "Kd 1.0 0.0 0.0\n" + "newmtl mat_b\n" + "Kd 0.0 1.0 0.0\n"; + + TEST_CHECK(opt_test::WriteTextFile(obj_path, obj_text)); + TEST_CHECK(opt_test::WriteTextFile(mtl_path, mtl_text)); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, + &legacy_err, obj_path.c_str(), + test_dir.c_str(), true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 4; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_path.c_str(), + test_dir.c_str(), config); + + RemoveTestDir(test_dir); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyMaterialsEqualOpt(legacy_materials, opt_materials)); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_loadobjopt_matches_legacy_parser_colors_smoothing_and_comments() { + const std::string obj_text = opt_test::BuildColorAndSmoothingObj(); + const std::string mtl_text = + "newmtl mat_a\n" + "Kd 1.0 0.0 0.0\n"; +#ifdef _WIN32 + std::string test_dir; + wchar_t wtmpbuf[MAX_PATH]; + GetTempPathW(MAX_PATH, wtmpbuf); + test_dir = WcharToUTF8(wtmpbuf) + "tinyobj_opt_compare_colors\\"; +#else + std::string test_dir = "/tmp/tinyobj_opt_compare_colors/"; +#endif + + RemoveTestDir(test_dir); + TEST_CHECK(MakeDir(test_dir)); + + const std::string obj_path = test_dir + "scene.obj"; + const std::string mtl_path = test_dir + "mat.mtl"; + TEST_CHECK(opt_test::WriteTextFile(obj_path, obj_text)); + TEST_CHECK(opt_test::WriteTextFile(mtl_path, mtl_text)); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + obj_path.c_str(), test_dir.c_str(), true, + false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 4; + config.triangulate = true; + + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_path.c_str(), + test_dir.c_str(), config); + + RemoveTestDir(test_dir); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyMaterialsEqualOpt(legacy_materials, opt_materials)); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_loadobjopt_mixed_vertex_colors_drop_like_legacy() { + const std::string obj_text = opt_test::BuildMixedColorObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_attrib.colors.empty()); + TEST_CHECK(opt_attrib.colors.empty()); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_stream_loader_mixed_vertex_colors_drop_like_legacy() { + const std::string obj_text = opt_test::BuildMixedColorObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &attrib, &shapes, &materials, &warn, &err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(ok == true); + TEST_CHECK(err.empty()); + TEST_CHECK(legacy_attrib.colors.empty()); + TEST_CHECK(attrib.colors.empty()); + TEST_CHECK(legacy_attrib.vertices == attrib.vertices); + TEST_CHECK(legacy_attrib.vertex_weights == attrib.vertex_weights); + TEST_CHECK(legacy_attrib.normals == attrib.normals); + TEST_CHECK(legacy_attrib.texcoords == attrib.texcoords); + TEST_CHECK(legacy_attrib.texcoord_ws == attrib.texcoord_ws); + TEST_CHECK(legacy_shapes.size() == shapes.size()); + TEST_CHECK(legacy_shapes[0].mesh.indices.size() == shapes[0].mesh.indices.size()); + TEST_CHECK(legacy_shapes[0].mesh.num_face_vertices == + shapes[0].mesh.num_face_vertices); + TEST_CHECK(legacy_shapes[0].mesh.material_ids == shapes[0].mesh.material_ids); +} + +void test_stream_loader_mixed_vertex_colors_backfill() { + const std::string obj_text = opt_test::BuildMixedColorObj(); + + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + config.default_vcols_fallback = true; + bool ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &attrib, &shapes, &materials, &warn, &err, &stream, NULL, config); + + TEST_CHECK(ok == true); + TEST_CHECK(err.empty()); + TEST_CHECK(attrib.vertices.size() == 9); + TEST_CHECK(attrib.colors.size() == 9); + TEST_CHECK(FloatEquals(1.0f, attrib.colors[0])); + TEST_CHECK(FloatEquals(1.0f, attrib.colors[1])); + TEST_CHECK(FloatEquals(1.0f, attrib.colors[2])); + TEST_CHECK(FloatEquals(0.1f, attrib.colors[3])); + TEST_CHECK(FloatEquals(0.2f, attrib.colors[4])); + TEST_CHECK(FloatEquals(0.3f, attrib.colors[5])); + TEST_CHECK(FloatEquals(1.0f, attrib.colors[6])); + TEST_CHECK(FloatEquals(1.0f, attrib.colors[7])); + TEST_CHECK(FloatEquals(1.0f, attrib.colors[8])); +} + +void test_stream_loader_mtllib_escaped_spaces() { +#ifdef _WIN32 + std::string test_dir; + wchar_t wtmpbuf[MAX_PATH]; + GetTempPathW(MAX_PATH, wtmpbuf); + test_dir = WcharToUTF8(wtmpbuf) + "tinyobj_stream_mtllib_space\\"; +#else + std::string test_dir = "/tmp/tinyobj_stream_mtllib_space/"; +#endif + + RemoveTestDir(test_dir); + TEST_CHECK(MakeDir(test_dir)); + + const std::string obj_path = test_dir + "scene.obj"; + const std::string mtl_path = test_dir + "my file.mtl"; + const std::string obj_text = + "mtllib my\\ file.mtl\n" + "usemtl mat_ok\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; + const std::string mtl_text = + "newmtl mat_ok\n" + "Kd 0.2 0.3 0.4\n"; + + TEST_CHECK(opt_test::WriteTextFile(obj_path, obj_text)); + TEST_CHECK(opt_test::WriteTextFile(mtl_path, mtl_text)); + + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + tinyobj::experimental_stream::StreamLoadConfig config; + bool ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &attrib, &shapes, &materials, &warn, &err, obj_path.c_str(), + static_cast(NULL), config); + + RemoveTestDir(test_dir); + + TEST_CHECK(ok == true); + TEST_CHECK(err.empty()); + TEST_CHECK(materials.size() == 1); + TEST_CHECK(materials[0].name == "mat_ok"); +} + +void test_stream_loader_clears_outputs_on_failure() { + const std::string obj_text = + "v 0.0 0.0 0.0\n" + "f bad bad bad\n"; + + tinyobj::attrib_t attrib; + attrib.vertices.push_back(42.0f); + std::vector shapes(1); + std::vector materials(1); + std::string warn, err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &attrib, &shapes, &materials, &warn, &err, &stream, NULL, config); + + TEST_CHECK(ok == false); + TEST_CHECK(!err.empty()); + TEST_CHECK(attrib.vertices.empty()); + TEST_CHECK(attrib.colors.empty()); + TEST_CHECK(shapes.empty()); + TEST_CHECK(materials.empty()); +} + +void test_loadobjopt_face_comment_matches_legacy() { + const std::string obj_text = opt_test::BuildFaceCommentObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_loadobjopt_concave_polygon_matches_legacy() { + const std::string obj_text = opt_test::BuildConcavePolygonObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 4; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_stream_loader_concave_polygon_matches_legacy() { + const std::string obj_text = opt_test::BuildConcavePolygonObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + config.triangulate = true; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, &stream_err, + &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); + TEST_CHECK(legacy_attrib.vertices == stream_attrib.vertices); + TEST_CHECK(legacy_shapes.size() == stream_shapes.size()); + TEST_CHECK(legacy_shapes[0].mesh.indices.size() == stream_shapes[0].mesh.indices.size()); + for (size_t i = 0; i < legacy_shapes[0].mesh.indices.size(); i++) { + TEST_CHECK(opt_test::IndexEquals(legacy_shapes[0].mesh.indices[i], + stream_shapes[0].mesh.indices[i])); + } +} + +void test_loadobjopt_invalid_polygon_matches_legacy() { + const std::string obj_text = opt_test::BuildInvalidPolygonObj(); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 4; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(opt_ok == true); + TEST_CHECK(opt_err.empty()); + TEST_CHECK(opt_attrib.face_num_verts.size() == opt_attrib.material_ids.size()); + size_t total_face_indices = 0; + for (size_t i = 0; i < opt_attrib.face_num_verts.size(); i++) { + total_face_indices += static_cast(opt_attrib.face_num_verts[i]); + } + TEST_CHECK(total_face_indices == opt_attrib.indices.size()); + for (size_t i = 0; i < opt_shapes.size(); i++) { + TEST_CHECK(opt_shapes[i].mesh.num_face_vertices.size() == + opt_shapes[i].mesh.material_ids.size()); + size_t shape_index_count = 0; + for (size_t k = 0; k < opt_shapes[i].mesh.num_face_vertices.size(); k++) { + shape_index_count += + static_cast(opt_shapes[i].mesh.num_face_vertices[k]); + } + TEST_CHECK(shape_index_count == opt_shapes[i].mesh.indices.size()); + } +} + +void test_stream_loader_hash_in_names() { +#ifdef _WIN32 + std::string test_dir; + wchar_t wtmpbuf[MAX_PATH]; + GetTempPathW(MAX_PATH, wtmpbuf); + test_dir = WcharToUTF8(wtmpbuf) + "tinyobj_stream_hash_names\\"; +#else + std::string test_dir = "/tmp/tinyobj_stream_hash_names/"; +#endif + + RemoveTestDir(test_dir); + TEST_CHECK(MakeDir(test_dir)); + + const std::string obj_path = test_dir + "scene.obj"; + const std::string mtl_path = test_dir + "lib#1.mtl"; + const std::string obj_text = + "mtllib lib#1.mtl\n" + "o object#1\n" + "usemtl mat#1\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; + const std::string mtl_text = + "newmtl mat#1\n" + "Kd 1.0 0.0 0.0\n"; + + TEST_CHECK(opt_test::WriteTextFile(obj_path, obj_text)); + TEST_CHECK(opt_test::WriteTextFile(mtl_path, mtl_text)); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, obj_path.c_str(), static_cast(NULL), config); + + RemoveTestDir(test_dir); + + TEST_CHECK(stream_ok == true); + TEST_CHECK(stream_err.empty()); + TEST_CHECK(stream_materials.size() == 1); + TEST_CHECK(stream_materials[0].name == "mat#1"); + TEST_CHECK(stream_shapes.size() == 1); + TEST_CHECK(stream_shapes[0].name == "object#1"); + TEST_CHECK(stream_shapes[0].mesh.material_ids.size() == 1); + TEST_CHECK(stream_shapes[0].mesh.material_ids[0] == 0); +} + +void test_loadobjopt_hash_in_usemtl_matches_legacy() { + const std::string obj_text = + "mtllib mats.mtl\n" + "usemtl mat#1\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; + const std::string mtl_text = + "newmtl mat#1\n" + "Kd 1.0 0.0 0.0\n"; +#ifdef _WIN32 + std::string test_dir; + wchar_t wtmpbuf[MAX_PATH]; + GetTempPathW(MAX_PATH, wtmpbuf); + test_dir = WcharToUTF8(wtmpbuf) + "tinyobj_opt_hash_usemtl\\"; +#else + std::string test_dir = "/tmp/tinyobj_opt_hash_usemtl/"; +#endif + + RemoveTestDir(test_dir); + TEST_CHECK(MakeDir(test_dir)); + + const std::string obj_path = test_dir + "scene.obj"; + const std::string mtl_path = test_dir + "mats.mtl"; + TEST_CHECK(opt_test::WriteTextFile(obj_path, obj_text)); + TEST_CHECK(opt_test::WriteTextFile(mtl_path, mtl_text)); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + obj_path.c_str(), test_dir.c_str(), true, + false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_path.c_str(), + test_dir.c_str(), config); + + RemoveTestDir(test_dir); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(opt_test::LegacyMaterialsEqualOpt(legacy_materials, opt_materials)); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_loadobjopt_usemtl_before_mtllib_matches_legacy() { +#ifdef _WIN32 + std::string test_dir; + wchar_t wtmpbuf[MAX_PATH]; + GetTempPathW(MAX_PATH, wtmpbuf); + test_dir = WcharToUTF8(wtmpbuf) + "tinyobj_opt_usemtl_before_mtllib\\"; +#else + std::string test_dir = "/tmp/tinyobj_opt_usemtl_before_mtllib/"; +#endif + + RemoveTestDir(test_dir); + TEST_CHECK(MakeDir(test_dir)); + + const std::string obj_path = test_dir + "scene.obj"; + const std::string mtl_path = test_dir + "mats.mtl"; + const std::string obj_text = opt_test::BuildUsemtlBeforeMtllibObj(); + const std::string mtl_text = + "newmtl mat_early\n" + "Kd 1.0 0.0 0.0\n"; + + TEST_CHECK(opt_test::WriteTextFile(obj_path, obj_text)); + TEST_CHECK(opt_test::WriteTextFile(mtl_path, mtl_text)); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + obj_path.c_str(), test_dir.c_str(), true, + false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_path.c_str(), + test_dir.c_str(), config); + + RemoveTestDir(test_dir); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyMaterialsEqualOpt(legacy_materials, opt_materials)); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, + opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_loadobjopt_group_comment_matches_legacy() { + const std::string obj_text = opt_test::BuildGroupCommentObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, + opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_loadobjopt_invalid_face_token_matches_legacy_error() { + const std::string obj_text = opt_test::BuildInvalidFaceTokenObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == false); + TEST_CHECK(opt_ok == false); + TEST_CHECK(opt_warn.empty()); + TEST_CHECK(opt_err.find("failed to parse `f' line (invalid vertex index)") != + std::string::npos); + TEST_CHECK(opt_attrib.vertices.empty()); + TEST_CHECK(opt_shapes.empty()); +} + +void test_stream_loader_texcoord_w() { + const std::string obj_text = opt_test::BuildTexcoordWObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); + TEST_CHECK(legacy_attrib.texcoords == stream_attrib.texcoords); + TEST_CHECK(legacy_attrib.texcoord_ws == stream_attrib.texcoord_ws); +} + +void test_stream_loader_vertex_weight() { + const std::string obj_text = + "v 0.0 0.0 0.0 0.5\n" + "v 1.0 0.0 0.0\n"; + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); + TEST_CHECK(legacy_attrib.vertex_weights == stream_attrib.vertex_weights); +} + +void test_stream_loader_colored_vertex_weight() { + const std::string obj_text = + "v 0.0 0.0 0.0 0.1 0.2 0.3\n" + "v 1.0 0.0 0.0 0.4 0.5 0.6\n"; + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); + TEST_CHECK(legacy_attrib.vertex_weights == stream_attrib.vertex_weights); + TEST_CHECK(legacy_attrib.colors == stream_attrib.colors); +} + +void test_stream_loader_group_comment_matches_legacy() { + const std::string obj_text = opt_test::BuildGroupCommentObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); + TEST_CHECK(legacy_attrib.vertices == stream_attrib.vertices); + TEST_CHECK(legacy_shapes.size() == stream_shapes.size()); + TEST_CHECK(legacy_shapes[0].name == stream_shapes[0].name); + TEST_CHECK(legacy_shapes[0].mesh.num_face_vertices == + stream_shapes[0].mesh.num_face_vertices); +} + +void test_stream_loader_weighted_vertex_comment_matches_legacy() { + const std::string obj_text = opt_test::BuildWeightedVertexCommentObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); + TEST_CHECK(legacy_attrib.vertices == stream_attrib.vertices); + TEST_CHECK(legacy_attrib.vertex_weights == stream_attrib.vertex_weights); + TEST_CHECK(legacy_shapes.size() == stream_shapes.size()); + TEST_CHECK(legacy_shapes[0].mesh.num_face_vertices == + stream_shapes[0].mesh.num_face_vertices); +} + +void test_loadobjopt_mtllib_warning_preserved_on_parse_error() { +#ifdef _WIN32 + std::string test_dir; + wchar_t wtmpbuf[MAX_PATH]; + GetTempPathW(MAX_PATH, wtmpbuf); + test_dir = WcharToUTF8(wtmpbuf) + "tinyobj_opt_mtllib_warn_error\\"; +#else + std::string test_dir = "/tmp/tinyobj_opt_mtllib_warn_error/"; +#endif + + RemoveTestDir(test_dir); + TEST_CHECK(MakeDir(test_dir)); + + const std::string obj_path = test_dir + "scene.obj"; + const std::string obj_text = + "mtllib missing.mtl\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 bad 3\n"; + + TEST_CHECK(opt_test::WriteTextFile(obj_path, obj_text)); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + obj_path.c_str(), test_dir.c_str(), true, + false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_path.c_str(), + test_dir.c_str(), config); + + RemoveTestDir(test_dir); + + TEST_CHECK(legacy_ok == false); + TEST_CHECK(opt_ok == false); + TEST_CHECK(legacy_warn.find("Material file [ missing.mtl ]") != + std::string::npos); + TEST_CHECK(legacy_warn.find( + "Failed to load material file(s). Use default material.") != + std::string::npos); + TEST_CHECK(opt_warn.find("Material file [ missing.mtl ]") != + std::string::npos); + TEST_CHECK(opt_warn.find( + "Failed to load material file(s). Use default material.") != + std::string::npos); + TEST_CHECK(opt_err.find("failed to parse `f' line (invalid vertex index)") != + std::string::npos); +} + +void test_loadobjopt_empty_group_warning_matches_legacy() { + const std::string obj_text = opt_test::BuildEmptyGroupObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, + opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_stream_loader_empty_group_warning_matches_legacy() { + const std::string obj_text = opt_test::BuildEmptyGroupObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); + TEST_CHECK(legacy_attrib.vertices == stream_attrib.vertices); + TEST_CHECK(legacy_shapes.size() == stream_shapes.size()); + TEST_CHECK(legacy_shapes[0].name == stream_shapes[0].name); + TEST_CHECK(legacy_shapes[0].mesh.num_face_vertices == + stream_shapes[0].mesh.num_face_vertices); +} + +void test_stream_loader_optional_vertex_token_matches_legacy() { + const std::string obj_text = opt_test::BuildOptionalVertexFallbackObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); + TEST_CHECK(legacy_attrib.vertices == stream_attrib.vertices); + TEST_CHECK(legacy_attrib.vertex_weights == stream_attrib.vertex_weights); + TEST_CHECK(legacy_attrib.colors == stream_attrib.colors); + TEST_CHECK(legacy_shapes.size() == stream_shapes.size()); + TEST_CHECK(legacy_shapes[0].mesh.indices.size() == + stream_shapes[0].mesh.indices.size()); + TEST_CHECK(legacy_shapes[0].mesh.num_face_vertices == + stream_shapes[0].mesh.num_face_vertices); +} + +void test_loadobjopt_buffer_ignores_mtllib_like_legacy_stream() { + const std::string obj_text = + "mtllib missing.mtl\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, + &legacy_err, &legacy_stream, NULL, true, + false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, + opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_loadobjopt_parse_error_reports_record_type() { + const std::string obj_text = opt_test::BuildInvalidVertexRecordObj(); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(opt_ok == false); + TEST_CHECK(opt_warn.empty()); + TEST_CHECK(opt_err.find("failed to parse `v' line") != std::string::npos); + TEST_CHECK(opt_err.find("Failed parse `f' line") == std::string::npos); +} + +void test_stream_loader_object_spacing_matches_legacy() { + const std::string obj_text = opt_test::BuildObjectSpacingObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, + &legacy_err, &legacy_stream, NULL, true, + false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); + TEST_CHECK(legacy_shapes.size() == stream_shapes.size()); + TEST_CHECK(legacy_shapes[0].name == stream_shapes[0].name); + TEST_CHECK(legacy_shapes[0].mesh.indices.size() == + stream_shapes[0].mesh.indices.size()); +} + +void test_loadobjopt_object_spacing_matches_legacy() { + const std::string obj_text = opt_test::BuildObjectSpacingObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, + &legacy_err, &legacy_stream, NULL, true, + false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, + opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_loadobjopt_empty_mtllib_warning_matches_legacy_file() { +#ifdef _WIN32 + std::string test_dir; + wchar_t wtmpbuf[MAX_PATH]; + GetTempPathW(MAX_PATH, wtmpbuf); + test_dir = WcharToUTF8(wtmpbuf) + "tinyobj_opt_empty_mtllib_warn\\"; +#else + std::string test_dir = "/tmp/tinyobj_opt_empty_mtllib_warn/"; +#endif + + RemoveTestDir(test_dir); + TEST_CHECK(MakeDir(test_dir)); + + const std::string obj_path = test_dir + "scene.obj"; + const std::string obj_text = opt_test::BuildEmptyMtllibObj(); + TEST_CHECK(opt_test::WriteTextFile(obj_path, obj_text)); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, + &legacy_err, obj_path.c_str(), + test_dir.c_str(), true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_path.c_str(), + test_dir.c_str(), config); + + RemoveTestDir(test_dir); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, + opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_stream_loader_empty_mtllib_warning_matches_legacy() { + const std::string obj_text = opt_test::BuildEmptyMtllibObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + tinyobj::MaterialFileReader legacy_reader((std::string())); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, + &legacy_err, &legacy_stream, + &legacy_reader, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::MaterialFileReader stream_reader((std::string())); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, &stream_reader, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); + TEST_CHECK(legacy_shapes.size() == stream_shapes.size()); + TEST_CHECK(legacy_shapes[0].mesh.indices.size() == + stream_shapes[0].mesh.indices.size()); +} + +void test_stream_loader_warnings_preserved_on_parse_error() { + const std::string obj_text = + "mtllib missing.mtl\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 bad 3\n"; + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + tinyobj::MaterialFileReader legacy_reader((std::string())); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, + &legacy_err, &legacy_stream, + &legacy_reader, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::MaterialFileReader stream_reader((std::string())); + tinyobj::experimental_stream::StreamLoadConfig config; + config.num_threads = 1; + config.chunk_line_count = 64; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, &stream_reader, config); + + TEST_CHECK(legacy_ok == false); + TEST_CHECK(stream_ok == false); + TEST_CHECK(legacy_warn.find("Material file [ missing.mtl ]") != + std::string::npos); + TEST_CHECK(legacy_warn.find( + "Failed to load material file(s). Use default material.") != + std::string::npos); + TEST_CHECK(stream_warn.find("Material file [ missing.mtl ]") != + std::string::npos); + TEST_CHECK(stream_warn.find( + "Failed to load material file(s). Use default material.") != + std::string::npos); + TEST_CHECK(stream_err.find("malformed face record") != std::string::npos); +} + +void test_stream_loader_material_resolution_without_material_output() { +#ifdef _WIN32 + std::string test_dir; + wchar_t wtmpbuf[MAX_PATH]; + GetTempPathW(MAX_PATH, wtmpbuf); + test_dir = WcharToUTF8(wtmpbuf) + "tinyobj_stream_null_materials\\"; +#else + std::string test_dir = "/tmp/tinyobj_stream_null_materials/"; +#endif + + RemoveTestDir(test_dir); + TEST_CHECK(MakeDir(test_dir)); + + const std::string obj_path = test_dir + "scene.obj"; + const std::string mtl_path = test_dir + "mats.mtl"; + const std::string obj_text = + "mtllib mats.mtl\n" + "usemtl mat1\n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; + const std::string mtl_text = + "newmtl mat1\n" + "Kd 1.0 0.0 0.0\n"; + + TEST_CHECK(opt_test::WriteTextFile(obj_path, obj_text)); + TEST_CHECK(opt_test::WriteTextFile(mtl_path, mtl_text)); + + tinyobj::attrib_t with_material_attrib; + std::vector with_material_shapes; + std::vector with_materials; + std::string with_warn, with_err; + tinyobj::experimental_stream::StreamLoadConfig config; + bool with_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &with_material_attrib, &with_material_shapes, &with_materials, + &with_warn, &with_err, obj_path.c_str(), test_dir.c_str(), config); + + tinyobj::attrib_t without_material_attrib; + std::vector without_material_shapes; + std::string without_warn, without_err; + bool without_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &without_material_attrib, &without_material_shapes, NULL, + &without_warn, &without_err, obj_path.c_str(), test_dir.c_str(), config); + + RemoveTestDir(test_dir); + + TEST_CHECK(with_ok == true); + TEST_CHECK(without_ok == true); + TEST_CHECK(with_warn == without_warn); + TEST_CHECK(with_err == without_err); + TEST_CHECK(with_materials.size() == 1); + TEST_CHECK(with_material_shapes.size() == without_material_shapes.size()); + TEST_CHECK(with_material_shapes[0].name == without_material_shapes[0].name); + TEST_CHECK(with_material_shapes[0].mesh.indices.size() == + without_material_shapes[0].mesh.indices.size()); + for (size_t i = 0; i < with_material_shapes[0].mesh.indices.size(); i++) { + TEST_CHECK(opt_test::IndexEquals(with_material_shapes[0].mesh.indices[i], + without_material_shapes[0].mesh.indices[i])); + } + TEST_CHECK(with_material_shapes[0].mesh.material_ids == + without_material_shapes[0].mesh.material_ids); + TEST_CHECK(without_material_shapes[0].mesh.material_ids.size() == 1); + TEST_CHECK(without_material_shapes[0].mesh.material_ids[0] == 0); +} + +void test_loadobjopt_matches_legacy_extended_features() { + const std::string obj_text = opt_test::BuildExtendedFeatureObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 4; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_loadobjopt_vertex_weight_and_texcoord_w_match_legacy() { + const std::string obj_text = opt_test::BuildWeightedVertexTexcoordObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_loadobjopt_single_component_texcoord_matches_legacy() { + const std::string obj_text = opt_test::BuildSingleComponentTexcoordObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, + opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_stream_loader_single_component_texcoord_matches_legacy() { + const std::string obj_text = opt_test::BuildSingleComponentTexcoordObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(stream_warn == legacy_warn); + TEST_CHECK(stream_err == legacy_err); + TEST_CHECK(stream_attrib.texcoords == legacy_attrib.texcoords); + TEST_CHECK(stream_attrib.texcoord_ws == legacy_attrib.texcoord_ws); +} + +void test_stream_loader_embedded_carriage_return_matches_legacy() { + const std::string obj_text = opt_test::BuildEmbeddedCarriageReturnObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_attrib.vertices == stream_attrib.vertices); + TEST_CHECK(legacy_attrib.texcoords == stream_attrib.texcoords); + TEST_CHECK(legacy_shapes.size() == stream_shapes.size()); + TEST_CHECK(legacy_shapes[0].name == stream_shapes[0].name); + TEST_CHECK(legacy_shapes[0].mesh.indices.size() == + stream_shapes[0].mesh.indices.size()); +} + +void test_stream_loader_embedded_nul_empty_shape_matches_legacy() { + const std::string obj_text = opt_test::BuildEmbeddedNulEmptyShapeObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_shapes.size() == stream_shapes.size()); + TEST_CHECK(legacy_shapes[0].name == stream_shapes[0].name); + TEST_CHECK(legacy_shapes[0].mesh.indices.size() == + stream_shapes[0].mesh.indices.size()); +} + +void test_stream_loader_object_only_matches_legacy() { + const std::string obj_text = opt_test::BuildObjectOnlyObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_shapes.size() == stream_shapes.size()); +} + +void test_stream_loader_group_only_matches_legacy() { + const std::string obj_text = opt_test::BuildGroupOnlyObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); + TEST_CHECK(legacy_shapes.size() == stream_shapes.size()); +} + +void test_stream_loader_degenerate_face_matches_legacy() { + const std::string obj_text = opt_test::BuildDegenerateFaceOnlyObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, false, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + config.triangulate = false; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); + TEST_CHECK(legacy_shapes.size() == stream_shapes.size()); + if (legacy_shapes.size() == stream_shapes.size() && !legacy_shapes.empty()) { + TEST_CHECK(legacy_shapes[0].name == stream_shapes[0].name); + TEST_CHECK(legacy_shapes[0].mesh.num_face_vertices == + stream_shapes[0].mesh.num_face_vertices); + TEST_CHECK(legacy_shapes[0].mesh.material_ids == + stream_shapes[0].mesh.material_ids); + TEST_CHECK(legacy_shapes[0].mesh.smoothing_group_ids == + stream_shapes[0].mesh.smoothing_group_ids); + TEST_CHECK(legacy_shapes[0].mesh.indices.size() == + stream_shapes[0].mesh.indices.size()); + for (size_t i = 0; i < legacy_shapes[0].mesh.indices.size(); i++) { + TEST_CHECK(opt_test::IndexEquals(legacy_shapes[0].mesh.indices[i], + stream_shapes[0].mesh.indices[i])); + } + } +} + +void test_loadobjopt_degenerate_face_matches_legacy() { + const std::string obj_text = opt_test::BuildDegenerateFaceOnlyObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, false, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = false; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, + opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_stream_loader_degenerate_face_warning_order_matches_legacy() { + const std::string obj_text = opt_test::BuildDegenerateFaceThenMissingUsemtlObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, false, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + config.triangulate = false; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); +} + +void test_stream_loader_degenerate_face_warning_suppressed_on_parse_error() { + const std::string obj_text = opt_test::BuildDegenerateFaceThenInvalidFaceObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, false, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + config.triangulate = false; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == false); + TEST_CHECK(stream_ok == false); + TEST_CHECK(legacy_warn.find("Degenerated face found") == std::string::npos); + TEST_CHECK(stream_warn.find("Degenerated face found") == std::string::npos); +} + +void test_loadobjopt_degenerate_face_warning_order_matches_legacy() { + const std::string obj_text = opt_test::BuildDegenerateFaceThenMissingUsemtlObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, false, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = false; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); +} + +void test_loadobjopt_degenerate_face_warning_suppressed_on_parse_error() { + const std::string obj_text = opt_test::BuildDegenerateFaceThenInvalidFaceObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, false, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = false; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == false); + TEST_CHECK(opt_ok == false); + TEST_CHECK(legacy_warn.find("Degenerated face found") == std::string::npos); + TEST_CHECK(opt_warn.find("Degenerated face found") == std::string::npos); +} + +void test_loadobjopt_initial_named_shape_matches_legacy() { + const std::string obj_text = opt_test::BuildInitialNamedShapeThenBoundaryObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, + opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_loadobjopt_invalid_relative_vertex_index_matches_legacy_error() { + const std::string obj_text = opt_test::BuildInvalidRelativeVertexIndexObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == false); + TEST_CHECK(opt_ok == false); + TEST_CHECK(opt_err.find("failed to parse `f' line (invalid vertex index)") != + std::string::npos); +} + +void test_loadobjopt_invalid_relative_texcoord_normal_index_matches_legacy_error() { + const std::string obj_text = + opt_test::BuildInvalidRelativeTexcoordNormalIndexObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == false); + TEST_CHECK(opt_ok == false); + TEST_CHECK(opt_err.find("failed to parse `f' line (invalid vertex index)") != + std::string::npos); +} + +void test_stream_loader_invalid_relative_vertex_index_matches_legacy_error() { + const std::string obj_text = opt_test::BuildInvalidRelativeVertexIndexObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == false); + TEST_CHECK(stream_ok == false); + TEST_CHECK(stream_err.find("malformed face record") != std::string::npos); +} + +void test_stream_loader_invalid_relative_texcoord_normal_index_matches_legacy_error() { + const std::string obj_text = + opt_test::BuildInvalidRelativeTexcoordNormalIndexObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == false); + TEST_CHECK(stream_ok == false); + TEST_CHECK(stream_err.find("malformed face record") != std::string::npos); +} + +void test_loadobjopt_out_of_bounds_warning_matches_legacy() { + const std::string obj_text = opt_test::BuildAcceptedOutOfBoundsFaceObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); +} + +void test_stream_loader_out_of_bounds_warning_matches_legacy() { + const std::string obj_text = opt_test::BuildAcceptedOutOfBoundsFaceObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, &legacy_err, + &legacy_stream, NULL, true, false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); +} + +void test_loadobjopt_zero_vertex_index_warning_matches_legacy() { + const std::string obj_text = + opt_test::BuildZeroVertexIndexThenMissingMaterialObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, + &legacy_err, &legacy_stream, NULL, true, + false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == false); + TEST_CHECK(opt_ok == false); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(opt_warn.find("material [ 'later_missing' ] not found in .mtl") == + std::string::npos); + TEST_CHECK(opt_err.find("failed to parse `f' line (invalid vertex index)") != + std::string::npos); +} + +void test_stream_loader_zero_vertex_index_warning_matches_legacy() { + const std::string obj_text = + opt_test::BuildZeroVertexIndexThenMissingMaterialObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, + &legacy_err, &legacy_stream, NULL, true, + false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == false); + TEST_CHECK(stream_ok == false); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(stream_warn.find("material [ 'later_missing' ] not found in .mtl") == + std::string::npos); + TEST_CHECK(stream_err.find("malformed face record") != std::string::npos); +} + +void test_loadobjopt_zero_texcoord_normal_indices_match_legacy() { + const std::string obj_text = opt_test::BuildZeroTexcoordNormalIndexObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, + &legacy_err, &legacy_stream, NULL, true, + false); + + tinyobj::basic_attrib_t<> opt_attrib; + std::vector> opt_shapes; + std::vector opt_materials; + std::string opt_warn, opt_err; + tinyobj::OptLoadConfig config; + config.num_threads = 2; + config.triangulate = true; + bool opt_ok = tinyobj::LoadObjOpt(&opt_attrib, &opt_shapes, &opt_materials, + &opt_warn, &opt_err, obj_text.c_str(), + obj_text.size(), config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(opt_ok == true); + TEST_CHECK(legacy_warn == opt_warn); + TEST_CHECK(legacy_err == opt_err); + TEST_CHECK(opt_test::LegacyAttribEqualsOpt(legacy_attrib, legacy_shapes, + opt_attrib)); + TEST_CHECK(opt_test::LegacyShapesEqualOpt(legacy_shapes, opt_shapes)); +} + +void test_stream_loader_zero_texcoord_normal_indices_match_legacy() { + const std::string obj_text = opt_test::BuildZeroTexcoordNormalIndexObj(); + + tinyobj::attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + std::string legacy_warn, legacy_err; + std::istringstream legacy_stream(obj_text); + bool legacy_ok = tinyobj::LoadObj(&legacy_attrib, &legacy_shapes, + &legacy_materials, &legacy_warn, + &legacy_err, &legacy_stream, NULL, true, + false); + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(legacy_ok == true); + TEST_CHECK(stream_ok == true); + TEST_CHECK(legacy_warn == stream_warn); + TEST_CHECK(legacy_err == stream_err); + TEST_CHECK(legacy_attrib.vertices == stream_attrib.vertices); + TEST_CHECK(legacy_attrib.normals == stream_attrib.normals); + TEST_CHECK(legacy_attrib.texcoords == stream_attrib.texcoords); + TEST_CHECK(legacy_shapes.size() == stream_shapes.size()); + TEST_CHECK(legacy_shapes[0].mesh.indices.size() == + stream_shapes[0].mesh.indices.size()); + for (size_t i = 0; i < legacy_shapes[0].mesh.indices.size(); i++) { + TEST_CHECK(opt_test::IndexEquals(legacy_shapes[0].mesh.indices[i], + stream_shapes[0].mesh.indices[i])); + } +} diff --git a/tests/tester.cc b/tests/tester.cc index 9709d1c5..f3ba3a4a 100644 --- a/tests/tester.cc +++ b/tests/tester.cc @@ -1,5 +1,12 @@ +#ifndef TINYOBJLOADER_USE_MULTITHREADING +#define TINYOBJLOADER_USE_MULTITHREADING +#endif +#ifndef TINYOBJLOADER_IMPLEMENTATION #define TINYOBJLOADER_IMPLEMENTATION +#endif +#ifndef TINYOBJLOADER_STREAM_READER_MAX_BYTES #define TINYOBJLOADER_STREAM_READER_MAX_BYTES (size_t(8) * size_t(1024) * size_t(1024)) +#endif #include "../tiny_obj_loader.h" #if defined(__clang__) @@ -32,6 +39,35 @@ #include #include +namespace tinyobj { +namespace experimental_stream { +struct StreamLoadConfig { + bool triangulate; + bool default_vcols_fallback; + int num_threads; + size_t chunk_line_count; + + StreamLoadConfig() + : triangulate(true), + default_vcols_fallback(false), + num_threads(1), + chunk_line_count(4096) {} +}; + +bool LoadObjStreamExperimental( + attrib_t *attrib, std::vector *shapes, + std::vector *materials, std::string *warn, std::string *err, + std::istream *input, MaterialReader *readMatFn, + const StreamLoadConfig &config = StreamLoadConfig()); + +bool LoadObjStreamExperimental( + attrib_t *attrib, std::vector *shapes, + std::vector *materials, std::string *warn, std::string *err, + const char *filename, const char *mtl_basedir, + const StreamLoadConfig &config = StreamLoadConfig()); +} // namespace experimental_stream +} // namespace tinyobj + #ifdef _WIN32 #include // _mkdir #include // GetTempPathW, CreateDirectoryW, RegOpenKeyExA @@ -2400,6 +2436,8 @@ void test_numeric_nan_inf() { if (!warn.empty()) std::cout << "WARN: " << warn << std::endl; if (!err.empty()) std::cerr << "ERR: " << err << std::endl; +#ifndef TINYOBJLOADER_DISABLE_FAST_FLOAT + // With fast_float, nan/inf keywords are recognized via tryParseNanInf. TEST_CHECK(true == ret); // 12 vertices * 3 components = 36 TEST_CHECK(attrib.vertices.size() == 36); @@ -2419,6 +2457,11 @@ void test_numeric_nan_inf() { // v4: -inf 1 1 TEST_CHECK(FloatEquals(1.0f, attrib.vertices[13])); TEST_CHECK(FloatEquals(1.0f, attrib.vertices[14])); +#else + // The legacy hand-written parser does not recognize nan/inf keywords, + // so the parse fails. Just verify it doesn't crash. + (void)ret; +#endif } void test_numeric_from_stream() { @@ -2459,7 +2502,8 @@ void test_numeric_from_stream() { void test_numeric_overflow_preserves_default() { // Regression: values that overflow double must not crash or corrupt memory. // tryParseDouble now parses into a temp; *result is only written on success. - // With the StreamReader-based parser, overflow is detected as a parse error. + // With fast_float, overflow is detected as a parse error (result_out_of_range). + // The hand-written fallback parser produces inf for large exponents instead. std::string obj_str = "v 1e9999 2.0 3.0\n" // first coord overflows "f 1\n"; @@ -2472,9 +2516,17 @@ void test_numeric_overflow_preserves_default() { bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, &obj_stream, NULL); - // Must not crash. Parser detects overflow and returns false. + // Must not crash. +#ifndef TINYOBJLOADER_DISABLE_FAST_FLOAT + // fast_float detects overflow and returns an error. TEST_CHECK(false == ret); TEST_CHECK(!err.empty()); +#else + // Fallback parser produces inf instead of erroring out. + TEST_CHECK(true == ret); + TEST_CHECK(attrib.vertices.size() == 3); + TEST_CHECK(std::isinf(attrib.vertices[0])); +#endif } void test_numeric_empty_and_whitespace() { @@ -3349,6 +3401,827 @@ main( } #endif +// ---- Tests for Optimized API (LoadObjOpt) ---- +void test_loadobjopt_from_buffer() { + // Simple triangle + const char *obj_text = + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "vn 0.0 0.0 1.0\n" + "vt 0.0 0.0\n" + "vt 1.0 0.0\n" + "vt 0.0 1.0\n" + "f 1/1/1 2/2/1 3/3/1\n"; + size_t obj_len = strlen(obj_text); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.num_threads = 1; // force single-threaded for determinism + config.triangulate = true; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text, obj_len, config); + if (!err.empty()) std::cerr << "ERR: " << err << "\n"; + TEST_CHECK(ret == true); + TEST_CHECK(attrib.vertices.size() == 9); // 3 vertices * 3 coords + TEST_CHECK(attrib.normals.size() == 3); // 1 normal * 3 coords + TEST_CHECK(attrib.texcoords.size() == 6); // 3 texcoords * 2 coords + TEST_CHECK(attrib.indices.size() == 3); // 3 face indices + TEST_CHECK(attrib.face_num_verts.size() == 1); // 1 face + TEST_CHECK(attrib.face_num_verts[0] == 3); // triangle + + // Check vertex values + TEST_CHECK(attrib.vertices[0] == 0.0f); + TEST_CHECK(attrib.vertices[1] == 0.0f); + TEST_CHECK(attrib.vertices[2] == 0.0f); + TEST_CHECK(attrib.vertices[3] == 1.0f); +} + +void test_loadobjopt_from_file() { + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.num_threads = 1; + config.triangulate = true; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + "../models/cornell_box.obj", gMtlBasePath, + config); + if (!err.empty()) std::cerr << "ERR: " << err << "\n"; + TEST_CHECK(ret == true); + TEST_CHECK(attrib.vertices.size() > 0); + TEST_CHECK(shapes.size() > 0); + + // Compare vertex count with standard LoadObj + tinyobj::attrib_t std_attrib; + std::vector std_shapes; + std::vector std_materials; + std::string warn2, err2; + bool ret2 = tinyobj::LoadObj(&std_attrib, &std_shapes, &std_materials, + &warn2, &err2, "../models/cornell_box.obj", + gMtlBasePath); + TEST_CHECK(ret2 == true); + TEST_CHECK(attrib.vertices.size() == std_attrib.vertices.size()); + TEST_CHECK(attrib.normals.size() == std_attrib.normals.size()); +} + +void test_loadobjopt_quad_triangulation() { + // Quad face that should be triangulated + const char *obj_text = + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 1.0 1.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3 4\n"; + size_t obj_len = strlen(obj_text); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.num_threads = 1; + config.triangulate = true; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text, obj_len, config); + TEST_CHECK(ret == true); + TEST_CHECK(attrib.vertices.size() == 12); // 4 vertices * 3 + // Quad triangulated into 2 triangles = 6 indices + TEST_CHECK(attrib.indices.size() == 6); + TEST_CHECK(attrib.face_num_verts.size() == 2); + TEST_CHECK(attrib.face_num_verts[0] == 3); + TEST_CHECK(attrib.face_num_verts[1] == 3); +} + +void test_loadobjopt_no_triangulation() { + const char *obj_text = + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 1.0 1.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3 4\n"; + size_t obj_len = strlen(obj_text); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.num_threads = 1; + config.triangulate = false; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text, obj_len, config); + TEST_CHECK(ret == true); + TEST_CHECK(attrib.indices.size() == 4); // quad = 4 indices + TEST_CHECK(attrib.face_num_verts.size() == 1); + TEST_CHECK(attrib.face_num_verts[0] == 4); +} + +void test_loadobjopt_multiple_groups() { + const char *obj_text = + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "v 1.0 1.0 0.0\n" + "v 0.0 0.0 1.0\n" + "v 1.0 0.0 1.0\n" + "g group1\n" + "f 1 2 3\n" + "g group2\n" + "f 4 5 6\n"; + size_t obj_len = strlen(obj_text); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.num_threads = 1; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text, obj_len, config); + TEST_CHECK(ret == true); + TEST_CHECK(shapes.size() == 2); +} + +void test_loadobjopt_empty_buffer() { + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + "", static_cast(0)); + TEST_CHECK(ret == true); // empty is not an error + TEST_CHECK(attrib.vertices.size() == 0); +} + +void test_loadobjopt_leading_decimal_dot() { + // OBJ files may use leading decimal dots (e.g. ".7", "-.5234") + const char *obj_text = + "v .5 -.25 .0\n" + "v 1.0 .7 -.5234\n" + "v 0.0 0.0 0.0\n" + "f 1 2 3\n"; + size_t obj_len = strlen(obj_text); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.triangulate = true; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text, obj_len, config); + TEST_CHECK(ret == true); + TEST_CHECK(attrib.vertices.size() == 9); // 3 vertices * 3 coords + + // v .5 -.25 .0 + TEST_CHECK(std::abs(attrib.vertices[0] - 0.5f) < 1e-6f); + TEST_CHECK(std::abs(attrib.vertices[1] - (-0.25f)) < 1e-6f); + TEST_CHECK(std::abs(attrib.vertices[2] - 0.0f) < 1e-6f); + + // v 1.0 .7 -.5234 + TEST_CHECK(std::abs(attrib.vertices[3] - 1.0f) < 1e-6f); + TEST_CHECK(std::abs(attrib.vertices[4] - 0.7f) < 1e-6f); + TEST_CHECK(std::abs(attrib.vertices[5] - (-0.5234f)) < 1e-6f); +} + +void test_loadobjopt_no_trailing_newline() { + // Buffer without trailing newline (tests sentinel handling) + const char *obj_text = + "v 1.0 2.0 3.0\n" + "v 4.0 5.0 6.0\n" + "v 7.0 8.0 9.0\n" + "f 1 2 3"; // no trailing newline + size_t obj_len = strlen(obj_text); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.triangulate = true; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text, obj_len, config); + TEST_CHECK(ret == true); + TEST_CHECK(attrib.vertices.size() == 9); // 3 vertices * 3 coords + TEST_CHECK(attrib.indices.size() == 3); // 3 face indices +} + +void test_loadobjopt_face_missing_vt_vn() { + // Test that missing vt/vn in face lines are correctly handled as -1 + // (not misinterpreted as relative index -1). + const char *obj_text = + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "vn 0.0 0.0 1.0\n" + "f 1//1 2//1 3//1\n"; // vertex//normal (no texcoord) + size_t obj_len = strlen(obj_text); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.triangulate = true; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text, obj_len, config); + TEST_CHECK(ret == true); + TEST_CHECK(attrib.indices.size() == 3); + + // texcoord_index should be -1 (not present), not a fixed-up relative index + for (size_t i = 0; i < attrib.indices.size(); i++) { + TEST_CHECK(attrib.indices[i].texcoord_index == -1); + TEST_CHECK(attrib.indices[i].normal_index == 0); // mapped from 1-based + } + + // Also test "f 1 2 3" (vertex-only, no texcoord, no normal) + const char *obj_text2 = + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; + size_t obj_len2 = strlen(obj_text2); + + tinyobj::basic_attrib_t<> attrib2; + std::vector> shapes2; + std::vector materials2; + std::string warn2, err2; + + ret = tinyobj::LoadObjOpt(&attrib2, &shapes2, &materials2, &warn2, &err2, + obj_text2, obj_len2, config); + TEST_CHECK(ret == true); + TEST_CHECK(attrib2.indices.size() == 3); + for (size_t i = 0; i < attrib2.indices.size(); i++) { + TEST_CHECK(attrib2.indices[i].texcoord_index == -1); + TEST_CHECK(attrib2.indices[i].normal_index == -1); + } +} + +void test_loadobjopt_bare_cr_line_endings() { + // Test OBJ buffer with bare \r line endings (old Mac style) + const char *obj_text = + "v 0.0 0.0 0.0\r" + "v 1.0 0.0 0.0\r" + "v 0.0 1.0 0.0\r" + "f 1 2 3\r"; + size_t obj_len = strlen(obj_text); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.triangulate = true; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text, obj_len, config); + TEST_CHECK(ret == true); + TEST_CHECK(attrib.vertices.size() == 9); // 3 vertices * 3 coords + TEST_CHECK(attrib.indices.size() == 3); // 3 face indices +} + +void test_loadobjopt_degenerate_face() { + // Test that faces with fewer than 3 vertices are skipped + const char *obj_text = + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2\n" // degenerate (2 vertices) — should be skipped + "f 1 2 3\n"; // valid triangle + size_t obj_len = strlen(obj_text); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.triangulate = true; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text, obj_len, config); + TEST_CHECK(ret == true); + TEST_CHECK(attrib.indices.size() == 3); // only the valid triangle + TEST_CHECK(attrib.face_num_verts.size() == 1); // 1 face +} + +void test_loadobjopt_usemtl_multiple_faces() { + // Test that usemtl applies to ALL subsequent faces, not just the next one. + // Without material files loaded, material_ids should default to -1. + const char *obj_text = + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "v 1.0 1.0 0.0\n" + "usemtl test_material\n" + "f 1 2 3\n" // face 0 — should have the material + "f 2 3 4\n" // face 1 — should also have the material (not reset) + "f 1 3 4\n"; // face 2 — should also have the material + size_t obj_len = strlen(obj_text); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.triangulate = true; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text, obj_len, config); + TEST_CHECK(ret == true); + TEST_CHECK(attrib.face_num_verts.size() == 3); // 3 faces + TEST_CHECK(attrib.material_ids.size() == 3); + + // All three faces should have the same material ID (-1 since no .mtl loaded) + // The key test: material_ids[1] and [2] should NOT be different from [0] + TEST_CHECK(attrib.material_ids[0] == attrib.material_ids[1]); + TEST_CHECK(attrib.material_ids[1] == attrib.material_ids[2]); +} + +void test_loadobjopt_crlf_line_endings() { + // Test OBJ buffer with Windows-style \r\n line endings + const char *obj_text = + "v 0.0 0.0 0.0\r\n" + "v 1.0 0.0 0.0\r\n" + "v 0.0 1.0 0.0\r\n" + "f 1 2 3\r\n"; + size_t obj_len = strlen(obj_text); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.triangulate = true; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text, obj_len, config); + TEST_CHECK(ret == true); + TEST_CHECK(attrib.vertices.size() == 9); // 3 vertices * 3 coords + TEST_CHECK(attrib.indices.size() == 3); // 3 face indices +} + +void test_arena_allocator() { + tinyobj::ArenaAllocator arena(4096); + + // Basic allocation + void *p1 = arena.allocate(100); + TEST_CHECK(p1 != nullptr); + + void *p2 = arena.allocate(200); + TEST_CHECK(p2 != nullptr); + TEST_CHECK(p1 != p2); + + // Aligned allocation + void *p3 = arena.allocate(64, 64); + TEST_CHECK(p3 != nullptr); + TEST_CHECK(reinterpret_cast(p3) % 64 == 0); + + // Large allocation (exceeds default block) + void *p4 = arena.allocate(8192); + TEST_CHECK(p4 != nullptr); + + // Reset and reuse + arena.reset(); + void *p5 = arena.allocate(100); + TEST_CHECK(p5 != nullptr); +} + +void test_arena_adapter_with_vector() { + tinyobj::ArenaAllocator arena(1024 * 1024); + tinyobj::arena_adapter alloc(&arena); + + // Use arena allocator with std::vector + std::vector> vec(alloc); + vec.reserve(100); + for (int i = 0; i < 100; i++) { + vec.push_back(static_cast(i)); + } + + TEST_CHECK(vec.size() == 100); + TEST_CHECK(vec[0] == 0.0f); + TEST_CHECK(vec[99] == 99.0f); +} + +void test_basic_attrib_with_arena() { + // Test basic_attrib_t with custom allocator + tinyobj::ArenaAllocator arena(1024 * 1024); + typedef tinyobj::arena_adapter ArenaAlloc; + + TEST_CHECK((std::uses_allocator, ArenaAlloc>::value)); + TEST_CHECK((std::uses_allocator, + ArenaAlloc>::value)); + + // Verify the template compiles and works + tinyobj::basic_attrib_t attrib; + attrib.vertices.push_back(1.0f); + attrib.vertices.push_back(2.0f); + attrib.vertices.push_back(3.0f); + tinyobj::basic_skin_weight_t sw; + sw.vertex_id = 0; + tinyobj::joint_and_weight_t jw; + jw.joint_id = 1; + jw.weight = 0.75f; + sw.weightValues.push_back(jw); + attrib.skin_weights.push_back(sw); + + tinyobj::basic_mesh_t mesh; + tinyobj::basic_tag_t tag; + tag.name = "crease"; + tag.intValues.push_back(1); + tag.floatValues.push_back(0.5f); + tag.stringValues.push_back("hard"); + mesh.tags.push_back(tag); + + TEST_CHECK(attrib.vertices.size() == 3); + TEST_CHECK(attrib.vertices[0] == 1.0f); + TEST_CHECK(attrib.skin_weights.size() == 1); + TEST_CHECK(mesh.tags.size() == 1); +} + +// ---- Tests for TypedArray-based LoadObjOptTyped API ---- + +void test_loadobjopt_typed_from_buffer() { + const char *obj_text = + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "vn 0.0 0.0 1.0\n" + "vt 0.0 0.0\n" + "vt 1.0 0.0\n" + "vt 0.0 1.0\n" + "f 1/1/1 2/2/1 3/3/1\n"; + size_t obj_len = strlen(obj_text); + + std::string warn, err; + tinyobj::OptLoadConfig config; + config.num_threads = 1; + config.triangulate = true; + + tinyobj::OptResult result = + tinyobj::LoadObjOptTyped(obj_text, obj_len, &warn, &err, config); + if (!err.empty()) std::cerr << "ERR: " << err << "\n"; + TEST_CHECK(result.valid == true); + TEST_CHECK(result.attrib.vertices.size() == 9); + TEST_CHECK(result.attrib.normals.size() == 3); + TEST_CHECK(result.attrib.texcoords.size() == 6); + TEST_CHECK(result.attrib.indices.size() == 3); + TEST_CHECK(result.attrib.face_num_verts.size() == 1); + TEST_CHECK(result.attrib.face_num_verts[0] == 3); + + // Check vertex values + TEST_CHECK(result.attrib.vertices[0] == 0.0f); + TEST_CHECK(result.attrib.vertices[3] == 1.0f); + + // Check shape ranges (should be 1 shape) + TEST_CHECK(result.shapes.size() == 1); + TEST_CHECK(result.shapes[0].face_offset == 0); + TEST_CHECK(result.shapes[0].face_count == 1); + TEST_CHECK(result.shapes[0].index_offset == 0); + TEST_CHECK(result.shapes[0].index_count == 3); +} + +void test_loadobjopt_typed_multiple_groups() { + const char *obj_text = + "v 0 0 0\nv 1 0 0\nv 0 1 0\nv 1 1 0\nv 2 0 0\nv 2 1 0\n" + "g group1\n" + "f 1 2 3\n" + "g group2\n" + "f 4 5 6\n"; + size_t obj_len = strlen(obj_text); + + std::string warn, err; + tinyobj::OptLoadConfig config; + config.num_threads = 1; + config.triangulate = true; + + tinyobj::OptResult result = + tinyobj::LoadObjOptTyped(obj_text, obj_len, &warn, &err, config); + TEST_CHECK(result.valid == true); + TEST_CHECK(result.shapes.size() == 2); + TEST_CHECK(result.shapes[0].name == "group1"); + TEST_CHECK(result.shapes[0].face_count == 1); + TEST_CHECK(result.shapes[0].index_count == 3); + TEST_CHECK(result.shapes[1].name == "group2"); + TEST_CHECK(result.shapes[1].face_count == 1); + TEST_CHECK(result.shapes[1].index_count == 3); + + // Shape ranges point into the flat attrib arrays + TEST_CHECK(result.shapes[0].face_offset == 0); + TEST_CHECK(result.shapes[1].face_offset == 1); + TEST_CHECK(result.shapes[0].index_offset == 0); + TEST_CHECK(result.shapes[1].index_offset == 3); +} + +void test_loadobjopt_typed_optional_arrays_lazy() { + // No vertex colors, no weights, no texcoord_w — these should be empty + const char *obj_text = + "v 0 0 0\nv 1 0 0\nv 0 1 0\n" + "f 1 2 3\n"; + size_t obj_len = strlen(obj_text); + + std::string warn, err; + tinyobj::OptLoadConfig config; + config.num_threads = 1; + config.triangulate = true; + + tinyobj::OptResult result = + tinyobj::LoadObjOptTyped(obj_text, obj_len, &warn, &err, config); + TEST_CHECK(result.valid == true); + TEST_CHECK(result.attrib.vertices.size() == 9); + // Optional arrays should be empty (lazy allocation) + TEST_CHECK(result.attrib.vertex_weights.empty()); + TEST_CHECK(result.attrib.texcoord_ws.empty()); + TEST_CHECK(result.attrib.colors.empty()); + TEST_CHECK(result.attrib.smoothing_group_ids.empty()); +} + +void test_loadobjopt_typed_matches_loadobjopt() { + // Compare results between LoadObjOpt and LoadObjOptTyped + const char *obj_text = + "v 0 0 0\nv 1 0 0\nv 0 1 0\nv 1 1 0\n" + "vn 0 0 1\n" + "vt 0 0\nvt 1 0\nvt 0 1\nvt 1 1\n" + "g quad\n" + "f 1/1/1 2/2/1 4/4/1 3/3/1\n"; + size_t obj_len = strlen(obj_text); + + tinyobj::OptLoadConfig config; + config.num_threads = 1; + config.triangulate = true; + + // LoadObjOpt path + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn1, err1; + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn1, &err1, + obj_text, obj_len, config); + TEST_CHECK(ret == true); + + // LoadObjOptTyped path + std::string warn2, err2; + tinyobj::OptResult result = + tinyobj::LoadObjOptTyped(obj_text, obj_len, &warn2, &err2, config); + TEST_CHECK(result.valid == true); + + // Compare attrib sizes + TEST_CHECK(result.attrib.vertices.size() == attrib.vertices.size()); + TEST_CHECK(result.attrib.normals.size() == attrib.normals.size()); + TEST_CHECK(result.attrib.texcoords.size() == attrib.texcoords.size()); + TEST_CHECK(result.attrib.indices.size() == attrib.indices.size()); + TEST_CHECK(result.attrib.face_num_verts.size() == attrib.face_num_verts.size()); + + // Compare vertex data + for (size_t i = 0; i < attrib.vertices.size(); i++) { + TEST_CHECK(result.attrib.vertices[i] == attrib.vertices[i]); + } + // Compare indices + for (size_t i = 0; i < attrib.indices.size(); i++) { + TEST_CHECK(result.attrib.indices[i].vertex_index == attrib.indices[i].vertex_index); + TEST_CHECK(result.attrib.indices[i].texcoord_index == attrib.indices[i].texcoord_index); + TEST_CHECK(result.attrib.indices[i].normal_index == attrib.indices[i].normal_index); + } +} + +void test_loadobjopt_typed_empty_buffer() { + std::string warn, err; + tinyobj::OptLoadConfig config; + config.num_threads = 1; + + tinyobj::OptResult result = + tinyobj::LoadObjOptTyped("", 0, &warn, &err, config); + TEST_CHECK(result.valid == true); + TEST_CHECK(result.attrib.vertices.empty()); + TEST_CHECK(result.attrib.indices.empty()); +} + +void test_loadobjopt_typed_move_semantics() { + const char *obj_text = + "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n"; + size_t obj_len = strlen(obj_text); + + std::string warn, err; + tinyobj::OptLoadConfig config; + config.num_threads = 1; + config.triangulate = true; + + tinyobj::OptResult result = + tinyobj::LoadObjOptTyped(obj_text, obj_len, &warn, &err, config); + TEST_CHECK(result.valid == true); + const float *verts_ptr = result.attrib.vertices.data(); + + // Move to new result + tinyobj::OptResult result2 = std::move(result); + TEST_CHECK(result2.valid == true); + TEST_CHECK(result2.attrib.vertices.data() == verts_ptr); // same pointer + TEST_CHECK(result2.attrib.vertices.size() == 9); +} + +void test_loadobjopt_typed_vertex_color_6_and_7() { + // 6-component: v x y z r g b — color=(r,g,b), weight=r (legacy compat) + // 7-component: v x y z w r g b — weight=w, color=(r,g,b) + { + const char *obj6 = + "v 1.0 2.0 3.0 0.2 0.4 0.6\n" + "v 4.0 5.0 6.0 0.2 0.4 0.6\n" + "v 7.0 8.0 9.0 0.2 0.4 0.6\n" + "f 1 2 3\n"; + std::string w, e; + tinyobj::OptLoadConfig cfg; + cfg.num_threads = 1; + cfg.triangulate = true; + tinyobj::OptResult r = tinyobj::LoadObjOptTyped(obj6, strlen(obj6), &w, &e, cfg); + TEST_CHECK(r.valid); + TEST_CHECK(r.attrib.vertices.size() == 9); + // Color should be (0.2, 0.4, 0.6) + TEST_CHECK(!r.attrib.colors.empty()); + TEST_CHECK(std::abs(r.attrib.colors[0] - 0.2f) < 1e-5f); + TEST_CHECK(std::abs(r.attrib.colors[1] - 0.4f) < 1e-5f); + TEST_CHECK(std::abs(r.attrib.colors[2] - 0.6f) < 1e-5f); + // Weight should be r (= 0.2) + TEST_CHECK(!r.attrib.vertex_weights.empty()); + TEST_CHECK(std::abs(r.attrib.vertex_weights[0] - 0.2f) < 1e-5f); + } + // 7-component: v x y z w r g b + { + const char *obj7 = + "v 1.0 2.0 3.0 0.5 0.1 0.2 0.3\n" + "v 4.0 5.0 6.0 0.5 0.1 0.2 0.3\n" + "v 7.0 8.0 9.0 0.5 0.1 0.2 0.3\n" + "f 1 2 3\n"; + std::string w, e; + tinyobj::OptLoadConfig cfg; + cfg.num_threads = 1; + cfg.triangulate = true; + tinyobj::OptResult r = tinyobj::LoadObjOptTyped(obj7, strlen(obj7), &w, &e, cfg); + TEST_CHECK(r.valid); + // Weight should be 0.5 + TEST_CHECK(!r.attrib.vertex_weights.empty()); + TEST_CHECK(std::abs(r.attrib.vertex_weights[0] - 0.5f) < 1e-5f); + // Color should be (0.1, 0.2, 0.3) — NOT (0.5, 0.1, 0.2) + TEST_CHECK(!r.attrib.colors.empty()); + TEST_CHECK(std::abs(r.attrib.colors[0] - 0.1f) < 1e-5f); + TEST_CHECK(std::abs(r.attrib.colors[1] - 0.2f) < 1e-5f); + TEST_CHECK(std::abs(r.attrib.colors[2] - 0.3f) < 1e-5f); + } + // Also verify LoadObjOpt produces same results + { + const char *obj7 = + "v 1.0 2.0 3.0 0.5 0.1 0.2 0.3\n" + "v 4.0 5.0 6.0 0.5 0.1 0.2 0.3\n" + "v 7.0 8.0 9.0 0.5 0.1 0.2 0.3\n" + "f 1 2 3\n"; + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector mats; + std::string w, e; + tinyobj::OptLoadConfig cfg; + cfg.num_threads = 1; + cfg.triangulate = true; + bool ok = tinyobj::LoadObjOpt(&attrib, &shapes, &mats, &w, &e, + obj7, strlen(obj7), cfg); + TEST_CHECK(ok); + TEST_CHECK(!attrib.colors.empty()); + TEST_CHECK(std::abs(attrib.colors[0] - 0.1f) < 1e-5f); + TEST_CHECK(std::abs(attrib.colors[1] - 0.2f) < 1e-5f); + TEST_CHECK(std::abs(attrib.colors[2] - 0.3f) < 1e-5f); + TEST_CHECK(std::abs(attrib.vertex_weights[0] - 0.5f) < 1e-5f); + } +} + +void test_loadobjopt_nan_inf_values() { + // Test that nan/inf in vertex data are handled with OBJ-compatible values + // (nan→0, +inf→max, -inf→lowest), matching the legacy parser. + const char *obj_text = + "v nan inf -inf\n" + "v 1.0 2.0 3.0\n" + "v 0.0 0.0 0.0\n" + "f 1 2 3\n"; + size_t obj_len = strlen(obj_text); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.triangulate = true; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text, obj_len, config); + TEST_CHECK(ret == true); + TEST_CHECK(attrib.vertices.size() == 9); // 3 vertices * 3 coords + + // nan maps to 0.0 + TEST_CHECK(std::abs(attrib.vertices[0] - 0.0f) < 1e-6f); + // +inf maps to double::max() → when cast to float becomes +inf + TEST_CHECK(std::isinf(attrib.vertices[1]) && attrib.vertices[1] > 0); + // -inf maps to double::lowest() → when cast to float becomes -inf + TEST_CHECK(std::isinf(attrib.vertices[2]) && attrib.vertices[2] < 0); + + // Verify second vertex is parsed normally + TEST_CHECK(std::abs(attrib.vertices[3] - 1.0f) < 1e-6f); + TEST_CHECK(std::abs(attrib.vertices[4] - 2.0f) < 1e-6f); + TEST_CHECK(std::abs(attrib.vertices[5] - 3.0f) < 1e-6f); +} + +void test_arena_adapter_overflow_guard() { + // Verify that arena_adapter::allocate rejects SIZE_MAX/sizeof(T) overflow. + // When TINYOBJLOADER_ENABLE_EXCEPTION is not defined, the allocator returns + // nullptr on overflow. When exceptions are enabled, it throws std::bad_alloc. + tinyobj::ArenaAllocator arena; + tinyobj::arena_adapter adapter(&arena); + // Request an allocation that would overflow size_t when multiplied by + // sizeof(double)=8. SIZE_MAX / 8 + 1 overflows. + double *p = adapter.allocate(SIZE_MAX / sizeof(double) + 1); + TEST_CHECK(p == nullptr); +} + +void test_loadobjopt_object_name_trimming() { + // Verify that object names match the legacy parser behavior. + // The legacy parser preserves leading/trailing spaces in object names. + const char *obj_text = + "o MyObject \n" + "v 0.0 0.0 0.0\n" + "v 1.0 0.0 0.0\n" + "v 0.0 1.0 0.0\n" + "f 1 2 3\n"; + size_t obj_len = strlen(obj_text); + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.triangulate = true; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_text, obj_len, config); + TEST_CHECK(ret == true); + TEST_CHECK(shapes.size() == 1); + // Legacy parser uses sr.advance(2) to skip 'o' + one space, then + // sr.read_line() captures the remainder verbatim. For "o MyObject \n", + // advance(2) skips 'o' and the first space, leaving " MyObject " as the + // object name (leading space is the second space from the original input, + // and trailing spaces are preserved). + TEST_CHECK(shapes[0].name == " MyObject "); +} + +void test_loadobjopt_mixed_line_endings() { + // File with mixed \n, \r\n, and bare \r line endings + std::string obj_data; + obj_data += "v 0.0 0.0 0.0\n"; // LF + obj_data += "v 1.0 0.0 0.0\r\n"; // CRLF + obj_data += "v 0.0 1.0 0.0\r"; // bare CR + obj_data += "v 1.0 1.0 0.0\n"; // LF + obj_data += "f 1 2 3\r\n"; // CRLF + obj_data += "f 1 3 4\n"; // LF + + tinyobj::basic_attrib_t<> attrib; + std::vector> shapes; + std::vector materials; + std::string warn, err; + + tinyobj::OptLoadConfig config; + config.triangulate = true; + + bool ret = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + obj_data.data(), obj_data.size(), config); + TEST_CHECK(ret == true); + TEST_CHECK(attrib.vertices.size() == 12); // 4 vertices * 3 coords + TEST_CHECK(shapes.size() == 1); + TEST_CHECK(shapes[0].mesh.indices.size() == 6); // 2 triangles * 3 +} + +#include "opt/loadobjopt_multithread.inc" + TEST_LIST = { {"cornell_box", test_cornell_box}, {"catmark_torus_creases0", test_catmark_torus_creases0}, @@ -3444,6 +4317,151 @@ TEST_LIST = { {"test_parse_error_backward_compat", test_parse_error_backward_compat}, {"test_split_string_preserves_non_escape_backslash", test_split_string_preserves_non_escape_backslash}, + {"test_loadobjopt_from_buffer", test_loadobjopt_from_buffer}, + {"test_loadobjopt_from_file", test_loadobjopt_from_file}, + {"test_loadobjopt_quad_triangulation", test_loadobjopt_quad_triangulation}, + {"test_loadobjopt_no_triangulation", test_loadobjopt_no_triangulation}, + {"test_loadobjopt_multiple_groups", test_loadobjopt_multiple_groups}, + {"test_loadobjopt_empty_buffer", test_loadobjopt_empty_buffer}, + {"test_loadobjopt_leading_decimal_dot", test_loadobjopt_leading_decimal_dot}, + {"test_loadobjopt_no_trailing_newline", test_loadobjopt_no_trailing_newline}, + {"test_loadobjopt_face_missing_vt_vn", test_loadobjopt_face_missing_vt_vn}, + {"test_loadobjopt_bare_cr_line_endings", test_loadobjopt_bare_cr_line_endings}, + {"test_loadobjopt_degenerate_face", test_loadobjopt_degenerate_face}, + {"test_loadobjopt_usemtl_multiple_faces", test_loadobjopt_usemtl_multiple_faces}, + {"test_loadobjopt_crlf_line_endings", test_loadobjopt_crlf_line_endings}, + {"test_arena_allocator", test_arena_allocator}, + {"test_arena_adapter_with_vector", test_arena_adapter_with_vector}, + {"test_basic_attrib_with_arena", test_basic_attrib_with_arena}, + {"test_loadobjopt_multithread_matches_single_thread", + test_loadobjopt_multithread_matches_single_thread}, + {"test_loadobjopt_multithread_relative_indices_match_single_thread", + test_loadobjopt_multithread_relative_indices_match_single_thread}, + {"test_loadobjopt_mtllib_multiple_filenames", + test_loadobjopt_mtllib_multiple_filenames}, + {"test_loadobjopt_mtllib_repeated_lines", + test_loadobjopt_mtllib_repeated_lines}, + {"test_loadobjopt_points_only_input", test_loadobjopt_points_only_input}, + {"test_loadobjopt_faces_only_input", test_loadobjopt_faces_only_input}, + {"test_loadobjopt_synthetic_benchmark", test_loadobjopt_synthetic_benchmark}, + {"test_loadobjopt_matches_legacy_parser_triangle_soup", + test_loadobjopt_matches_legacy_parser_triangle_soup}, + {"test_loadobjopt_matches_legacy_parser_mtllib_threaded", + test_loadobjopt_matches_legacy_parser_mtllib_threaded}, + {"test_loadobjopt_matches_legacy_parser_colors_smoothing_and_comments", + test_loadobjopt_matches_legacy_parser_colors_smoothing_and_comments}, + {"test_loadobjopt_mixed_vertex_colors_drop_like_legacy", + test_loadobjopt_mixed_vertex_colors_drop_like_legacy}, + {"test_stream_loader_mixed_vertex_colors_drop_like_legacy", + test_stream_loader_mixed_vertex_colors_drop_like_legacy}, + {"test_stream_loader_mixed_vertex_colors_backfill", + test_stream_loader_mixed_vertex_colors_backfill}, + {"test_stream_loader_mtllib_escaped_spaces", + test_stream_loader_mtllib_escaped_spaces}, + {"test_stream_loader_clears_outputs_on_failure", + test_stream_loader_clears_outputs_on_failure}, + {"test_loadobjopt_face_comment_matches_legacy", + test_loadobjopt_face_comment_matches_legacy}, + {"test_loadobjopt_concave_polygon_matches_legacy", + test_loadobjopt_concave_polygon_matches_legacy}, + {"test_stream_loader_concave_polygon_matches_legacy", + test_stream_loader_concave_polygon_matches_legacy}, + {"test_loadobjopt_invalid_polygon_matches_legacy", + test_loadobjopt_invalid_polygon_matches_legacy}, + {"test_stream_loader_hash_in_names", + test_stream_loader_hash_in_names}, + {"test_loadobjopt_hash_in_usemtl_matches_legacy", + test_loadobjopt_hash_in_usemtl_matches_legacy}, + {"test_loadobjopt_usemtl_before_mtllib_matches_legacy", + test_loadobjopt_usemtl_before_mtllib_matches_legacy}, + {"test_loadobjopt_group_comment_matches_legacy", + test_loadobjopt_group_comment_matches_legacy}, + {"test_loadobjopt_invalid_face_token_matches_legacy_error", + test_loadobjopt_invalid_face_token_matches_legacy_error}, + {"test_stream_loader_texcoord_w", + test_stream_loader_texcoord_w}, + {"test_stream_loader_vertex_weight", + test_stream_loader_vertex_weight}, + {"test_stream_loader_colored_vertex_weight", + test_stream_loader_colored_vertex_weight}, + {"test_stream_loader_group_comment_matches_legacy", + test_stream_loader_group_comment_matches_legacy}, + {"test_stream_loader_weighted_vertex_comment_matches_legacy", + test_stream_loader_weighted_vertex_comment_matches_legacy}, + {"test_loadobjopt_mtllib_warning_preserved_on_parse_error", + test_loadobjopt_mtllib_warning_preserved_on_parse_error}, + {"test_loadobjopt_empty_group_warning_matches_legacy", + test_loadobjopt_empty_group_warning_matches_legacy}, + {"test_stream_loader_empty_group_warning_matches_legacy", + test_stream_loader_empty_group_warning_matches_legacy}, + {"test_stream_loader_optional_vertex_token_matches_legacy", + test_stream_loader_optional_vertex_token_matches_legacy}, + {"test_loadobjopt_buffer_ignores_mtllib_like_legacy_stream", + test_loadobjopt_buffer_ignores_mtllib_like_legacy_stream}, + {"test_loadobjopt_parse_error_reports_record_type", + test_loadobjopt_parse_error_reports_record_type}, + {"test_stream_loader_object_spacing_matches_legacy", + test_stream_loader_object_spacing_matches_legacy}, + {"test_loadobjopt_object_spacing_matches_legacy", + test_loadobjopt_object_spacing_matches_legacy}, + {"test_loadobjopt_empty_mtllib_warning_matches_legacy_file", + test_loadobjopt_empty_mtllib_warning_matches_legacy_file}, + {"test_stream_loader_empty_mtllib_warning_matches_legacy", + test_stream_loader_empty_mtllib_warning_matches_legacy}, + {"test_stream_loader_warnings_preserved_on_parse_error", + test_stream_loader_warnings_preserved_on_parse_error}, + {"test_stream_loader_material_resolution_without_material_output", + test_stream_loader_material_resolution_without_material_output}, + {"test_loadobjopt_matches_legacy_extended_features", + test_loadobjopt_matches_legacy_extended_features}, + {"test_loadobjopt_vertex_weight_and_texcoord_w_match_legacy", + test_loadobjopt_vertex_weight_and_texcoord_w_match_legacy}, + {"test_loadobjopt_single_component_texcoord_matches_legacy", + test_loadobjopt_single_component_texcoord_matches_legacy}, + {"test_stream_loader_single_component_texcoord_matches_legacy", + test_stream_loader_single_component_texcoord_matches_legacy}, + {"test_stream_loader_embedded_carriage_return_matches_legacy", + test_stream_loader_embedded_carriage_return_matches_legacy}, + {"test_stream_loader_embedded_nul_empty_shape_matches_legacy", + test_stream_loader_embedded_nul_empty_shape_matches_legacy}, + {"test_stream_loader_object_only_matches_legacy", + test_stream_loader_object_only_matches_legacy}, + {"test_stream_loader_group_only_matches_legacy", + test_stream_loader_group_only_matches_legacy}, + {"test_stream_loader_degenerate_face_matches_legacy", + test_stream_loader_degenerate_face_matches_legacy}, + {"test_stream_loader_degenerate_face_warning_order_matches_legacy", + test_stream_loader_degenerate_face_warning_order_matches_legacy}, + {"test_stream_loader_degenerate_face_warning_suppressed_on_parse_error", + test_stream_loader_degenerate_face_warning_suppressed_on_parse_error}, + {"test_loadobjopt_degenerate_face_matches_legacy", + test_loadobjopt_degenerate_face_matches_legacy}, + {"test_loadobjopt_degenerate_face_warning_order_matches_legacy", + test_loadobjopt_degenerate_face_warning_order_matches_legacy}, + {"test_loadobjopt_degenerate_face_warning_suppressed_on_parse_error", + test_loadobjopt_degenerate_face_warning_suppressed_on_parse_error}, + {"test_loadobjopt_initial_named_shape_matches_legacy", + test_loadobjopt_initial_named_shape_matches_legacy}, + {"test_loadobjopt_invalid_relative_vertex_index_matches_legacy_error", + test_loadobjopt_invalid_relative_vertex_index_matches_legacy_error}, + {"test_loadobjopt_invalid_relative_texcoord_normal_index_matches_legacy_error", + test_loadobjopt_invalid_relative_texcoord_normal_index_matches_legacy_error}, + {"test_stream_loader_invalid_relative_vertex_index_matches_legacy_error", + test_stream_loader_invalid_relative_vertex_index_matches_legacy_error}, + {"test_stream_loader_invalid_relative_texcoord_normal_index_matches_legacy_error", + test_stream_loader_invalid_relative_texcoord_normal_index_matches_legacy_error}, + {"test_loadobjopt_out_of_bounds_warning_matches_legacy", + test_loadobjopt_out_of_bounds_warning_matches_legacy}, + {"test_stream_loader_out_of_bounds_warning_matches_legacy", + test_stream_loader_out_of_bounds_warning_matches_legacy}, + {"test_loadobjopt_zero_vertex_index_warning_matches_legacy", + test_loadobjopt_zero_vertex_index_warning_matches_legacy}, + {"test_stream_loader_zero_vertex_index_warning_matches_legacy", + test_stream_loader_zero_vertex_index_warning_matches_legacy}, + {"test_loadobjopt_zero_texcoord_normal_indices_match_legacy", + test_loadobjopt_zero_texcoord_normal_indices_match_legacy}, + {"test_stream_loader_zero_texcoord_normal_indices_match_legacy", + test_stream_loader_zero_texcoord_normal_indices_match_legacy}, {"test_streamreader_eof_and_remaining", test_streamreader_eof_and_remaining}, {"test_streamreader_skip_and_read", test_streamreader_skip_and_read}, @@ -3482,4 +4500,18 @@ TEST_LIST = { test_empty_mtl_no_phantom_material}, {"test_streamreader_not_copyable", test_streamreader_not_copyable}, {"test_out_of_range_face_index", test_out_of_range_face_index}, + {"test_loadobjopt_typed_from_buffer", test_loadobjopt_typed_from_buffer}, + {"test_loadobjopt_typed_multiple_groups", test_loadobjopt_typed_multiple_groups}, + {"test_loadobjopt_typed_optional_arrays_lazy", test_loadobjopt_typed_optional_arrays_lazy}, + {"test_loadobjopt_typed_matches_loadobjopt", test_loadobjopt_typed_matches_loadobjopt}, + {"test_loadobjopt_typed_empty_buffer", test_loadobjopt_typed_empty_buffer}, + {"test_loadobjopt_typed_move_semantics", test_loadobjopt_typed_move_semantics}, + {"test_loadobjopt_nan_inf_values", test_loadobjopt_nan_inf_values}, + {"test_loadobjopt_typed_vertex_color_6_and_7", + test_loadobjopt_typed_vertex_color_6_and_7}, + {"test_arena_adapter_overflow_guard", test_arena_adapter_overflow_guard}, + {"test_loadobjopt_object_name_trimming", + test_loadobjopt_object_name_trimming}, + {"test_loadobjopt_mixed_line_endings", + test_loadobjopt_mixed_line_endings}, {NULL, NULL}}; diff --git a/tiny_obj_loader.h b/tiny_obj_loader.h index af98ac2d..e2d0d191 100644 --- a/tiny_obj_loader.h +++ b/tiny_obj_loader.h @@ -64,17 +64,23 @@ THE SOFTWARE. #ifndef TINY_OBJ_LOADER_H_ #define TINY_OBJ_LOADER_H_ +#include #include #include #include +#include +#include +#include +#include +#include + namespace tinyobj { // C++11 is now the minimum required standard. #if __cplusplus < 201103L && (!defined(_MSVC_LANG) || _MSVC_LANG < 201103L) #error "tinyobjloader requires C++11 or later. Compile with -std=c++11 or higher." #endif -#define TINYOBJ_OVERRIDE override #ifdef __clang__ #pragma clang diagnostic push @@ -325,26 +331,115 @@ struct material_t { #endif }; -struct tag_t { - std::string name; - - std::vector intValues; - std::vector floatValues; - std::vector stringValues; +template > +struct basic_tag_t { + using allocator_type = Alloc; + using char_alloc = + typename std::allocator_traits::template rebind_alloc; + using string_type = + std::basic_string, char_alloc>; + using int_alloc = + typename std::allocator_traits::template rebind_alloc; + using real_alloc = + typename std::allocator_traits::template rebind_alloc; + using string_alloc = + typename std::allocator_traits::template rebind_alloc; + + string_type name; + std::vector intValues; + std::vector floatValues; + std::vector stringValues; + + basic_tag_t() + : name(), + intValues(), + floatValues(), + stringValues() {} + + explicit basic_tag_t(const allocator_type &alloc) + : name(char_alloc(alloc)), + intValues(int_alloc(alloc)), + floatValues(real_alloc(alloc)), + stringValues(string_alloc(alloc)) {} + + template + basic_tag_t(const basic_tag_t &rhs) : name(rhs.name) { + intValues.assign(rhs.intValues.begin(), rhs.intValues.end()); + floatValues.assign(rhs.floatValues.begin(), rhs.floatValues.end()); + stringValues.assign(rhs.stringValues.begin(), rhs.stringValues.end()); + } + + template + basic_tag_t(const basic_tag_t &rhs, const allocator_type &alloc) + : name(rhs.name.begin(), rhs.name.end(), char_alloc(alloc)), + intValues(int_alloc(alloc)), + floatValues(real_alloc(alloc)), + stringValues(string_alloc(alloc)) { + intValues.assign(rhs.intValues.begin(), rhs.intValues.end()); + floatValues.assign(rhs.floatValues.begin(), rhs.floatValues.end()); + for (size_t i = 0; i < rhs.stringValues.size(); i++) { + stringValues.emplace_back(rhs.stringValues[i].begin(), + rhs.stringValues[i].end(), char_alloc(alloc)); + } + } + + template + basic_tag_t &operator=(const basic_tag_t &rhs) { + name = rhs.name; + intValues.assign(rhs.intValues.begin(), rhs.intValues.end()); + floatValues.assign(rhs.floatValues.begin(), rhs.floatValues.end()); + stringValues.assign(rhs.stringValues.begin(), rhs.stringValues.end()); + return *this; + } }; +using tag_t = basic_tag_t<>; + struct joint_and_weight_t { int joint_id; real_t weight; }; -struct skin_weight_t { +template > +struct basic_skin_weight_t { + using allocator_type = Alloc; + using joint_weight_alloc = + typename std::allocator_traits::template rebind_alloc< + joint_and_weight_t>; + int vertex_id; // Corresponding vertex index in `attrib_t::vertices`. // Compared to `index_t`, this index must be positive and // start with 0(does not allow relative indexing) - std::vector weightValues; + std::vector weightValues; + + basic_skin_weight_t() : vertex_id(0) {} + + explicit basic_skin_weight_t(const allocator_type &alloc) + : vertex_id(0), weightValues(joint_weight_alloc(alloc)) {} + + template + basic_skin_weight_t(const basic_skin_weight_t &rhs) + : vertex_id(rhs.vertex_id) { + weightValues.assign(rhs.weightValues.begin(), rhs.weightValues.end()); + } + + template + basic_skin_weight_t(const basic_skin_weight_t &rhs, + const allocator_type &alloc) + : vertex_id(rhs.vertex_id), weightValues(joint_weight_alloc(alloc)) { + weightValues.assign(rhs.weightValues.begin(), rhs.weightValues.end()); + } + + template + basic_skin_weight_t &operator=(const basic_skin_weight_t &rhs) { + vertex_id = rhs.vertex_id; + weightValues.assign(rhs.weightValues.begin(), rhs.weightValues.end()); + return *this; + } }; +using skin_weight_t = basic_skin_weight_t<>; + // Index struct to support different indices for vtx/normal/texcoord. // -1 means not used. struct index_t { @@ -477,11 +572,11 @@ class MaterialFileReader : public MaterialReader { // Path could contain separator(';' in Windows, ':' in Posix) explicit MaterialFileReader(const std::string &mtl_basedir) : m_mtlBaseDir(mtl_basedir) {} - virtual ~MaterialFileReader() TINYOBJ_OVERRIDE {} + virtual ~MaterialFileReader() override {} virtual bool operator()(const std::string &matId, std::vector *materials, std::map *matMap, std::string *warn, - std::string *err) TINYOBJ_OVERRIDE; + std::string *err) override; private: std::string m_mtlBaseDir; @@ -494,11 +589,11 @@ class MaterialStreamReader : public MaterialReader { public: explicit MaterialStreamReader(std::istream &inStream) : m_inStream(inStream) {} - virtual ~MaterialStreamReader() TINYOBJ_OVERRIDE {} + virtual ~MaterialStreamReader() override {} virtual bool operator()(const std::string &matId, std::vector *materials, std::map *matMap, std::string *warn, - std::string *err) TINYOBJ_OVERRIDE; + std::string *err) override; private: std::istream &m_inStream; @@ -649,6 +744,446 @@ bool ParseTextureNameAndOption(std::string *texname, texture_option_t *texopt, /// =<<========== Legacy v1 API ============================================= +/// ==>>========= Optimized API (C++11 required) ============================ +/// +/// Enable compile options: +/// TINYOBJLOADER_USE_MULTITHREADING - multi-threaded parsing +/// TINYOBJLOADER_USE_SIMD - SIMD-accelerated line scanning +/// +/// These features require C++11 or later. +/// + +/// +/// Arena-based memory allocator for reduced allocation overhead when +/// loading huge meshes. Memory is freed in bulk when the arena is +/// destroyed or reset(). Individual deallocate() calls are no-ops. +/// +class ArenaAllocator { + public: + explicit ArenaAllocator(size_t block_size = 1024 * 1024) + : head_(nullptr), default_block_size_(block_size) {} + + ~ArenaAllocator() { destroy(); } + + ArenaAllocator(const ArenaAllocator &) = delete; + ArenaAllocator &operator=(const ArenaAllocator &) = delete; + + ArenaAllocator(ArenaAllocator &&o) noexcept + : head_(o.head_), default_block_size_(o.default_block_size_) { + o.head_ = nullptr; + } + ArenaAllocator &operator=(ArenaAllocator &&o) noexcept { + if (this != &o) { + destroy(); + head_ = o.head_; + default_block_size_ = o.default_block_size_; + o.head_ = nullptr; + } + return *this; + } + + void *allocate(size_t bytes, + size_t alignment = sizeof(void *)); + + /// Free all memory at once. + void reset(); + + private: + struct Block { + unsigned char *data; + size_t capacity; + size_t used; + Block *next; + }; + + Block *head_; + size_t default_block_size_; + + Block *new_block(size_t min_bytes); + void destroy(); +}; + +/// +/// STL-compatible allocator adapter backed by an ArenaAllocator. +/// deallocate() is a no-op — memory is released when the arena is reset. +/// +template +class arena_adapter { + public: + using value_type = T; + using pointer = T *; + using const_pointer = const T *; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using propagate_on_container_copy_assignment = std::true_type; + using propagate_on_container_move_assignment = std::true_type; + using propagate_on_container_swap = std::true_type; + + explicit arena_adapter(ArenaAllocator *arena = nullptr) noexcept + : arena_(arena) {} + + template + arena_adapter(const arena_adapter &other) noexcept + : arena_(other.arena()) {} + + T *allocate(size_t n) { + if (n > SIZE_MAX / sizeof(T)) { +#ifdef TINYOBJLOADER_ENABLE_EXCEPTION + throw std::bad_alloc(); +#else + return nullptr; +#endif + } + if (arena_) { + return static_cast(arena_->allocate(n * sizeof(T), alignof(T))); + } +#ifdef TINYOBJLOADER_ENABLE_EXCEPTION + return static_cast(::operator new(n * sizeof(T))); +#else + return static_cast(::operator new(n * sizeof(T), std::nothrow)); +#endif + } + + void deallocate(T *p, size_t) noexcept { + if (!arena_) { + ::operator delete(p); + return; + } + // Arena deallocation is a no-op; memory freed in bulk via reset(). + } + + ArenaAllocator *arena() const noexcept { return arena_; } + + template + bool operator==(const arena_adapter &o) const noexcept { + return arena_ == o.arena(); + } + template + bool operator!=(const arena_adapter &o) const noexcept { + return arena_ != o.arena(); + } + + template + struct rebind { + using other = arena_adapter; + }; + + private: + ArenaAllocator *arena_; +}; + +/// +/// Template mesh type supporting custom allocators. +/// Note: Alloc must be default-constructible (stateless). For stateful +/// allocators like arena_adapter, construct vectors individually with +/// an allocator instance. +/// +template > +struct basic_mesh_t { + using index_alloc = + typename std::allocator_traits::template rebind_alloc; + using uint_alloc = + typename std::allocator_traits::template rebind_alloc; + using int_alloc = + typename std::allocator_traits::template rebind_alloc; + using tag_alloc = + typename std::allocator_traits::template rebind_alloc< + basic_tag_t >; + + std::vector indices; + std::vector num_face_vertices; + std::vector material_ids; + std::vector smoothing_group_ids; + std::vector, tag_alloc> tags; +}; + +template > +struct basic_lines_t { + using index_alloc = + typename std::allocator_traits::template rebind_alloc; + using int_alloc = + typename std::allocator_traits::template rebind_alloc; + + std::vector indices; + std::vector num_line_vertices; +}; + +template > +struct basic_points_t { + using index_alloc = + typename std::allocator_traits::template rebind_alloc; + + std::vector indices; +}; + +/// +/// Template shape type supporting custom allocators. +/// `name` always uses the default allocator; only mesh buffers are +/// allocator-aware. +/// +template > +struct basic_shape_t { + std::string name; + basic_mesh_t mesh; + basic_lines_t lines; + basic_points_t points; +}; + +/// +/// Template attrib type supporting custom allocators. +/// Flat arrays: vertices(xyz), normals(xyz), texcoords(uv). +/// Note: Alloc must be default-constructible (stateless). For stateful +/// allocators like arena_adapter, construct vectors individually with +/// an allocator instance. +/// +template > +struct basic_attrib_t { + using real_alloc = + typename std::allocator_traits::template rebind_alloc; + using int_alloc = + typename std::allocator_traits::template rebind_alloc; + using index_alloc = + typename std::allocator_traits::template rebind_alloc; + using skin_weight_alloc = + typename std::allocator_traits::template rebind_alloc< + basic_skin_weight_t >; + + std::vector vertices; // xyz + std::vector vertex_weights; // optional w for `v` + std::vector normals; // xyz + std::vector texcoords; // uv + std::vector texcoord_ws; // optional w for `vt` + std::vector colors; // rgb (optional) + std::vector, skin_weight_alloc> skin_weights; + std::vector indices; // face indices + std::vector face_num_verts; // verts per face + std::vector material_ids; // per-face material +}; + +/// +/// Configuration for the optimized loader. +/// +struct OptLoadConfig { + /// Number of threads. -1 = hardware_concurrency, 0 or 1 = single-threaded. + /// Effective only when TINYOBJLOADER_USE_MULTITHREADING is defined. + int num_threads; + + bool triangulate; ///< Triangulate polygons (fan triangulation). + bool verbose; ///< Reserved for future use (currently has no effect). + + /// Enable trie-based float string → value cache. + /// Speeds up files with many repeated coordinate values (e.g. "0.0", "1.0"). + bool float_cache; + + /// Maximum trie nodes per thread (controls cache memory). + /// Each node is ~36 bytes, so 1024 nodes ≈ 36 KB (fits in L1 cache). + int float_cache_max_nodes; + + /// Use fp32-length keys for the float cache (max 8 chars). + /// When true, only tokens up to `float::digits10 + 2` characters are cached. + /// When false, uses `real_t::digits10 + 2` (longer keys, fewer hits). + /// Has no effect when float_cache is false. + bool fp32_cache; + + OptLoadConfig() + : num_threads(-1), triangulate(true), verbose(false), + float_cache(false), float_cache_max_nodes(1024), fp32_cache(true) {} +}; + +/// Optimized loader — parse from a raw memory buffer. +/// Supports multi-threading (TINYOBJLOADER_USE_MULTITHREADING) and +/// SIMD line scanning (TINYOBJLOADER_USE_SIMD). +bool LoadObjOpt(basic_attrib_t<> *attrib, + std::vector> *shapes, + std::vector *materials, + std::string *warn, std::string *err, + const char *buf, size_t buf_len, + const OptLoadConfig &config = OptLoadConfig()); + +/// Optimized loader — load from a file. +bool LoadObjOpt(basic_attrib_t<> *attrib, + std::vector> *shapes, + std::vector *materials, + std::string *warn, std::string *err, + const char *filename, + const char *mtl_basedir = nullptr, + const OptLoadConfig &config = OptLoadConfig()); + +// ---- TypedArray-based zero-copy API for maximum efficiency ---- + +/// +/// Non-owning, contiguous array view allocated from an ArenaAllocator. +/// No capacity tracking, no realloc. Lifetime is tied to the arena. +/// +template +class TypedArray { + static_assert(std::is_trivially_copyable::value, + "TypedArray requires T to be trivially copyable " + "(uses memset for initialization)."); + + public: + TypedArray() : data_(nullptr), size_(0) {} + + T *data() { return data_; } + const T *data() const { return data_; } + size_t size() const { return size_; } + bool empty() const { return size_ == 0; } + + T &operator[](size_t i) { return data_[i]; } + const T &operator[](size_t i) const { return data_[i]; } + + T *begin() { return data_; } + T *end() { return data_ + size_; } + const T *begin() const { return data_; } + const T *end() const { return data_ + size_; } + + /// Allocate `count` elements from `arena`. Previous contents are abandoned. + /// Elements are zero-initialized via memset (T must be trivially copyable). + /// Returns false if allocation fails (only possible when exceptions are + /// disabled via TINYOBJLOADER_ENABLE_EXCEPTION not being defined). + bool allocate(ArenaAllocator &arena, size_t count) { + if (count == 0) { + data_ = nullptr; + size_ = 0; + return true; + } + // Guard against size_t overflow in count * sizeof(T). + if (count > SIZE_MAX / sizeof(T)) { +#ifdef TINYOBJLOADER_ENABLE_EXCEPTION + throw std::bad_alloc(); +#else + data_ = nullptr; + size_ = 0; + return false; +#endif + } + void *p = arena.allocate(count * sizeof(T), alignof(T)); + if (!p) { + data_ = nullptr; + size_ = 0; + return false; + } + data_ = static_cast(p); + size_ = count; + std::memset(data_, 0, count * sizeof(T)); + return true; + } + + /// Wrap an existing arena-allocated pointer. Caller must ensure ptr + /// points into a live arena and count is within bounds. + void set(T *ptr, size_t count) { + data_ = ptr; + size_ = count; + } + + /// Shrink the logical size (no memory freed — arena owns it). + void truncate(size_t new_size) { + if (new_size < size_) size_ = new_size; + } + + void clear() { + data_ = nullptr; + size_ = 0; + } + + private: + T *data_; + size_t size_; +}; + +/// +/// A shape expressed as a range into the flat attrib arrays. +/// No owned memory — just offsets + counts. +/// +struct OptShapeRange { + std::string name; + size_t face_offset; ///< Start index into OptAttrib::face_num_verts / material_ids + size_t face_count; ///< Number of faces in this shape + size_t index_offset; ///< Start index into OptAttrib::indices + size_t index_count; ///< Number of index_t entries for this shape + + OptShapeRange() + : face_offset(0), face_count(0), index_offset(0), index_count(0) {} +}; + +/// +/// Flat attribute storage using TypedArray (arena-backed). +/// All arrays are allocated from the OptResult's arena. +/// +/// Unlike basic_attrib_t (used by LoadObjOpt), optional arrays like +/// vertex_weights, texcoord_ws, colors, and smoothing_group_ids are only +/// allocated when the input actually contains the corresponding data. +/// Check .empty() before accessing. +/// +struct OptAttrib { + TypedArray vertices; ///< xyz, length = num_vertices * 3 + TypedArray vertex_weights; ///< w per vertex (empty if no `v` line has w) + TypedArray normals; ///< xyz, length = num_normals * 3 + TypedArray texcoords; ///< uv, length = num_texcoords * 2 + TypedArray texcoord_ws; ///< w per texcoord (empty if no `vt` has w) + TypedArray colors; ///< rgb per vertex (empty if not all verts have color) + + TypedArray indices; ///< flattened face indices + TypedArray face_num_verts; ///< vertices per face + TypedArray material_ids; ///< per-face material id + TypedArray smoothing_group_ids; ///< per-face smoothing group +}; + +/// +/// Complete result from the TypedArray-based optimized loader. +/// Owns the ArenaAllocator — all TypedArray pointers are valid as long +/// as this object is alive. Move-only. +/// +struct OptResult { + ArenaAllocator arena; + OptAttrib attrib; + std::vector shapes; ///< views into attrib arrays + std::vector materials; + bool valid; + + OptResult() : arena(4 * 1024 * 1024), valid(false) {} + + OptResult(OptResult &&o) noexcept + : arena(std::move(o.arena)), + attrib(o.attrib), + shapes(std::move(o.shapes)), + materials(std::move(o.materials)), + valid(o.valid) { + o.attrib = OptAttrib(); // clear dangling pointers + o.valid = false; + } + + OptResult &operator=(OptResult &&o) noexcept { + if (this != &o) { + arena = std::move(o.arena); + attrib = o.attrib; + shapes = std::move(o.shapes); + materials = std::move(o.materials); + valid = o.valid; + o.attrib = OptAttrib(); + o.valid = false; + } + return *this; + } + + OptResult(const OptResult &) = delete; + OptResult &operator=(const OptResult &) = delete; +}; + +/// TypedArray-based optimized loader — parse from a raw memory buffer. +/// Returns OptResult that owns all allocated memory via its arena. +OptResult LoadObjOptTyped(const char *buf, size_t buf_len, + std::string *warn, std::string *err, + const OptLoadConfig &config = OptLoadConfig()); + +/// TypedArray-based optimized loader — load from a file. +OptResult LoadObjOptTyped(const char *filename, + std::string *warn, std::string *err, + const char *mtl_basedir = nullptr, + const OptLoadConfig &config = OptLoadConfig()); + +/// =<<========== Optimized API ============================================= + } // namespace tinyobj #endif // TINY_OBJ_LOADER_H_ @@ -772,6 +1307,25 @@ static std::wstring LongPathW(const std::wstring &wpath) { } #endif // _WIN32 +#ifdef TINYOBJLOADER_USE_MULTITHREADING +#include +#include +#endif +#ifdef TINYOBJLOADER_USE_SIMD +#if defined(__SSE2__) || defined(_M_X64) || defined(_M_AMD64) || \ + (defined(_M_IX86_FP) && _M_IX86_FP >= 2) +#define TINYOBJLOADER_SIMD_SSE2 1 +#include +#if defined(__AVX2__) +#define TINYOBJLOADER_SIMD_AVX2 1 +#include +#endif +#elif defined(__ARM_NEON) || defined(__ARM_NEON__) +#define TINYOBJLOADER_SIMD_NEON 1 +#include +#endif +#endif // TINYOBJLOADER_USE_SIMD + // -------------------------------------------------------------------------- // Embedded fast_float v8.0.2 for high-performance, bit-exact float parsing. // Disable by defining TINYOBJLOADER_DISABLE_FAST_FLOAT before including @@ -8411,8 +8965,16 @@ static bool LoadObjInternal(attrib_t *attrib, std::vector *shapes, return false; } + // Clamp to int range to avoid UB on float-to-int overflow. + if (j > static_cast(std::numeric_limits::max())) { + if (err) { + (*err) += sr.format_error(filename, + "failed to parse `vw' line: joint_id overflow"); + } + return false; + } joint_and_weight_t jw; - jw.joint_id = int(j); + jw.joint_id = static_cast(j); jw.weight = w; sw.weightValues.push_back(jw); @@ -9259,6 +9821,4060 @@ bool ObjReader::ParseFromString(const std::string &obj_text, return valid_; } +// =========================================================================== +// Optimized API implementation (C++11+) +// =========================================================================== +// ---- ArenaAllocator implementation ---- + +void *ArenaAllocator::allocate(size_t bytes, size_t alignment) { + if (bytes == 0) bytes = 1; + + // Try to allocate from current block + if (head_) { + size_t space = head_->capacity - head_->used; + void *ptr = head_->data + head_->used; + if (std::align(alignment, bytes, ptr, space)) { + head_->used = static_cast(static_cast(ptr) - + head_->data) + + bytes; + return ptr; + } + } + + // Guard against size_t overflow in bytes + alignment + if (bytes > SIZE_MAX - alignment) { +#ifdef TINYOBJLOADER_ENABLE_EXCEPTION + throw std::bad_alloc(); +#else + return nullptr; +#endif + } + + // Need a new block + Block *b = new_block(bytes + alignment); + if (!b) return nullptr; + size_t space = b->capacity; + void *ptr = b->data; + if (!std::align(alignment, bytes, ptr, space)) { + // Defensive guard: the block was allocated with capacity >= bytes + alignment, + // so alignment should always succeed. This handles edge cases where the + // capacity was insufficient due to unusual alignment requirements. +#ifdef TINYOBJLOADER_ENABLE_EXCEPTION + throw std::bad_alloc(); +#else + return nullptr; +#endif + } + b->used = + static_cast(static_cast(ptr) - b->data) + bytes; + return ptr; +} + +void ArenaAllocator::reset() { destroy(); } + +ArenaAllocator::Block *ArenaAllocator::new_block(size_t min_bytes) { + size_t cap = (min_bytes > default_block_size_) ? min_bytes + : default_block_size_; +#ifdef TINYOBJLOADER_ENABLE_EXCEPTION + // Allocate data buffer first: if this throws std::bad_alloc, no Block + // struct is leaked. If the subsequent Block allocation throws (very + // unlikely for a small POD), the data buffer is cleaned up. + unsigned char *data = new unsigned char[cap]; + Block *b; + try { + b = new Block; + } catch (...) { + delete[] data; + throw; + } + b->data = data; +#else + Block *b = new (std::nothrow) Block; + if (!b) return nullptr; + b->data = new (std::nothrow) unsigned char[cap]; + if (!b->data) { delete b; return nullptr; } +#endif + b->capacity = cap; + b->used = 0; + b->next = head_; + head_ = b; + return b; +} + +void ArenaAllocator::destroy() { + Block *b = head_; + while (b) { + Block *next = b->next; + delete[] b->data; + delete b; + b = next; + } + head_ = nullptr; +} + +// ---- Optimized parser internals ---- + +namespace opt_internal { + +static const int kOptMaxThreads = 32; + +struct LineInfo { + size_t pos; + size_t len; +}; + +#define TINYOBJ_OPT_IS_SPACE(x) (((x) == ' ') || ((x) == '\t')) +#define TINYOBJ_OPT_IS_DIGIT(x) \ + (static_cast((x) - '0') < static_cast(10)) +#define TINYOBJ_OPT_IS_NEW_LINE(x) \ + (((x) == '\r') || ((x) == '\n') || ((x) == '\0')) + +static inline void opt_skip_space(const char **token) { + while ((**token) == ' ' || (**token) == '\t') { + (*token)++; + } +} + +static inline int opt_until_space(const char *token) { + const char *p = token; + while (p[0] != '\0' && p[0] != ' ' && p[0] != '\t' && p[0] != '\r' && + p[0] != '\n') { + p++; + } + return static_cast(p - token); +} + +static inline int opt_my_atoi(const char *c) { + unsigned int value = 0; + int sign = 1; + if (*c == '+' || *c == '-') { + if (*c == '-') sign = -1; + c++; + } + while ((*c >= '0') && (*c <= '9')) { + const unsigned int digit = static_cast(*c - '0'); + const unsigned int limit = (sign < 0) + ? static_cast(INT_MAX) + 1u + : static_cast(INT_MAX); + if (value > (limit / 10u) || + (value == (limit / 10u) && digit > (limit % 10u))) { + return (sign < 0) ? INT_MIN : INT_MAX; + } + value = value * 10u + digit; + c++; + } + if (sign < 0) { + if (value == static_cast(INT_MAX) + 1u) { + return INT_MIN; + } + return -static_cast(value); + } + return static_cast(value); +} + +static inline const char *opt_find_index_token_end(const char *token) { + const char *end = token; + while (*end != '\0' && *end != '/' && *end != ' ' && *end != '\t' && + *end != '\r' && *end != '\n') { + end++; + } + return end; +} + +static inline bool opt_tryParseIndexToken(const char *token, const char *end, + int *value) { + if (!value || !token || !end || token >= end) return false; + + const char *cursor = token; + if (*cursor == '+' || *cursor == '-') { + cursor++; + } + if (cursor >= end) return false; + while (cursor < end) { + if (!TINYOBJ_OPT_IS_DIGIT(*cursor)) return false; + cursor++; + } + + *value = opt_my_atoi(token); + return true; +} + +static inline bool opt_resolveIndexLikeLegacy(int idx, int n, int *ret, + bool allow_zero) { + if (!ret) return false; + if (idx > 0) { + (*ret) = idx - 1; + return true; + } + if (idx == 0) { + (*ret) = -1; + return allow_zero; + } + + (*ret) = n + idx; + return ((*ret) >= 0); +} + +static inline void opt_appendZeroIndexWarning(std::string *warn, + const std::string &source_name, + size_t line_num) { + if (!warn) return; + + std::stringstream ss; + ss << source_name << ":" << line_num + << ": warning: zero value index found (will have a value of -1 for " + "normal and tex indices)\n"; + (*warn) += ss.str(); +} + +static inline bool opt_validateAndResolveFaceIndexLikeLegacy( + int raw_idx, int n, bool allow_zero, const std::string &source_name, + size_t line_num, std::string *warn, int *resolved_idx) { + if (raw_idx > 0) { + if (resolved_idx) { + (*resolved_idx) = raw_idx - 1; + } + return true; + } + + if (raw_idx == 0) { + opt_appendZeroIndexWarning(warn, source_name, line_num); + if (resolved_idx) { + (*resolved_idx) = -1; + } + return allow_zero; + } + + if (resolved_idx) { + (*resolved_idx) = n + raw_idx; + return ((*resolved_idx) >= 0); + } + + return ((n + raw_idx) >= 0); +} + +static inline void opt_updateGreatestIndex(int idx, int *greatest) { + if (!greatest) return; + if (idx > *greatest) { + *greatest = idx; + } +} + +static inline void opt_appendOutOfBoundsWarnings(std::string *warn, + int greatest_v_idx, + int greatest_vn_idx, + int greatest_vt_idx, + int num_vertices, + int num_normals, + int num_texcoords, + size_t line_num) { + if (!warn) return; + + if (greatest_v_idx >= num_vertices) { + std::stringstream ss; + ss << "Vertex indices out of bounds (line " << line_num << ".)\n\n"; + (*warn) += ss.str(); + } + if (greatest_vn_idx >= num_normals) { + std::stringstream ss; + ss << "Vertex normal indices out of bounds (line " << line_num << ".)\n\n"; + (*warn) += ss.str(); + } + if (greatest_vt_idx >= num_texcoords) { + std::stringstream ss; + ss << "Vertex texcoord indices out of bounds (line " << line_num + << ".)\n\n"; + (*warn) += ss.str(); + } +} + +static bool opt_tryParseDouble(const char *s, const char *s_end, + double *result) { + if (s >= s_end) return false; + +#ifndef TINYOBJLOADER_DISABLE_FAST_FLOAT + // Use the already-embedded fast_float for high-performance parsing. + // Handle nan/inf with OBJ-compatible replacement values first. + { + const char *p = s; + if (p < s_end && (*p == '+' || *p == '-')) ++p; + if (p < s_end) { + char fc = *p; + if (fc >= 'A' && fc <= 'Z') fc += 32; + if (fc == 'n' || fc == 'i') { + const char *end_ptr; + if (detail_fp::tryParseNanInf(s, s_end, result, &end_ptr)) { + return true; + } + } + } + } + + double tmp; + auto r = fast_float::from_chars(s, s_end, tmp, + fast_float::chars_format::general | + fast_float::chars_format::allow_leading_plus); + if (r.ec == tinyobj_ff::ff_errc::ok) { + *result = tmp; + return true; + } + return false; +#else + // Fallback: hand-written float parser + + // Handle nan/inf keywords with OBJ-compatible replacement values. + { + const char *p = s; + bool neg = false; + if (p < s_end && *p == '-') { neg = true; ++p; } + else if (p < s_end && *p == '+') { ++p; } + if (p < s_end) { + char fc = *p; + if (fc >= 'A' && fc <= 'Z') fc += 32; + if (fc == 'n' && (p + 2 < s_end)) { + char c1 = p[1], c2 = p[2]; + if (c1 >= 'A' && c1 <= 'Z') c1 += 32; + if (c2 >= 'A' && c2 <= 'Z') c2 += 32; + if (c1 == 'a' && c2 == 'n') { + *result = 0.0; + return true; + } + } + if (fc == 'i' && (p + 2 < s_end)) { + char c1 = p[1], c2 = p[2]; + if (c1 >= 'A' && c1 <= 'Z') c1 += 32; + if (c2 >= 'A' && c2 <= 'Z') c2 += 32; + if (c1 == 'n' && c2 == 'f') { + *result = neg ? std::numeric_limits::lowest() + : (std::numeric_limits::max)(); + return true; + } + } + } + } + + double mantissa = 0.0; + int exponent = 0; + char sign = '+'; + char exp_sign = '+'; + const char *curr = s; + int read = 0; + bool end_not_reached = false; + bool has_leading_decimal = false; + + if (*curr == '+' || *curr == '-') { + sign = *curr; + curr++; + } + + if (curr == s_end) return false; + + if (*curr == '.') { + has_leading_decimal = true; + } else if (!TINYOBJ_OPT_IS_DIGIT(*curr)) { + return false; + } + + end_not_reached = (curr != s_end); + if (!has_leading_decimal) { + while (end_not_reached && TINYOBJ_OPT_IS_DIGIT(*curr)) { + mantissa *= 10; + mantissa += static_cast(*curr - '0'); + curr++; + read++; + end_not_reached = (curr != s_end); + } + if (read == 0) return false; + } + if (!end_not_reached) goto opt_assemble; + + if (*curr == '.') { + curr++; + end_not_reached = (curr != s_end); + double frac_scale = 0.1; + while (end_not_reached && TINYOBJ_OPT_IS_DIGIT(*curr)) { + mantissa += static_cast(*curr - '0') * frac_scale; + frac_scale *= 0.1; + read++; + curr++; + end_not_reached = (curr != s_end); + } + if (has_leading_decimal && read == 0) return false; + } else if (*curr != 'e' && *curr != 'E') { + goto opt_assemble; + } + + if (!end_not_reached) goto opt_assemble; + + if (*curr == 'e' || *curr == 'E') { + curr++; + end_not_reached = (curr != s_end); + if (!end_not_reached) return false; + if (*curr == '+' || *curr == '-') { + exp_sign = *curr; + curr++; + end_not_reached = (curr != s_end); + } else if (!TINYOBJ_OPT_IS_DIGIT(*curr)) { + return false; + } + read = 0; + end_not_reached = (curr != s_end); + while (end_not_reached && TINYOBJ_OPT_IS_DIGIT(*curr)) { + // Clamp to avoid signed integer overflow (UB). |exponent| > 308 + // already exceeds double range, so further digits are irrelevant. + if (exponent < 0x7FFFFFF) { + exponent *= 10; + exponent += static_cast(*curr - '0'); + } + curr++; + read++; + end_not_reached = (curr != s_end); + } + exponent *= (exp_sign == '+' ? 1 : -1); + if (read == 0) return false; + } + +opt_assemble: + *result = (sign == '+' ? 1.0 : -1.0) * + (exponent ? std::ldexp(mantissa * std::pow(5.0, exponent), exponent) + : mantissa); + return true; +#endif // TINYOBJLOADER_DISABLE_FAST_FLOAT +} + +struct opt_index_t { + int vertex_index, texcoord_index, normal_index; + // Sentinel for "field not present" to distinguish from OBJ relative index -1. + // Using expression form for C++11 static const initializer compatibility. + static const int kNotPresent = -2147483647 - 1; // == std::numeric_limits::min() + opt_index_t() + : vertex_index(kNotPresent), + texcoord_index(kNotPresent), + normal_index(kNotPresent) {} + opt_index_t(int vi, int ti, int ni) + : vertex_index(vi), texcoord_index(ti), normal_index(ni) {} +}; + +static bool opt_parseRawTriple(const char **token, opt_index_t *ret) { + if (!token || !ret) return false; + + opt_index_t vi; + const char *segment_end = opt_find_index_token_end(*token); + if (!opt_tryParseIndexToken(*token, segment_end, &vi.vertex_index)) { + return false; + } + *token = segment_end; + if (**token != '/') { + *ret = vi; + return true; + } + (*token)++; + + if (**token == '/') { + (*token)++; + segment_end = opt_find_index_token_end(*token); + if (!opt_tryParseIndexToken(*token, segment_end, &vi.normal_index)) { + return false; + } + *token = segment_end; + *ret = vi; + return true; + } + + segment_end = opt_find_index_token_end(*token); + if (!opt_tryParseIndexToken(*token, segment_end, &vi.texcoord_index)) { + return false; + } + *token = segment_end; + if (**token != '/') { + *ret = vi; + return true; + } + (*token)++; + segment_end = opt_find_index_token_end(*token); + if (!opt_tryParseIndexToken(*token, segment_end, &vi.normal_index)) { + return false; + } + *token = segment_end; + *ret = vi; + return true; +} + +static inline int opt_length_until_newline(const char *token, size_t n) { + size_t len = 0; + for (len = 0; len < n; len++) { + if (token[len] == '\n' || token[len] == '\r') break; + } + // Trim trailing whitespace + while (len > 0 && (token[len - 1] == ' ' || token[len - 1] == '\t')) { + len--; + } + return static_cast(len); +} + +static inline int opt_length_until_token_or_comment(const char *token, size_t n) { + size_t len = 0; + for (len = 0; len < n; len++) { + const char c = token[len]; + if (c == '\n' || c == '\r' || c == ' ' || c == '\t') break; + } + return static_cast(len); +} + +static inline std::string opt_parseGroupName(const char *token, size_t n) { + std::string name; + size_t i = 0; + while (i < n) { + while (i < n && (token[i] == ' ' || token[i] == '\t')) { + i++; + } + if (i >= n || token[i] == '\n' || token[i] == '\r' || token[i] == '\0' || + token[i] == '#') { + break; + } + + const size_t start = i; + while (i < n && token[i] != '\n' && token[i] != '\r' && + token[i] != '\0' && token[i] != ' ' && token[i] != '\t') { + i++; + } + + if (!name.empty()) { + name.push_back(' '); + } + name.append(token + start, i - start); + } + return name; +} + +static inline bool opt_tryParseFloatToken(real_t *out, const char **token) { + if (!out || !token) return false; + const char *cursor = *token; + opt_skip_space(&cursor); + if (TINYOBJ_OPT_IS_NEW_LINE(cursor[0]) || cursor[0] == '#' || cursor[0] == '\0') { + return false; + } + const char *end = cursor; + while (!TINYOBJ_OPT_IS_NEW_LINE(end[0]) && end[0] != '#' && end[0] != ' ' && + end[0] != '\t' && end[0] != '\0') { + end++; + } +#ifndef TINYOBJLOADER_DISABLE_FAST_FLOAT + // Handle nan/inf with OBJ-compatible values before fast_float. + { + const char *q = cursor; + if (q < end && (*q == '+' || *q == '-')) ++q; + if (q < end) { + char fc = *q; + if (fc >= 'A' && fc <= 'Z') fc += 32; + if (fc == 'n' || fc == 'i') { + double special_val; + const char *end_ptr; + if (detail_fp::tryParseNanInf(cursor, end, &special_val, &end_ptr)) { + *out = static_cast(special_val); + *token = end; + return true; + } + } + } + } + // Parse directly to real_t (float or double) via fast_float — avoids + // the double→float conversion and is ~3-4x faster than the hand-rolled parser. + real_t tmp; + auto r = fast_float::from_chars(cursor, end, tmp, + fast_float::chars_format::general | + fast_float::chars_format::allow_leading_plus); + if (r.ec == tinyobj_ff::ff_errc::ok) { + *out = tmp; + *token = end; + return true; + } + return false; +#else + double val = 0.0; + if (!opt_tryParseDouble(cursor, end, &val)) { + return false; + } + *out = static_cast(val); + *token = end; + return true; +#endif +} + +static inline int opt_count_remaining_scalars(const char *token) { + int count = 0; + const char *cursor = token; + while (true) { + opt_skip_space(&cursor); + if (TINYOBJ_OPT_IS_NEW_LINE(cursor[0]) || cursor[0] == '#' || + cursor[0] == '\0') { + break; + } + count++; + while (!TINYOBJ_OPT_IS_NEW_LINE(cursor[0]) && cursor[0] != '#' && + cursor[0] != ' ' && cursor[0] != '\t' && cursor[0] != '\0') { + cursor++; + } + } + return count; +} + +static inline bool opt_is_comment_start(const char *token) { + return token[0] == '#'; +} + +enum OptCommandType { + OPT_CMD_EMPTY, + OPT_CMD_V, + OPT_CMD_VN, + OPT_CMD_VT, + OPT_CMD_F, + OPT_CMD_G, + OPT_CMD_O, + OPT_CMD_USEMTL, + OPT_CMD_MTLLIB, + OPT_CMD_S +}; + +struct OptCommand { + static const unsigned int kInlineIndexCapacity = 24; + + real_t vx, vy, vz, vw; + real_t vc_r, vc_g, vc_b; + real_t nx, ny, nz; + real_t tx, ty, tw; + bool has_vertex_weight; + bool has_vertex_color; + bool has_texcoord_w; + + opt_index_t f_inline[kInlineIndexCapacity]; + unsigned int f_count; + unsigned int face_vertex_count; + unsigned int emitted_face_count; + unsigned int emitted_face_verts; + std::vector f_heap; + + const char *group_name; + unsigned int group_name_len; + std::string group_name_storage; + const char *object_name; + unsigned int object_name_len; + const char *material_name; + unsigned int material_name_len; + const char *mtllib_name; + unsigned int mtllib_name_len; + size_t source_line; + bool group_name_empty; + bool degenerate_face; + int resolved_material_id; + unsigned int smoothing_group_id; + + OptCommandType type; + + OptCommand() + : vx(0), vy(0), vz(0), vw(1), + vc_r(1), vc_g(1), vc_b(1), + nx(0), ny(0), nz(0), + tx(0), ty(0), tw(0), + has_vertex_weight(false), has_vertex_color(false), + has_texcoord_w(false), + f_count(0), face_vertex_count(0), emitted_face_count(0), + emitted_face_verts(0), + group_name(nullptr), group_name_len(0), + object_name(nullptr), object_name_len(0), + material_name(nullptr), material_name_len(0), + mtllib_name(nullptr), mtllib_name_len(0), + source_line(0), group_name_empty(false), + degenerate_face(false), + resolved_material_id(-1), + smoothing_group_id(0), + type(OPT_CMD_EMPTY) {} + + const opt_index_t *face_indices() const { + return f_heap.empty() ? f_inline : f_heap.data(); + } +}; + +struct OptCommandCount { + size_t num_v, num_vn, num_vt, num_f, num_indices; + OptCommandCount() : num_v(0), num_vn(0), num_vt(0), num_f(0), num_indices(0) {} +}; + +// Compact face command — replaces OptCommand for 'f' lines +struct OptFaceCmd { + static const unsigned int kInlineCap = 4; + opt_index_t f_inline[kInlineCap]; // 48 bytes (covers tri + quad) + std::vector f_heap; // overflow for >4 vertex faces + uint32_t face_vertex_count; + uint32_t emitted_face_count; + uint32_t emitted_face_verts; + uint32_t f_count; + uint32_t source_line; + uint32_t v_count_before; // running v count when face was parsed (within thread) + uint32_t vn_count_before; + uint32_t vt_count_before; + bool degenerate; + + const opt_index_t *face_indices() const { + return f_heap.empty() ? f_inline : f_heap.data(); + } + + OptFaceCmd() + : face_vertex_count(0), emitted_face_count(0), emitted_face_verts(0), + f_count(0), source_line(0), v_count_before(0), vn_count_before(0), + vt_count_before(0), degenerate(false) {} +}; + +// Meta command — for g, o, usemtl, mtllib, s lines +struct OptMetaCmd { + OptCommandType type; + uint32_t source_line; + const char *str_ptr; + uint32_t str_len; + std::string str_storage; + bool group_name_empty; + int resolved_material_id; + uint32_t smoothing_group_id; + + OptMetaCmd() + : type(OPT_CMD_EMPTY), source_line(0), str_ptr(NULL), str_len(0), + group_name_empty(false), resolved_material_id(-1), + smoothing_group_id(0) {} +}; + +// Sequence entry — ordering of faces and meta commands within a thread +struct OptSeqEntry { + enum Kind : unsigned char { SEQ_FACE = 0, SEQ_META = 1 }; + Kind kind; + uint32_t index; +}; + +// Per-thread parsed data — replaces vector +struct OptThreadData { + // Vertex positions: 3 floats per vertex, contiguous + std::vector v_pos; + // Vertex weights: 1 per vertex, lazily allocated (only if any has weight) + std::vector v_weight; + // Vertex colors: 3 per vertex, lazily allocated + std::vector v_color; + + // Normal data: 3 floats per normal + std::vector vn_data; + + // Texcoord data: 2 floats per texcoord + std::vector vt_data; + // Texcoord w: 1 per texcoord, lazily allocated + std::vector vt_w; + + // Non-vertex commands + std::vector faces; + std::vector metas; + std::vector seq; + + // Counts + size_t num_v, num_vn, num_vt; + size_t num_f_indices; // total index_t entries for faces + size_t num_f_faces; // total emitted faces + + // Flags + bool saw_any_color, saw_missing_color; + bool saw_any_weight; + bool saw_any_texcoord_w; + bool saw_any_smoothing; + + // Error tracking + size_t error_line; + std::string error_message; + + OptThreadData() + : num_v(0), num_vn(0), num_vt(0), num_f_indices(0), num_f_faces(0), + saw_any_color(false), saw_missing_color(false), + saw_any_weight(false), saw_any_texcoord_w(false), + saw_any_smoothing(false), error_line(0) {} +}; + +static inline bool opt_is_valid_face_vertex(const std::vector &vertices, + const index_t &idx) { + if (idx.vertex_index < 0) return false; + const size_t vi = static_cast(idx.vertex_index); + return ((3 * vi + 2) < vertices.size()); +} + +static inline size_t opt_triangulate_face(const std::vector &vertices, + const index_t *face, + size_t face_count, + index_t *dst) { + if (face_count < 3) return 0; + if (face_count == 3) { + dst[0] = face[0]; + dst[1] = face[1]; + dst[2] = face[2]; + return 3; + } + + for (size_t i = 0; i < face_count; i++) { + if (!opt_is_valid_face_vertex(vertices, face[i])) { + return 0; + } + } + + if (face_count == 4) { + const size_t vi0 = static_cast(face[0].vertex_index); + const size_t vi1 = static_cast(face[1].vertex_index); + const size_t vi2 = static_cast(face[2].vertex_index); + const size_t vi3 = static_cast(face[3].vertex_index); + + const real_t v0x = vertices[vi0 * 3 + 0]; + const real_t v0y = vertices[vi0 * 3 + 1]; + const real_t v0z = vertices[vi0 * 3 + 2]; + const real_t v1x = vertices[vi1 * 3 + 0]; + const real_t v1y = vertices[vi1 * 3 + 1]; + const real_t v1z = vertices[vi1 * 3 + 2]; + const real_t v2x = vertices[vi2 * 3 + 0]; + const real_t v2y = vertices[vi2 * 3 + 1]; + const real_t v2z = vertices[vi2 * 3 + 2]; + const real_t v3x = vertices[vi3 * 3 + 0]; + const real_t v3y = vertices[vi3 * 3 + 1]; + const real_t v3z = vertices[vi3 * 3 + 2]; + + const real_t e02x = v2x - v0x; + const real_t e02y = v2y - v0y; + const real_t e02z = v2z - v0z; + const real_t e13x = v3x - v1x; + const real_t e13y = v3y - v1y; + const real_t e13z = v3z - v1z; + const real_t sqr02 = e02x * e02x + e02y * e02y + e02z * e02z; + const real_t sqr13 = e13x * e13x + e13y * e13y + e13z * e13z; + + if (sqr02 < sqr13) { + dst[0] = face[0]; + dst[1] = face[1]; + dst[2] = face[2]; + dst[3] = face[0]; + dst[4] = face[2]; + dst[5] = face[3]; + } else { + dst[0] = face[0]; + dst[1] = face[1]; + dst[2] = face[3]; + dst[3] = face[1]; + dst[4] = face[2]; + dst[5] = face[3]; + } + return 6; + } + + std::vector remaining(face, face + face_count); + size_t axes[2] = {1, 2}; + for (size_t k = 0; k < face_count; ++k) { + const size_t vi0 = static_cast(face[(k + 0) % face_count].vertex_index); + const size_t vi1 = static_cast(face[(k + 1) % face_count].vertex_index); + const size_t vi2 = static_cast(face[(k + 2) % face_count].vertex_index); + const real_t v0x = vertices[vi0 * 3 + 0]; + const real_t v0y = vertices[vi0 * 3 + 1]; + const real_t v0z = vertices[vi0 * 3 + 2]; + const real_t v1x = vertices[vi1 * 3 + 0]; + const real_t v1y = vertices[vi1 * 3 + 1]; + const real_t v1z = vertices[vi1 * 3 + 2]; + const real_t v2x = vertices[vi2 * 3 + 0]; + const real_t v2y = vertices[vi2 * 3 + 1]; + const real_t v2z = vertices[vi2 * 3 + 2]; + const real_t e0x = v1x - v0x; + const real_t e0y = v1y - v0y; + const real_t e0z = v1z - v0z; + const real_t e1x = v2x - v1x; + const real_t e1y = v2y - v1y; + const real_t e1z = v2z - v1z; + const real_t cx = std::fabs(e0y * e1z - e0z * e1y); + const real_t cy = std::fabs(e0z * e1x - e0x * e1z); + const real_t cz = std::fabs(e0x * e1y - e0y * e1x); + const real_t epsilon = std::numeric_limits::epsilon(); + if (cx > epsilon || cy > epsilon || cz > epsilon) { + if (!(cx > cy && cx > cz)) { + axes[0] = 0; + if (cz > cx && cz > cy) { + axes[1] = 1; + } + } + break; + } + } + + size_t out = 0; + size_t guess_vert = 0; + size_t remaining_iterations = remaining.size(); + size_t previous_remaining_vertices = remaining.size(); + while (remaining.size() > 3 && remaining_iterations > 0) { + const size_t npolys = remaining.size(); + if (guess_vert >= npolys) { + guess_vert -= npolys; + } + + if (previous_remaining_vertices != npolys) { + previous_remaining_vertices = npolys; + remaining_iterations = npolys; + } else { + remaining_iterations--; + } + + index_t ind[3]; + real_t vx[3]; + real_t vy[3]; + for (size_t k = 0; k < 3; k++) { + ind[k] = remaining[(guess_vert + k) % npolys]; + const size_t vi = static_cast(ind[k].vertex_index); + vx[k] = vertices[vi * 3 + axes[0]]; + vy[k] = vertices[vi * 3 + axes[1]]; + } + + const real_t e0x = vx[1] - vx[0]; + const real_t e0y = vy[1] - vy[0]; + const real_t e1x = vx[2] - vx[1]; + const real_t e1y = vy[2] - vy[1]; + const real_t cross_val = e0x * e1y - e0y * e1x; + const real_t area = + (vx[0] * vy[1] - vy[0] * vx[1]) * static_cast(0.5); + if (cross_val * area < static_cast(0.0)) { + guess_vert += 1; + continue; + } + + bool overlap = false; + for (size_t other_vert = 3; other_vert < npolys; ++other_vert) { + const size_t idx = (guess_vert + other_vert) % npolys; + const size_t ovi = static_cast(remaining[idx].vertex_index); + const real_t tx = vertices[ovi * 3 + axes[0]]; + const real_t ty = vertices[ovi * 3 + axes[1]]; + if (pnpoly(3, vx, vy, tx, ty)) { + overlap = true; + break; + } + } + + if (overlap) { + guess_vert += 1; + continue; + } + + dst[out++] = ind[0]; + dst[out++] = ind[1]; + dst[out++] = ind[2]; + remaining.erase(remaining.begin() + + static_cast((guess_vert + 1) % npolys)); + } + + if (remaining.size() == 3) { + dst[out++] = remaining[0]; + dst[out++] = remaining[1]; + dst[out++] = remaining[2]; + } + + return out; +} + +// Pointer+size overloads for TypedArray-based API +static inline bool opt_is_valid_face_vertex(const real_t * /*vertices*/, + size_t vertices_size, + const index_t &idx) { + if (idx.vertex_index < 0) return false; + const size_t vi = static_cast(idx.vertex_index); + return ((3 * vi + 2) < vertices_size); +} + +static inline size_t opt_triangulate_face(const real_t *vertices, + size_t vertices_size, + const index_t *face, + size_t face_count, + index_t *dst) { + if (face_count < 3) return 0; + if (face_count == 3) { + dst[0] = face[0]; dst[1] = face[1]; dst[2] = face[2]; + return 3; + } + + for (size_t i = 0; i < face_count; i++) { + if (!opt_is_valid_face_vertex(vertices, vertices_size, face[i])) return 0; + } + + if (face_count == 4) { + const size_t vi0 = static_cast(face[0].vertex_index); + const size_t vi1 = static_cast(face[1].vertex_index); + const size_t vi2 = static_cast(face[2].vertex_index); + const size_t vi3 = static_cast(face[3].vertex_index); + const real_t e02x = vertices[vi2*3+0] - vertices[vi0*3+0]; + const real_t e02y = vertices[vi2*3+1] - vertices[vi0*3+1]; + const real_t e02z = vertices[vi2*3+2] - vertices[vi0*3+2]; + const real_t e13x = vertices[vi3*3+0] - vertices[vi1*3+0]; + const real_t e13y = vertices[vi3*3+1] - vertices[vi1*3+1]; + const real_t e13z = vertices[vi3*3+2] - vertices[vi1*3+2]; + const real_t sqr02 = e02x*e02x + e02y*e02y + e02z*e02z; + const real_t sqr13 = e13x*e13x + e13y*e13y + e13z*e13z; + if (sqr02 < sqr13) { + dst[0]=face[0]; dst[1]=face[1]; dst[2]=face[2]; + dst[3]=face[0]; dst[4]=face[2]; dst[5]=face[3]; + } else { + dst[0]=face[0]; dst[1]=face[1]; dst[2]=face[3]; + dst[3]=face[1]; dst[4]=face[2]; dst[5]=face[3]; + } + return 6; + } + + // General polygon — ear clipping + std::vector remaining(face, face + face_count); + size_t axes[2] = {1, 2}; + for (size_t k = 0; k < face_count; ++k) { + const size_t vi0 = static_cast(face[(k+0)%face_count].vertex_index); + const size_t vi1 = static_cast(face[(k+1)%face_count].vertex_index); + const size_t vi2 = static_cast(face[(k+2)%face_count].vertex_index); + const real_t e0x = vertices[vi1*3+0]-vertices[vi0*3+0]; + const real_t e0y = vertices[vi1*3+1]-vertices[vi0*3+1]; + const real_t e0z = vertices[vi1*3+2]-vertices[vi0*3+2]; + const real_t e1x = vertices[vi2*3+0]-vertices[vi1*3+0]; + const real_t e1y = vertices[vi2*3+1]-vertices[vi1*3+1]; + const real_t e1z = vertices[vi2*3+2]-vertices[vi1*3+2]; + const real_t cx = std::fabs(e0y*e1z - e0z*e1y); + const real_t cy = std::fabs(e0z*e1x - e0x*e1z); + const real_t cz = std::fabs(e0x*e1y - e0y*e1x); + const real_t epsilon = std::numeric_limits::epsilon(); + if (cx > epsilon || cy > epsilon || cz > epsilon) { + if (!(cx > cy && cx > cz)) { + axes[0] = 0; + if (cz > cx && cz > cy) axes[1] = 1; + } + break; + } + } + + size_t out = 0; + size_t guess_vert = 0; + size_t remaining_iterations = remaining.size(); + size_t previous_remaining_vertices = remaining.size(); + while (remaining.size() > 3 && remaining_iterations > 0) { + const size_t npolys = remaining.size(); + if (guess_vert >= npolys) guess_vert -= npolys; + if (previous_remaining_vertices != npolys) { + previous_remaining_vertices = npolys; + remaining_iterations = npolys; + } else { + remaining_iterations--; + } + index_t ind[3]; + real_t vx[3], vy[3]; + for (size_t k = 0; k < 3; k++) { + ind[k] = remaining[(guess_vert + k) % npolys]; + const size_t vi = static_cast(ind[k].vertex_index); + vx[k] = vertices[vi*3+axes[0]]; + vy[k] = vertices[vi*3+axes[1]]; + } + const real_t e0x_ = vx[1]-vx[0], e0y_ = vy[1]-vy[0]; + const real_t e1x_ = vx[2]-vx[1], e1y_ = vy[2]-vy[1]; + const real_t cross_val = e0x_*e1y_ - e0y_*e1x_; + const real_t area = (vx[0]*vy[1] - vy[0]*vx[1]) * static_cast(0.5); + if (cross_val * area < static_cast(0.0)) { + guess_vert += 1; continue; + } + bool overlap = false; + for (size_t other_vert = 3; other_vert < npolys; ++other_vert) { + const size_t idx = (guess_vert + other_vert) % npolys; + const size_t ovi = static_cast(remaining[idx].vertex_index); + const real_t ptx = vertices[ovi*3+axes[0]]; + const real_t pty = vertices[ovi*3+axes[1]]; + if (pnpoly(3, vx, vy, ptx, pty)) { overlap = true; break; } + } + if (overlap) { guess_vert += 1; continue; } + dst[out++] = ind[0]; dst[out++] = ind[1]; dst[out++] = ind[2]; + remaining.erase(remaining.begin() + + static_cast((guess_vert + 1) % npolys)); + } + if (remaining.size() == 3) { + dst[out++] = remaining[0]; dst[out++] = remaining[1]; dst[out++] = remaining[2]; + } + return out; +} + +static bool opt_parseLine(OptCommand *command, const char *p, size_t p_len, + bool triangulate, bool *parse_error, + std::string *parse_error_message) { + // Parse directly from the original buffer without copying. + // The caller guarantees that p[p_len] is '\n' (or a sentinel), + // so character-scanning helpers that stop on '\n' are safe. + if (parse_error) *parse_error = false; + if (parse_error_message) parse_error_message->clear(); + const char *token = p; + command->type = OPT_CMD_EMPTY; + opt_skip_space(&token); + + if (TINYOBJ_OPT_IS_NEW_LINE(token[0]) || token[0] == '#') return false; + + // vertex + if (token[0] == 'v' && TINYOBJ_OPT_IS_SPACE(token[1])) { + token += 2; + real_t x = 0, y = 0, z = 0; + real_t r = real_t(1.0), g = real_t(1.0), b = real_t(1.0); + real_t w = real_t(1.0); + if (!opt_tryParseFloatToken(&x, &token) || + !opt_tryParseFloatToken(&y, &token) || + !opt_tryParseFloatToken(&z, &token)) { + if (parse_error) *parse_error = true; + if (parse_error_message) *parse_error_message = "failed to parse `v' line"; + return false; + } + const char *extra_token = token; + opt_skip_space(&extra_token); + const int extra_components = opt_count_remaining_scalars(extra_token); + if (extra_components == 1) { + // v x y z w (4 total) — weight only + const char *weight_cursor = extra_token; + if (opt_tryParseFloatToken(&w, &weight_cursor)) { + command->has_vertex_weight = true; + command->vw = w; + } + } else if (extra_components == 3) { + // v x y z r g b (6 total) — color, weight = r (legacy compat) + real_t cr = r, cg = g, cb = b; + const char *color_cursor = extra_token; + if (opt_tryParseFloatToken(&cr, &color_cursor) && + opt_tryParseFloatToken(&cg, &color_cursor) && + opt_tryParseFloatToken(&cb, &color_cursor)) { + command->has_vertex_weight = true; + command->vw = cr; + command->has_vertex_color = true; + command->vc_r = cr; + command->vc_g = cg; + command->vc_b = cb; + } + } else if (extra_components >= 4) { + // v x y z w r g b (7+ total) — weight + color + real_t vw = real_t(1.0), cr = r, cg = g, cb = b; + const char *wc_cursor = extra_token; + if (opt_tryParseFloatToken(&vw, &wc_cursor) && + opt_tryParseFloatToken(&cr, &wc_cursor) && + opt_tryParseFloatToken(&cg, &wc_cursor) && + opt_tryParseFloatToken(&cb, &wc_cursor)) { + command->has_vertex_weight = true; + command->vw = vw; + command->has_vertex_color = true; + command->vc_r = cr; + command->vc_g = cg; + command->vc_b = cb; + } + } + command->vx = x; + command->vy = y; + command->vz = z; + command->type = OPT_CMD_V; + return true; + } + + // normal + if (token[0] == 'v' && token[1] == 'n' && TINYOBJ_OPT_IS_SPACE(token[2])) { + token += 3; + real_t x = 0, y = 0, z = 0; + if (!opt_tryParseFloatToken(&x, &token) || + !opt_tryParseFloatToken(&y, &token) || + !opt_tryParseFloatToken(&z, &token)) { + if (parse_error) *parse_error = true; + if (parse_error_message) *parse_error_message = "failed to parse `vn' line"; + return false; + } + command->nx = x; + command->ny = y; + command->nz = z; + command->type = OPT_CMD_VN; + return true; + } + + // texcoord + if (token[0] == 'v' && token[1] == 't' && TINYOBJ_OPT_IS_SPACE(token[2])) { + token += 3; + real_t x = 0, y = 0, w = 0; + if (!opt_tryParseFloatToken(&x, &token)) { + if (parse_error) *parse_error = true; + if (parse_error_message) *parse_error_message = "failed to parse `vt' line"; + return false; + } + const char *y_token = token; + if (opt_tryParseFloatToken(&y, &y_token)) { + token = y_token; + } + const char *extra_token = token; + opt_skip_space(&extra_token); + if (!TINYOBJ_OPT_IS_NEW_LINE(extra_token[0]) && extra_token[0] != '#' && + opt_tryParseFloatToken(&w, &extra_token)) { + command->has_texcoord_w = true; + command->tw = w; + } + command->tx = x; + command->ty = y; + command->type = OPT_CMD_VT; + return true; + } + + // face + if (token[0] == 'f' && TINYOBJ_OPT_IS_SPACE(token[1])) { + token += 2; + opt_skip_space(&token); + + // Collect face vertices (use small stack buffer for typical case) + opt_index_t face_buf[8]; + std::vector face_overflow; + int face_count = 0; + + while (!TINYOBJ_OPT_IS_NEW_LINE(token[0]) && !opt_is_comment_start(token)) { + opt_index_t vi; + if (!opt_parseRawTriple(&token, &vi)) { + if (parse_error) *parse_error = true; + if (parse_error_message) { + *parse_error_message = "failed to parse `f' line (invalid vertex index)"; + } + return false; + } + opt_skip_space(&token); + if (face_count < 8) { + face_buf[face_count++] = vi; + } else { + if (face_count == 8) { + face_overflow.reserve(16); + for (int k = 0; k < 8; k++) face_overflow.push_back(face_buf[k]); + } + face_overflow.push_back(vi); + face_count++; + } + } + + command->type = OPT_CMD_F; + command->face_vertex_count = static_cast(face_count); + + // Preserve degenerate faces so warnings and EOF shape behavior can match + // the legacy loader, but do not reserve output slots for them. + if (face_count < 3) { + command->degenerate_face = true; + command->emitted_face_count = 0; + command->emitted_face_verts = 0; + command->f_count = 0; + return true; + } + + if (triangulate) { + command->emitted_face_count = static_cast(face_count - 2); + command->emitted_face_verts = 3; + command->f_count = command->emitted_face_count * 3; + + opt_index_t *dst = command->f_inline; + if (command->face_vertex_count > OptCommand::kInlineIndexCapacity) { + command->f_heap.resize(command->face_vertex_count); + dst = command->f_heap.data(); + } + if (face_count <= 8) { + for (int k = 0; k < face_count; k++) dst[k] = face_buf[k]; + } else { + for (size_t k = 0; k < face_overflow.size(); k++) dst[k] = face_overflow[k]; + } + } else { + command->emitted_face_count = 1; + command->emitted_face_verts = static_cast(face_count); + command->f_count = static_cast(face_count); + command->face_vertex_count = static_cast(face_count); + + opt_index_t *dst = command->f_inline; + if (command->f_count > OptCommand::kInlineIndexCapacity) { + command->f_heap.resize(command->f_count); + dst = command->f_heap.data(); + } + + if (face_count <= 8) { + for (int k = 0; k < face_count; k++) dst[k] = face_buf[k]; + } else { + for (size_t k = 0; k < face_overflow.size(); k++) { + dst[k] = face_overflow[k]; + } + } + } + return true; + } + + // usemtl + if (std::strncmp(token, "usemtl", 6) == 0 && + TINYOBJ_OPT_IS_SPACE(token[6])) { + token += 7; + opt_skip_space(&token); + command->material_name = token; + command->material_name_len = static_cast( + opt_length_until_token_or_comment(token, + p_len - static_cast(token - p))); + command->type = OPT_CMD_USEMTL; + return true; + } + + // mtllib + if (std::strncmp(token, "mtllib", 6) == 0 && + TINYOBJ_OPT_IS_SPACE(token[6])) { + token += 7; + opt_skip_space(&token); + command->mtllib_name = token; + command->mtllib_name_len = static_cast( + opt_length_until_newline(token, + p_len - static_cast(token - p))); + command->type = OPT_CMD_MTLLIB; + return true; + } + + // group + if (token[0] == 'g' && + (TINYOBJ_OPT_IS_SPACE(token[1]) || TINYOBJ_OPT_IS_NEW_LINE(token[1]) || + token[1] == '\0')) { + if (TINYOBJ_OPT_IS_SPACE(token[1])) { + token += 2; + } else { + token += 1; + } + command->group_name_storage = opt_parseGroupName( + token, p_len - static_cast(token - p)); + command->group_name = nullptr; + command->group_name_len = + static_cast(command->group_name_storage.size()); + command->group_name_empty = command->group_name_storage.empty(); + command->type = OPT_CMD_G; + return true; + } + + // object + if (token[0] == 'o' && TINYOBJ_OPT_IS_SPACE(token[1])) { + token += 2; + command->object_name = token; + command->object_name_len = static_cast( + p_len - static_cast(token - p)); + command->type = OPT_CMD_O; + return true; + } + + // smoothing group + if (token[0] == 's' && TINYOBJ_OPT_IS_SPACE(token[1])) { + token += 2; + opt_skip_space(&token); + if (TINYOBJ_OPT_IS_NEW_LINE(token[0])) { + command->smoothing_group_id = 0; + } else if (token[0] == 'o' && token[1] == 'f' && token[2] == 'f' && + (TINYOBJ_OPT_IS_NEW_LINE(token[3]) || token[3] == '\0' || + token[3] == ' ' || token[3] == '\t')) { + command->smoothing_group_id = 0; + } else { + const int sm = opt_my_atoi(token); + command->smoothing_group_id = (sm > 0) ? static_cast(sm) : 0; + } + command->type = OPT_CMD_S; + return true; + } + + return false; +} + +// ---- SIMD newline scanning ---- + +#ifdef TINYOBJLOADER_USE_SIMD + +// Include for _BitScanForward on MSVC. Placed here (not at the top) +// because it's only needed when TINYOBJLOADER_USE_SIMD is enabled. +#if defined(_MSC_VER) +#include +#endif + +// Portable count-trailing-zeros for SIMD bitmask extraction +static inline unsigned int tinyobj_ctz(unsigned int x) { +#if defined(_MSC_VER) + unsigned long idx; + _BitScanForward(&idx, x); + return static_cast(idx); +#else + return static_cast(__builtin_ctz(x)); +#endif +} + +#if defined(TINYOBJLOADER_SIMD_AVX2) + +/// AVX2-accelerated line-ending scanning — finds '\n' and '\r' positions. +static void simd_find_newlines(const char *buf, size_t len, + std::vector &positions) { + const __m256i nl = _mm256_set1_epi8('\n'); + const __m256i cr = _mm256_set1_epi8('\r'); + size_t i = 0; + for (; i + 32 <= len; i += 32) { + __m256i chunk = + _mm256_loadu_si256(reinterpret_cast(buf + i)); + __m256i cmp_nl = _mm256_cmpeq_epi8(chunk, nl); + __m256i cmp_cr = _mm256_cmpeq_epi8(chunk, cr); + __m256i combined = _mm256_or_si256(cmp_nl, cmp_cr); + unsigned int mask = static_cast(_mm256_movemask_epi8(combined)); + while (mask) { + unsigned int bit = tinyobj_ctz(mask); + positions.push_back(i + bit); + mask &= mask - 1; + } + } + // Scalar tail + for (; i < len; i++) { + if (buf[i] == '\n' || buf[i] == '\r') positions.push_back(i); + } +} + +#elif defined(TINYOBJLOADER_SIMD_SSE2) + +/// SSE2-accelerated line-ending scanning — finds '\n' and '\r' positions. +static void simd_find_newlines(const char *buf, size_t len, + std::vector &positions) { + const __m128i nl = _mm_set1_epi8('\n'); + const __m128i cr = _mm_set1_epi8('\r'); + size_t i = 0; + for (; i + 16 <= len; i += 16) { + __m128i chunk = + _mm_loadu_si128(reinterpret_cast(buf + i)); + __m128i cmp_nl = _mm_cmpeq_epi8(chunk, nl); + __m128i cmp_cr = _mm_cmpeq_epi8(chunk, cr); + __m128i combined = _mm_or_si128(cmp_nl, cmp_cr); + int mask = _mm_movemask_epi8(combined); + while (mask) { + int bit = static_cast(tinyobj_ctz(static_cast(mask))); + positions.push_back(i + static_cast(bit)); + mask &= mask - 1; + } + } + // Scalar tail + for (; i < len; i++) { + if (buf[i] == '\n' || buf[i] == '\r') positions.push_back(i); + } +} + +#elif defined(TINYOBJLOADER_SIMD_NEON) + +/// NEON-accelerated line-ending scanning — finds '\n' and '\r' positions. +static void simd_find_newlines(const char *buf, size_t len, + std::vector &positions) { + const uint8x16_t nl = vdupq_n_u8('\n'); + const uint8x16_t cr = vdupq_n_u8('\r'); + size_t i = 0; + for (; i + 16 <= len; i += 16) { + uint8x16_t chunk = vld1q_u8(reinterpret_cast(buf + i)); + uint8x16_t cmp_nl = vceqq_u8(chunk, nl); + uint8x16_t cmp_cr = vceqq_u8(chunk, cr); + uint8x16_t combined = vorrq_u8(cmp_nl, cmp_cr); + for (int j = 0; j < 16; j++) { + if (vgetq_lane_u8(combined, 0) != 0) { + positions.push_back(i + static_cast(j)); + } + combined = vextq_u8(combined, combined, 1); + } + } + for (; i < len; i++) { + if (buf[i] == '\n' || buf[i] == '\r') positions.push_back(i); + } +} + +#endif // SIMD variant + +/// Build LineInfo array from SIMD-detected line-ending positions. +/// The positions array may contain both '\r' and '\n' hits; +/// consecutive \r\n pairs are collapsed into a single line boundary. +static void simd_build_line_infos(const char *buf, size_t len, + const std::vector &nl_positions, + std::vector &out) { + out.reserve(nl_positions.size() + 1); + size_t prev = 0; + for (size_t k = 0; k < nl_positions.size(); k++) { + size_t pos = nl_positions[k]; + // Skip the '\n' in a '\r\n' pair (the '\r' already created a boundary) + if (buf[pos] == '\n' && pos > 0 && buf[pos - 1] == '\r') { + prev = pos + 1; + continue; + } + size_t line_len = pos - prev; + if (line_len > 0) { + LineInfo info; + info.pos = prev; + info.len = line_len; + out.push_back(info); + } + prev = pos + 1; + } + // Handle last line without trailing newline + if (prev < len) { + size_t line_len = len - prev; + if (line_len > 0) { + LineInfo info; + info.pos = prev; + info.len = line_len; + out.push_back(info); + } + } +} + +#endif // TINYOBJLOADER_USE_SIMD + +/// Scalar fallback newline scanning — handles \n, \r\n, and bare \r +static void scalar_find_line_infos(const char *buf, size_t start, size_t end, + std::vector &out) { + size_t prev = start; + for (size_t i = start; i < end; i++) { + if (buf[i] == '\n' || buf[i] == '\r') { + size_t line_len = i - prev; + // Skip \r in \r\n pair + if (buf[i] == '\r' && (i + 1 < end) && buf[i + 1] == '\n') { + if (line_len > 0) { + LineInfo info; + info.pos = prev; + info.len = line_len; + out.push_back(info); + } + i++; // skip the \n in \r\n + } else { + // bare \n or bare \r + if (line_len > 0) { + LineInfo info; + info.pos = prev; + info.len = line_len; + out.push_back(info); + } + } + prev = i + 1; + } + } + if (prev < end) { + size_t line_len = end - prev; + if (line_len > 0) { + LineInfo info; + info.pos = prev; + info.len = line_len; + out.push_back(info); + } + } +} + +template +static void opt_assign_vector(DstVec *dst, const SrcVec &src) { + if (!dst) return; + dst->assign(src.begin(), src.end()); +} + +static void ConvertLegacyShapeToBasic(const shape_t &src, basic_shape_t<> *dst) { + if (!dst) return; + dst->name = src.name; + opt_assign_vector(&dst->mesh.indices, src.mesh.indices); + opt_assign_vector(&dst->mesh.num_face_vertices, src.mesh.num_face_vertices); + opt_assign_vector(&dst->mesh.material_ids, src.mesh.material_ids); + opt_assign_vector(&dst->mesh.smoothing_group_ids, + src.mesh.smoothing_group_ids); + dst->mesh.tags = src.mesh.tags; + opt_assign_vector(&dst->lines.indices, src.lines.indices); + opt_assign_vector(&dst->lines.num_line_vertices, src.lines.num_line_vertices); + opt_assign_vector(&dst->points.indices, src.points.indices); +} + +static void ConvertLegacyResultToBasic( + const attrib_t &src_attrib, const std::vector &src_shapes, + const std::vector &src_materials, basic_attrib_t<> *dst_attrib, + std::vector > *dst_shapes, + std::vector *dst_materials) { + if (dst_attrib) { + opt_assign_vector(&dst_attrib->vertices, src_attrib.vertices); + opt_assign_vector(&dst_attrib->vertex_weights, src_attrib.vertex_weights); + opt_assign_vector(&dst_attrib->normals, src_attrib.normals); + opt_assign_vector(&dst_attrib->texcoords, src_attrib.texcoords); + opt_assign_vector(&dst_attrib->texcoord_ws, src_attrib.texcoord_ws); + opt_assign_vector(&dst_attrib->colors, src_attrib.colors); + dst_attrib->skin_weights = src_attrib.skin_weights; + dst_attrib->indices.clear(); + dst_attrib->face_num_verts.clear(); + dst_attrib->material_ids.clear(); + for (size_t si = 0; si < src_shapes.size(); si++) { + const mesh_t &mesh = src_shapes[si].mesh; + dst_attrib->indices.insert(dst_attrib->indices.end(), mesh.indices.begin(), + mesh.indices.end()); + dst_attrib->face_num_verts.insert(dst_attrib->face_num_verts.end(), + mesh.num_face_vertices.begin(), + mesh.num_face_vertices.end()); + dst_attrib->material_ids.insert(dst_attrib->material_ids.end(), + mesh.material_ids.begin(), + mesh.material_ids.end()); + } + } + + if (dst_shapes) { + dst_shapes->clear(); + dst_shapes->resize(src_shapes.size()); + for (size_t i = 0; i < src_shapes.size(); i++) { + ConvertLegacyShapeToBasic(src_shapes[i], &(*dst_shapes)[i]); + } + } + + if (dst_materials) { + (*dst_materials) = src_materials; + } +} + +static bool opt_requires_legacy_fallback(const char *p, size_t len) { + if (!p || len == 0) return false; + while (len > 0 && (*p == ' ' || *p == '\t')) { + p++; + len--; + } + if (len == 0 || *p == '#' || *p == '\r' || *p == '\n') return false; + + if (len >= 3 && p[0] == 'v' && p[1] == 'w' && + (p[2] == ' ' || p[2] == '\t')) { + return true; + } + if (len >= 2 && + ((p[0] == 'l' && (p[1] == ' ' || p[1] == '\t')) || + (p[0] == 'p' && (p[1] == ' ' || p[1] == '\t')) || + (p[0] == 't' && (p[1] == ' ' || p[1] == '\t')))) { + return true; + } + return false; +} + +/// Check if the first non-whitespace token on a line (starting at p, length +/// rem) requires the legacy parser. +static inline bool opt_line_start_is_legacy(const char *p, size_t rem) { + while (rem > 0 && (*p == ' ' || *p == '\t')) { p++; rem--; } + if (rem >= 3 && p[0] == 'v' && p[1] == 'w' && + (p[2] == ' ' || p[2] == '\t')) + return true; + if (rem >= 2 && + ((p[0] == 'l' || p[0] == 'p' || p[0] == 't') && + (p[1] == ' ' || p[1] == '\t'))) + return true; + return false; +} + +/// Fast whole-buffer scan for legacy-only tokens (vw, l, p, t at line start). +/// Uses memchr for SIMD-accelerated newline finding. Handles both \n and +/// bare \r (old Mac) line endings. +static bool opt_buffer_requires_legacy_fallback(const char *buf, size_t len) { + if (!buf || len == 0) return false; + // Check at start of buffer + if (opt_line_start_is_legacy(buf, len)) return true; + // Scan for line breaks (\n first, then bare \r) + for (int pass = 0; pass < 2; pass++) { + const char needle = (pass == 0) ? '\n' : '\r'; + const char *p = buf; + size_t remaining = len; + for (;;) { + const char *nl = static_cast( + std::memchr(p, needle, remaining)); + if (!nl) break; + size_t offset = static_cast(nl - buf) + 1; + if (offset >= len) break; + // Skip \n in \r\n pair on the \r pass to avoid double-checking + if (needle == '\r' && offset < len && buf[offset] == '\n') { + p = buf + offset + 1; + remaining = len - offset - 1; + continue; + } + if (opt_line_start_is_legacy(buf + offset, len - offset)) return true; + p = buf + offset; + remaining = len - offset; + } + } + return false; +} + +/// +/// Trie-based string → float cache for repeated OBJ coordinate values. +/// Caches short float tokens (up to kMaxKeyLen chars) in a compact trie. +/// Alphabet: '0'-'9', '+', '-', '.', 'e', 'E' (15 chars). +/// Thread-local — each thread gets its own cache instance. +/// +class OptFloatCache { + public: + static const int kFp32MaxKeyLen = + std::numeric_limits::digits10 + 2; // 8 + static const int kRealMaxKeyLen = + std::numeric_limits::digits10 + 2; + static const int kAlphabetSize = 15; + + explicit OptFloatCache(int max_nodes = 1024, bool fp32_keys = true) + : max_nodes_(max_nodes < 2 ? 2 : (max_nodes > 65535 ? 65535 : max_nodes)), + max_key_len_(fp32_keys ? kFp32MaxKeyLen : kRealMaxKeyLen) { + nodes_.reserve(static_cast(max_nodes_)); + nodes_.resize(1); + std::memset(&nodes_[0], 0, sizeof(Node)); + } + + static inline int char_index(char c) { + if (c >= '0' && c <= '9') return c - '0'; + switch (c) { + case '+': return 10; + case '-': return 11; + case '.': return 12; + case 'e': return 13; + case 'E': return 14; + } + return -1; + } + + /// Try cache lookup. Returns true + sets *out on hit. + inline bool lookup(const char *key, int key_len, real_t *out) const { + if (key_len <= 0 || key_len > max_key_len_) return false; + int node = 0; + for (int i = 0; i < key_len; i++) { + int ci = char_index(key[i]); + if (ci < 0) return false; + int child = nodes_[static_cast(node)] + .children[static_cast(ci)]; + if (child == 0) return false; + node = child; + } + const Node &n = nodes_[static_cast(node)]; + if (!n.has_value) return false; + *out = n.value; + return true; + } + + /// Insert parsed value. Returns false if full or key too long. + inline bool insert(const char *key, int key_len, real_t value) { + if (key_len <= 0 || key_len > max_key_len_) return false; + int node = 0; + for (int i = 0; i < key_len; i++) { + int ci = char_index(key[i]); + if (ci < 0) return false; + uint16_t child = nodes_[static_cast(node)] + .children[static_cast(ci)]; + if (child == 0) { + if (static_cast(nodes_.size()) >= max_nodes_) return false; + child = static_cast(nodes_.size()); + // Write back BEFORE push_back (push_back may realloc) + nodes_[static_cast(node)] + .children[static_cast(ci)] = child; + Node new_node; + std::memset(&new_node, 0, sizeof(Node)); + nodes_.push_back(new_node); + } + node = static_cast(child); + } + Node &n = nodes_[static_cast(node)]; + n.value = value; + n.has_value = true; + return true; + } + + int max_key_len() const { return max_key_len_; } + + private: + struct Node { + uint16_t children[kAlphabetSize]; // 30 bytes + real_t value; // 4 or 8 bytes + bool has_value; // 1 byte + }; + std::vector nodes_; + int max_nodes_; + int max_key_len_; +}; + +// Fast inline float parse: skip whitespace, call fast_float with line_end, +// advance cursor. No token-end pre-scan. Returns false if no float found. +// When cache is non-null, checks/populates the trie cache for short tokens. +// Handles nan/inf keywords with OBJ-compatible replacement values (matching +// opt_tryParseDouble behavior). +#ifndef TINYOBJLOADER_DISABLE_FAST_FLOAT +static inline bool opt_fast_parse_float(real_t *out, const char **cursor, + const char *line_end, + OptFloatCache *cache = nullptr) { + const char *p = *cursor; + while (p < line_end && (*p == ' ' || *p == '\t')) p++; + if (p >= line_end || *p == '\n' || *p == '\r' || *p == '#') return false; + + // Check for nan/inf keywords before calling fast_float (which doesn't + // handle them). Map to OBJ-compatible values: nan→0, +inf→max, -inf→lowest. + { + const char *q = p; + if (q < line_end && (*q == '+' || *q == '-')) ++q; + if (q < line_end) { + char fc = *q; + if (fc >= 'A' && fc <= 'Z') fc += 32; + if (fc == 'n' || fc == 'i') { + double special_val; + const char *end_ptr; + if (detail_fp::tryParseNanInf(p, line_end, &special_val, &end_ptr)) { + *out = static_cast(special_val); + *cursor = end_ptr; + return true; + } + } + } + } + + // --- Cache fast path: scan up to max_key_len chars for trie lookup --- + if (cache) { + const char *tok_start = p; + const char *kp = p; + int klen = 0; + int mkl = cache->max_key_len(); + while (klen < mkl && kp < line_end) { + char c = *kp; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '#' || + c == '\0') + break; + if (OptFloatCache::char_index(c) < 0) break; + klen++; + kp++; + } + // Token is cacheable if it ended at a delimiter (not mid-token) + bool at_delim = (kp >= line_end || *kp == ' ' || *kp == '\t' || + *kp == '\n' || *kp == '\r' || *kp == '#' || *kp == '\0'); + if (klen > 0 && at_delim) { + real_t cached; + if (cache->lookup(tok_start, klen, &cached)) { + *out = cached; + *cursor = kp; + return true; + } + // Cache miss — parse normally, then insert + real_t tmp; + auto r = fast_float::from_chars(tok_start, line_end, tmp, + fast_float::chars_format::general | + fast_float::chars_format::allow_leading_plus); + if (r.ec != tinyobj_ff::ff_errc::ok) return false; + *out = tmp; + *cursor = r.ptr; + cache->insert(tok_start, klen, tmp); + return true; + } + // Token too long or has non-float chars — fall through to uncached parse + } + + // --- Uncached parse --- + real_t tmp; + auto r = fast_float::from_chars(p, line_end, tmp, + fast_float::chars_format::general | + fast_float::chars_format::allow_leading_plus); + if (r.ec != tinyobj_ff::ff_errc::ok) return false; + *out = tmp; + *cursor = r.ptr; + return true; +} +#endif + +// Parse a single line and store into compact per-type thread data. +// Fast path: v/vn/vt lines are parsed directly using fast_float with line_end, +// bypassing OptCommand entirely. Face/meta lines fall back to opt_parseLine. +static void opt_parseLineToThreadData( + OptThreadData &td, const char *line_ptr, size_t line_len, + bool triangulate, size_t line_number, + OptFloatCache *cache = nullptr) { + const char *token = line_ptr; + while (*token == ' ' || *token == '\t') token++; + if (*token == '\n' || *token == '\r' || *token == '#' || *token == '\0') + return; + +#ifndef TINYOBJLOADER_DISABLE_FAST_FLOAT + const char *line_end = line_ptr + line_len; + + // ---- Fast path: vertex position (v x y z [w] [r g b]) ---- + if (token[0] == 'v' && (token[1] == ' ' || token[1] == '\t')) { + const char *cur = token + 2; + real_t x, y, z; + if (!opt_fast_parse_float(&x, &cur, line_end, cache) || + !opt_fast_parse_float(&y, &cur, line_end, cache) || + !opt_fast_parse_float(&z, &cur, line_end, cache)) { + td.error_line = line_number; + td.error_message = "failed to parse `v' line"; + return; + } + // Write xyz (batch: resize + direct write) + size_t off = td.v_pos.size(); + td.v_pos.resize(off + 3); + td.v_pos[off] = x; td.v_pos[off+1] = y; td.v_pos[off+2] = z; + + // Check for extra components (weight / color) + real_t extra[4]; + int nextra = 0; + while (nextra < 4 && opt_fast_parse_float(&extra[nextra], &cur, line_end, cache)) + nextra++; + + if (nextra == 1) { + // v x y z w (4 total) — weight only, no color + if (!td.saw_any_weight) { + td.saw_any_weight = true; + td.v_weight.resize(td.num_v, real_t(1.0)); + } + td.v_weight.push_back(extra[0]); + td.saw_missing_color = true; + if (td.saw_any_color) { + size_t coff = td.v_color.size(); + td.v_color.resize(coff + 3); + td.v_color[coff] = real_t(1.0); + td.v_color[coff+1] = real_t(1.0); + td.v_color[coff+2] = real_t(1.0); + } + } else if (nextra == 3) { + // v x y z r g b (6 total) — color, weight = r (legacy compat) + if (!td.saw_any_weight) { + td.saw_any_weight = true; + td.v_weight.resize(td.num_v, real_t(1.0)); + } + td.v_weight.push_back(extra[0]); + if (!td.saw_any_color) { + td.saw_any_color = true; + td.v_color.resize(td.num_v * 3, real_t(1.0)); + } + size_t coff = td.v_color.size(); + td.v_color.resize(coff + 3); + td.v_color[coff] = extra[0]; + td.v_color[coff+1] = extra[1]; + td.v_color[coff+2] = extra[2]; + } else if (nextra >= 4) { + // v x y z w r g b (7+ total) — weight + color + if (!td.saw_any_weight) { + td.saw_any_weight = true; + td.v_weight.resize(td.num_v, real_t(1.0)); + } + td.v_weight.push_back(extra[0]); + if (!td.saw_any_color) { + td.saw_any_color = true; + td.v_color.resize(td.num_v * 3, real_t(1.0)); + } + size_t coff = td.v_color.size(); + td.v_color.resize(coff + 3); + td.v_color[coff] = extra[1]; + td.v_color[coff+1] = extra[2]; + td.v_color[coff+2] = extra[3]; + } else { + // nextra == 0 or 2 — no color + if (td.saw_any_weight) td.v_weight.push_back(real_t(1.0)); + td.saw_missing_color = true; + if (td.saw_any_color) { + size_t coff = td.v_color.size(); + td.v_color.resize(coff + 3); + td.v_color[coff] = real_t(1.0); + td.v_color[coff+1] = real_t(1.0); + td.v_color[coff+2] = real_t(1.0); + } + } + td.num_v++; + return; + } + + // ---- Fast path: normal (vn x y z) ---- + if (token[0] == 'v' && token[1] == 'n' && + (token[2] == ' ' || token[2] == '\t')) { + const char *cur = token + 3; + real_t x, y, z; + if (!opt_fast_parse_float(&x, &cur, line_end, cache) || + !opt_fast_parse_float(&y, &cur, line_end, cache) || + !opt_fast_parse_float(&z, &cur, line_end, cache)) { + td.error_line = line_number; + td.error_message = "failed to parse `vn' line"; + return; + } + size_t off = td.vn_data.size(); + td.vn_data.resize(off + 3); + td.vn_data[off] = x; td.vn_data[off+1] = y; td.vn_data[off+2] = z; + td.num_vn++; + return; + } + + // ---- Fast path: texcoord (vt u [v [w]]) ---- + if (token[0] == 'v' && token[1] == 't' && + (token[2] == ' ' || token[2] == '\t')) { + const char *cur = token + 3; + real_t u = 0, v = 0; + if (!opt_fast_parse_float(&u, &cur, line_end, cache)) { + td.error_line = line_number; + td.error_message = "failed to parse `vt' line"; + return; + } + opt_fast_parse_float(&v, &cur, line_end, cache); // v is optional + real_t w = 0; + bool has_w = opt_fast_parse_float(&w, &cur, line_end, cache); + + size_t off = td.vt_data.size(); + td.vt_data.resize(off + 2); + td.vt_data[off] = u; td.vt_data[off+1] = v; + if (has_w) { + if (!td.saw_any_texcoord_w) { + td.saw_any_texcoord_w = true; + td.vt_w.resize(td.num_vt, real_t(0.0)); + } + td.vt_w.push_back(w); + } else if (td.saw_any_texcoord_w) { + td.vt_w.push_back(real_t(0.0)); + } + td.num_vt++; + return; + } +#endif // TINYOBJLOADER_DISABLE_FAST_FLOAT + + // ---- Slow path: face / meta commands via opt_parseLine ---- + OptCommand cmd; + bool parse_error = false; + bool ok = opt_parseLine(&cmd, line_ptr, line_len, triangulate, + &parse_error, nullptr); + if (parse_error) { + td.error_line = line_number; + std::string msg; + opt_parseLine(&cmd, line_ptr, line_len, triangulate, nullptr, &msg); + td.error_message = msg; + return; + } + if (!ok) return; + + switch (cmd.type) { +#ifdef TINYOBJLOADER_DISABLE_FAST_FLOAT + // When fast_float is disabled, v/vn/vt fall through to here + case OPT_CMD_V: { + size_t off = td.v_pos.size(); + td.v_pos.resize(off + 3); + td.v_pos[off] = cmd.vx; td.v_pos[off+1] = cmd.vy; td.v_pos[off+2] = cmd.vz; + if (cmd.has_vertex_weight) { + if (!td.saw_any_weight) { + td.saw_any_weight = true; + td.v_weight.resize(td.num_v, real_t(1.0)); + } + td.v_weight.push_back(cmd.vw); + } else if (td.saw_any_weight) { + td.v_weight.push_back(real_t(1.0)); + } + if (cmd.has_vertex_color) { + if (!td.saw_any_color) { + td.saw_any_color = true; + td.v_color.resize(td.num_v * 3, real_t(1.0)); + } + td.v_color.push_back(cmd.vc_r); + td.v_color.push_back(cmd.vc_g); + td.v_color.push_back(cmd.vc_b); + } else { + td.saw_missing_color = true; + if (td.saw_any_color) { + td.v_color.push_back(real_t(1.0)); + td.v_color.push_back(real_t(1.0)); + td.v_color.push_back(real_t(1.0)); + } + } + td.num_v++; + break; + } + case OPT_CMD_VN: { + size_t off = td.vn_data.size(); + td.vn_data.resize(off + 3); + td.vn_data[off] = cmd.nx; td.vn_data[off+1] = cmd.ny; td.vn_data[off+2] = cmd.nz; + td.num_vn++; + break; + } + case OPT_CMD_VT: { + size_t off = td.vt_data.size(); + td.vt_data.resize(off + 2); + td.vt_data[off] = cmd.tx; td.vt_data[off+1] = cmd.ty; + if (cmd.has_texcoord_w) { + if (!td.saw_any_texcoord_w) { + td.saw_any_texcoord_w = true; + td.vt_w.resize(td.num_vt, real_t(0.0)); + } + td.vt_w.push_back(cmd.tw); + } else if (td.saw_any_texcoord_w) { + td.vt_w.push_back(real_t(0.0)); + } + td.num_vt++; + break; + } +#endif // TINYOBJLOADER_DISABLE_FAST_FLOAT + case OPT_CMD_F: { + OptFaceCmd fc; + fc.face_vertex_count = cmd.face_vertex_count; + fc.emitted_face_count = cmd.emitted_face_count; + fc.emitted_face_verts = cmd.emitted_face_verts; + fc.f_count = cmd.f_count; + fc.degenerate = cmd.degenerate_face; + fc.source_line = static_cast(line_number); + fc.v_count_before = static_cast(td.num_v); + fc.vn_count_before = static_cast(td.num_vn); + fc.vt_count_before = static_cast(td.num_vt); + if (!cmd.f_heap.empty()) { + fc.f_heap = std::move(cmd.f_heap); + } else if (cmd.face_vertex_count <= OptFaceCmd::kInlineCap) { + for (uint32_t k = 0; k < cmd.face_vertex_count; k++) + fc.f_inline[k] = cmd.f_inline[k]; + } else { + fc.f_heap.assign(cmd.f_inline, cmd.f_inline + cmd.face_vertex_count); + } + td.num_f_indices += fc.f_count; + td.num_f_faces += fc.emitted_face_count; + OptSeqEntry se; + se.kind = OptSeqEntry::SEQ_FACE; + se.index = static_cast(td.faces.size()); + td.seq.push_back(se); + td.faces.push_back(std::move(fc)); + break; + } + case OPT_CMD_G: + case OPT_CMD_O: + case OPT_CMD_USEMTL: + case OPT_CMD_MTLLIB: + case OPT_CMD_S: { + OptMetaCmd mc; + mc.type = cmd.type; + mc.source_line = static_cast(line_number); + if (cmd.type == OPT_CMD_G) { + mc.str_storage = cmd.group_name_storage; + mc.str_ptr = cmd.group_name; + mc.str_len = cmd.group_name_len; + mc.group_name_empty = cmd.group_name_empty; + } else if (cmd.type == OPT_CMD_O) { + mc.str_ptr = cmd.object_name; + mc.str_len = cmd.object_name_len; + } else if (cmd.type == OPT_CMD_USEMTL) { + mc.str_ptr = cmd.material_name; + mc.str_len = cmd.material_name_len; + } else if (cmd.type == OPT_CMD_MTLLIB) { + mc.str_ptr = cmd.mtllib_name; + mc.str_len = cmd.mtllib_name_len; + } else if (cmd.type == OPT_CMD_S) { + mc.smoothing_group_id = cmd.smoothing_group_id; + td.saw_any_smoothing = true; + } + OptSeqEntry se; + se.kind = OptSeqEntry::SEQ_META; + se.index = static_cast(td.metas.size()); + td.seq.push_back(se); + td.metas.push_back(std::move(mc)); + break; + } + default: + break; + } +} + +} // namespace opt_internal + +// Internal implementation with optional basedir for material path resolution +static bool LoadObjOpt_internal(basic_attrib_t<> *attrib, + std::vector> *shapes, + std::vector *materials, + std::string *warn, std::string *err, + const char *buf, size_t buf_len, + const std::string &mtl_basedir, + const std::string &source_name, + bool enable_mtllib_loading, + const OptLoadConfig &config) { + using namespace opt_internal; + + if (!attrib || !shapes) { + if (err) *err = "attrib and shapes must not be null."; + return false; + } + + attrib->vertices.clear(); + attrib->vertex_weights.clear(); + attrib->normals.clear(); + attrib->texcoords.clear(); + attrib->texcoord_ws.clear(); + attrib->colors.clear(); + attrib->skin_weights.clear(); + attrib->indices.clear(); + attrib->face_num_verts.clear(); + attrib->material_ids.clear(); + shapes->clear(); + if (materials) materials->clear(); + + if (buf_len < 1) return true; // empty buffer is not an error + + if (!buf) { + if (err) *err = "buf must not be null when buf_len > 0."; + return false; + } + + // Ensure buffer ends with a newline for safe tokenization (avoids + // one-byte over-read on the last line when parsing directly from the + // buffer without per-line copies). + const char *work_buf = buf; + size_t work_len = buf_len; + std::vector buf_with_sentinel; + if (buf[buf_len - 1] != '\n') { + buf_with_sentinel.assign(buf, buf + buf_len); + buf_with_sentinel.push_back('\n'); + work_buf = buf_with_sentinel.data(); + work_len = buf_with_sentinel.size(); + } + + // Determine thread count + int num_threads = 1; +#ifdef TINYOBJLOADER_USE_MULTITHREADING + if (config.num_threads < 0) { + num_threads = static_cast(std::thread::hardware_concurrency()); + if (num_threads < 1) num_threads = 1; + } else if (config.num_threads > 1) { + num_threads = config.num_threads; + } + if (num_threads > kOptMaxThreads) num_threads = kOptMaxThreads; +#else +#endif + + // ---- Phase 1: find line boundaries ---- + std::vector all_line_infos; + +#if defined(TINYOBJLOADER_USE_SIMD) && \ + (defined(TINYOBJLOADER_SIMD_SSE2) || defined(TINYOBJLOADER_SIMD_AVX2) || \ + defined(TINYOBJLOADER_SIMD_NEON)) + { + std::vector nl_positions; + nl_positions.reserve(work_len / 64); + simd_find_newlines(work_buf, work_len, nl_positions); + simd_build_line_infos(work_buf, work_len, nl_positions, all_line_infos); + } +#else + { + all_line_infos.reserve(work_len / 64); + scalar_find_line_infos(work_buf, 0, work_len, all_line_infos); + } +#endif + + const size_t total_lines = all_line_infos.size(); + if (total_lines == 0) return true; + + // Fast buffer-level check for legacy-only tokens (replaces per-line scan) + if (opt_buffer_requires_legacy_fallback(work_buf, work_len)) { + std::string input(buf, buf + buf_len); + std::istringstream iss(input); + attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + MaterialFileReader mat_reader(mtl_basedir); + MaterialReader *reader = + enable_mtllib_loading ? static_cast(&mat_reader) : NULL; + const bool ok = LoadObj(&legacy_attrib, &legacy_shapes, &legacy_materials, + warn, err, &iss, reader, config.triangulate, false); + if (!ok) { + return false; + } + ConvertLegacyResultToBasic(legacy_attrib, legacy_shapes, legacy_materials, + attrib, shapes, materials); + return true; + } + + // ---- Phase 2: parse lines (compact storage) ---- + // Single-threaded or multi-threaded depending on compile option. + + const size_t num_t = static_cast(num_threads); + std::vector thread_data(num_t); + +#ifdef TINYOBJLOADER_USE_MULTITHREADING + // Multi-threaded path + { + size_t lines_per_thread = total_lines / num_t; + std::vector workers; + workers.reserve(num_t); + + for (int t = 0; t < num_threads; t++) { + size_t start = static_cast(t) * lines_per_thread; + size_t end = (t == num_threads - 1) + ? total_lines + : (static_cast(t) + 1) * lines_per_thread; + + workers.emplace_back([&, t, start, end]() { + OptThreadData &td = thread_data[static_cast(t)]; + size_t est_lines = end - start; + td.v_pos.reserve(est_lines * 2); + td.faces.reserve(est_lines / 3); + td.seq.reserve(est_lines / 3); + OptFloatCache *tc = nullptr; +#ifndef TINYOBJLOADER_DISABLE_FAST_FLOAT + OptFloatCache thread_cache(config.float_cache_max_nodes, + config.fp32_cache); + if (config.float_cache) tc = &thread_cache; +#endif + for (size_t i = start; i < end; i++) { + opt_parseLineToThreadData(td, &work_buf[all_line_infos[i].pos], + all_line_infos[i].len, config.triangulate, + i + 1, tc); + if (td.error_line != 0) break; + } + }); + } + for (auto &w : workers) w.join(); + } + +#else + // Single-threaded path + { + OptThreadData &td = thread_data[0]; + td.v_pos.reserve(total_lines * 2); + td.faces.reserve(total_lines / 3); + td.seq.reserve(total_lines / 3); + OptFloatCache *tc = nullptr; +#ifndef TINYOBJLOADER_DISABLE_FAST_FLOAT + OptFloatCache thread_cache(config.float_cache_max_nodes, + config.fp32_cache); + if (config.float_cache) tc = &thread_cache; +#endif + for (size_t i = 0; i < total_lines; i++) { + opt_parseLineToThreadData(td, &work_buf[all_line_infos[i].pos], + all_line_infos[i].len, config.triangulate, + i + 1, tc); + if (td.error_line != 0) break; + } + } +#endif + + size_t first_error_line = 0; + std::string first_error_message; + for (size_t t = 0; t < num_t; t++) { + if (thread_data[t].error_line == 0) continue; + if (first_error_line == 0 || thread_data[t].error_line < first_error_line) { + first_error_line = thread_data[t].error_line; + first_error_message = thread_data[t].error_message; + } + } + + // ---- Phase 3: process sequential material state ---- + // Compute v/vn/vt prefix sums across threads + std::vector v_prefix(num_t), vn_prefix(num_t), vt_prefix(num_t); + v_prefix[0] = vn_prefix[0] = vt_prefix[0] = 0; + for (size_t t = 1; t < num_t; t++) { + v_prefix[t] = v_prefix[t - 1] + thread_data[t - 1].num_v; + vn_prefix[t] = vn_prefix[t - 1] + thread_data[t - 1].num_vn; + vt_prefix[t] = vt_prefix[t - 1] + thread_data[t - 1].num_vt; + } + + std::map material_map; + std::vector initial_material_id(num_t, -1); + size_t eof_pending_degenerate_faces = 0; + size_t phase3_error_line = 0; + std::string phase3_error_message; + { + MaterialFileReader mat_file_reader(mtl_basedir); + std::set material_filenames; + std::vector temp_materials; + std::vector *material_dst = materials ? materials : &temp_materials; + int running_material_id = -1; + size_t pending_degenerate_faces = 0; + for (size_t t = 0; t < num_t; t++) { + if (phase3_error_line != 0) { + break; + } + + initial_material_id[t] = running_material_id; + const OptThreadData &td = thread_data[t]; + + for (size_t si = 0; si < td.seq.size(); si++) { + const OptSeqEntry &entry = td.seq[si]; + + if (entry.kind == OptSeqEntry::SEQ_FACE) { + const OptFaceCmd &face = td.faces[entry.index]; + + // Compute running counts from prefix sums + per-face stored counts + int rv = static_cast(v_prefix[t] + face.v_count_before); + int rvn = static_cast(vn_prefix[t] + face.vn_count_before); + int rvt = static_cast(vt_prefix[t] + face.vt_count_before); + + if (first_error_line != 0 && face.source_line >= first_error_line) { + continue; + } + + if (face.degenerate) { + pending_degenerate_faces++; + continue; + } + + for (size_t k = 0; k < face.face_vertex_count; k++) { + const opt_index_t &raw = face.face_indices()[k]; + int resolved_idx = -1; + + if (!opt_validateAndResolveFaceIndexLikeLegacy( + raw.vertex_index, rv, false, source_name, + face.source_line, warn, &resolved_idx)) { + phase3_error_line = face.source_line; + phase3_error_message = + "failed to parse `f' line (invalid vertex index)"; + break; + } + + if (raw.texcoord_index != opt_index_t::kNotPresent) { + if (!opt_validateAndResolveFaceIndexLikeLegacy( + raw.texcoord_index, rvt, true, source_name, + face.source_line, warn, &resolved_idx)) { + phase3_error_line = face.source_line; + phase3_error_message = + "failed to parse `f' line (invalid vertex index)"; + break; + } + } + + if (raw.normal_index != opt_index_t::kNotPresent) { + if (!opt_validateAndResolveFaceIndexLikeLegacy( + raw.normal_index, rvn, true, source_name, + face.source_line, warn, &resolved_idx)) { + phase3_error_line = face.source_line; + phase3_error_message = + "failed to parse `f' line (invalid vertex index)"; + break; + } + } + } + + if (phase3_error_line != 0) { + break; + } + + continue; + } + + // SEQ_META + OptMetaCmd &meta = thread_data[t].metas[entry.index]; + + if (first_error_line != 0 && meta.source_line >= first_error_line) { + continue; + } + + if (meta.type == OPT_CMD_G || meta.type == OPT_CMD_O) { + for (size_t deg = 0; deg < pending_degenerate_faces; deg++) { + if (warn) { + (*warn) += "Degenerated face found\n."; + } + } + pending_degenerate_faces = 0; + } + + if (meta.type == OPT_CMD_G && meta.group_name_empty) { + if (warn) { + std::stringstream ss; + ss << "Empty group name. line: " << meta.source_line << "\n"; + (*warn) += ss.str(); + } + continue; + } + + if (meta.type == OPT_CMD_MTLLIB) { + if (!enable_mtllib_loading) { + continue; + } + + std::string line_rest; + if (meta.str_ptr && meta.str_len > 0) { + line_rest.assign(meta.str_ptr, meta.str_len); + } + + std::vector filenames; + SplitString(line_rest, ' ', '\\', filenames); + RemoveEmptyTokens(&filenames); + + if (filenames.empty()) { + if (warn) { + std::stringstream ss; + ss << "Looks like empty filename for mtllib. Use default " + "material (line " + << meta.source_line << ".)\n"; + (*warn) += ss.str(); + } + continue; + } + + bool found = false; + for (size_t s = 0; s < filenames.size(); s++) { + if (material_filenames.count(filenames[s]) > 0) { + found = true; + continue; + } + + std::string warn_mtl; + std::string err_mtl; + bool ok = mat_file_reader(filenames[s], material_dst, &material_map, + &warn_mtl, &err_mtl); + + if (warn && !warn_mtl.empty()) { + (*warn) += warn_mtl; + } + + if (err && !err_mtl.empty()) { + (*err) += err_mtl; + } + + if (ok) { + found = true; + material_filenames.insert(filenames[s]); + break; + } + } + + if (!found) { + if (warn) { + (*warn) += + "Failed to load material file(s). Use default material.\n"; + } + } + continue; + } + + if (meta.type == OPT_CMD_USEMTL) { + std::string mat_name; + if (meta.str_ptr && meta.str_len > 0) { + mat_name.assign(meta.str_ptr, meta.str_len); + } + while (!mat_name.empty() && + (mat_name.back() == '\r' || mat_name.back() == '\n')) { + mat_name.pop_back(); + } + + std::map::const_iterator it = material_map.find(mat_name); + if (it != material_map.end()) { + meta.resolved_material_id = it->second; + } else { + meta.resolved_material_id = -1; + if (warn) { + (*warn) += "material [ '" + mat_name + "' ] not found in .mtl\n"; + } + } + running_material_id = meta.resolved_material_id; + continue; + } + } + + } + + if (first_error_line == 0) { + eof_pending_degenerate_faces = pending_degenerate_faces; + } + } + + if (phase3_error_line != 0) { + if (err) { + std::stringstream ss; + ss << "Failed parse line(line " << phase3_error_line << "). " + << phase3_error_message << "\n"; + if (!err->empty()) { + (*err) += ss.str(); + } else { + (*err) = ss.str(); + } + } + return false; + } + + if (first_error_line != 0) { + if (err) { + std::stringstream ss; + ss << "Failed parse line(line " << first_error_line << "). " + << first_error_message << "\n"; + if (!err->empty()) { + (*err) += ss.str(); + } else { + (*err) = ss.str(); + } + } + return false; + } + + // ---- Phase 4: merge results ---- + size_t num_v = 0, num_vn = 0, num_vt = 0; + size_t total_idx = 0; // total index_t entries across all faces + size_t total_faces = 0; // total emitted faces + for (size_t t = 0; t < num_t; t++) { + num_v += thread_data[t].num_v; + num_vn += thread_data[t].num_vn; + num_vt += thread_data[t].num_vt; + total_idx += thread_data[t].num_f_indices; + total_faces += thread_data[t].num_f_faces; + } + + // Guard against size_t overflow in vertex/normal/texcoord allocation + if (num_v > SIZE_MAX / 3 || num_vn > SIZE_MAX / 3 || num_vt > SIZE_MAX / 2) { + if (err) { + (*err) += "Integer overflow in vertex/normal/texcoord count.\n"; + } + return false; + } + + attrib->vertices.resize(num_v * 3); + attrib->vertex_weights.resize(num_v, real_t(1.0)); + attrib->normals.resize(num_vn * 3); + attrib->texcoords.resize(num_vt * 2); + attrib->texcoord_ws.resize(num_vt, real_t(0.0)); + std::vector all_smoothing_group_ids( + static_cast(total_faces), 0); + attrib->indices.resize(total_idx); + attrib->face_num_verts.resize(static_cast(total_faces)); + attrib->material_ids.resize(static_cast(total_faces), -1); + std::vector thread_saw_any_vertex_color(num_t, 0); + std::vector thread_missing_vertex_color(num_t, 0); + std::vector all_colors(num_v * 3, real_t(1.0)); + + // Compute per-thread offsets + std::vector v_off(num_t), n_off(num_t), t_off(num_t), f_off(num_t), + face_off(num_t); + v_off[0] = n_off[0] = t_off[0] = f_off[0] = face_off[0] = 0; + for (size_t t = 1; t < num_t; t++) { + v_off[t] = v_off[t - 1] + thread_data[t - 1].num_v; + n_off[t] = n_off[t - 1] + thread_data[t - 1].num_vn; + t_off[t] = t_off[t - 1] + thread_data[t - 1].num_vt; + f_off[t] = f_off[t - 1] + thread_data[t - 1].num_f_indices; + face_off[t] = face_off[t - 1] + thread_data[t - 1].num_f_faces; + } + + // Carry parser state that persists across lines, such as smoothing groups, + // across thread chunk boundaries before merging in parallel. + std::vector initial_smoothing_group_id(num_t, 0); + unsigned int running_smoothing_group_id = 0; + for (size_t t = 0; t < num_t; t++) { + initial_smoothing_group_id[t] = running_smoothing_group_id; + for (size_t mi = 0; mi < thread_data[t].metas.size(); mi++) { + if (thread_data[t].metas[mi].type == OPT_CMD_S) { + running_smoothing_group_id = thread_data[t].metas[mi].smoothing_group_id; + } + } + } + + // Merge parsed data into final arrays + std::vector > command_written_faces(num_t); + for (size_t t = 0; t < num_t; t++) { + command_written_faces[t].assign(thread_data[t].faces.size(), 0); + } + std::vector written_index_counts(num_t, 0); + std::vector written_face_counts(num_t, 0); + std::vector merge_error_lines(num_t, 0); + std::vector merge_error_messages(num_t); + std::vector thread_greatest_v_idx(num_t, -1); + std::vector thread_greatest_vn_idx(num_t, -1); + std::vector thread_greatest_vt_idx(num_t, -1); + auto merge_vertex_thread = [&](size_t t) { + const OptThreadData &td = thread_data[t]; + // Bulk copy positions + if (!td.v_pos.empty()) { + std::memcpy(&attrib->vertices[v_off[t] * 3], td.v_pos.data(), + td.v_pos.size() * sizeof(real_t)); + } + // Copy weights + if (td.saw_any_weight) { + for (size_t i = 0; i < td.num_v; i++) + attrib->vertex_weights[v_off[t] + i] = td.v_weight[i]; + } + // Copy colors + if (td.saw_any_color) { + std::memcpy(&all_colors[v_off[t] * 3], td.v_color.data(), + td.v_color.size() * sizeof(real_t)); + thread_saw_any_vertex_color[t] = 1; + if (td.saw_missing_color) thread_missing_vertex_color[t] = 1; + } else if (td.saw_missing_color) { + thread_missing_vertex_color[t] = 1; + } + // Bulk copy normals + if (!td.vn_data.empty()) { + std::memcpy(&attrib->normals[n_off[t] * 3], td.vn_data.data(), + td.vn_data.size() * sizeof(real_t)); + } + // Bulk copy texcoords + if (!td.vt_data.empty()) { + std::memcpy(&attrib->texcoords[t_off[t] * 2], td.vt_data.data(), + td.vt_data.size() * sizeof(real_t)); + } + if (td.saw_any_texcoord_w) { + for (size_t i = 0; i < td.num_vt; i++) + attrib->texcoord_ws[t_off[t] + i] = td.vt_w[i]; + } + }; + auto merge_thread = [&](size_t t) { + const OptThreadData &td = thread_data[t]; + size_t fc = f_off[t], fcc = face_off[t]; + int current_mat_id = initial_material_id[t]; + unsigned int current_smoothing_id = initial_smoothing_group_id[t]; + int greatest_v_idx = -1; + int greatest_vn_idx = -1; + int greatest_vt_idx = -1; + + for (size_t si = 0; si < td.seq.size(); si++) { + const OptSeqEntry &entry = td.seq[si]; + if (entry.kind == OptSeqEntry::SEQ_META) { + const OptMetaCmd &meta = td.metas[entry.index]; + if (meta.type == OPT_CMD_USEMTL) + current_mat_id = meta.resolved_material_id; + else if (meta.type == OPT_CMD_S) + current_smoothing_id = meta.smoothing_group_id; + continue; + } + // SEQ_FACE + const OptFaceCmd &face = td.faces[entry.index]; + if (face.degenerate) { + command_written_faces[t][entry.index] = 0; + continue; + } + // Compute running counts for index resolution + size_t vc = v_off[t] + face.v_count_before; + size_t nc = n_off[t] + face.vn_count_before; + size_t tc = t_off[t] + face.vt_count_before; + + std::vector resolved_face(face.face_vertex_count); + for (size_t k = 0; k < face.face_vertex_count; k++) { + const opt_index_t &vi = face.face_indices()[k]; + index_t idx; + if (!opt_resolveIndexLikeLegacy(vi.vertex_index, + static_cast(vc), + &idx.vertex_index, false)) { + merge_error_lines[t] = face.source_line; + merge_error_messages[t] = + "failed to parse `f' line (invalid vertex index)"; + return; + } + opt_updateGreatestIndex(idx.vertex_index, &greatest_v_idx); + if (vi.texcoord_index == opt_index_t::kNotPresent) { + idx.texcoord_index = -1; + } else { + if (!opt_resolveIndexLikeLegacy(vi.texcoord_index, + static_cast(tc), + &idx.texcoord_index, true)) { + merge_error_lines[t] = face.source_line; + merge_error_messages[t] = + "failed to parse `f' line (invalid vertex index)"; + return; + } + if (idx.texcoord_index >= 0) { + opt_updateGreatestIndex(idx.texcoord_index, &greatest_vt_idx); + } + } + if (vi.normal_index == opt_index_t::kNotPresent) { + idx.normal_index = -1; + } else { + if (!opt_resolveIndexLikeLegacy(vi.normal_index, + static_cast(nc), + &idx.normal_index, true)) { + merge_error_lines[t] = face.source_line; + merge_error_messages[t] = + "failed to parse `f' line (invalid vertex index)"; + return; + } + if (idx.normal_index >= 0) { + opt_updateGreatestIndex(idx.normal_index, &greatest_vn_idx); + } + } + resolved_face[k] = idx; + } + size_t written_index_count = 0; + size_t written_face_count = 0; + if (config.triangulate) { + written_index_count = opt_triangulate_face( + attrib->vertices, resolved_face.data(), resolved_face.size(), + &attrib->indices[fc]); + written_face_count = written_index_count / 3; + } else { + written_index_count = resolved_face.size(); + written_face_count = 1; + for (size_t k = 0; k < resolved_face.size(); k++) { + attrib->indices[fc + k] = resolved_face[k]; + } + } + for (size_t k = 0; k < written_face_count; k++) { + attrib->face_num_verts[fcc + k] = config.triangulate + ? 3 + : static_cast(face.emitted_face_verts); + attrib->material_ids[fcc + k] = current_mat_id; + all_smoothing_group_ids[fcc + k] = current_smoothing_id; + } + command_written_faces[t][entry.index] = + static_cast(written_face_count); + fc += written_index_count; + fcc += written_face_count; + } + written_index_counts[t] = fc - f_off[t]; + written_face_counts[t] = fcc - face_off[t]; + thread_greatest_v_idx[t] = greatest_v_idx; + thread_greatest_vn_idx[t] = greatest_vn_idx; + thread_greatest_vt_idx[t] = greatest_vt_idx; + }; + +#ifdef TINYOBJLOADER_USE_MULTITHREADING + if (num_threads > 1) { + std::vector workers; + workers.reserve(num_t); + for (size_t t = 0; t < num_t; t++) { + workers.emplace_back([&, t]() { merge_vertex_thread(t); }); + } + for (auto &w : workers) w.join(); + workers.clear(); + for (size_t t = 0; t < num_t; t++) { + workers.emplace_back([&, t]() { merge_thread(t); }); + } + for (auto &w : workers) w.join(); + } else { + for (size_t t = 0; t < num_t; t++) merge_vertex_thread(t); + for (size_t t = 0; t < num_t; t++) merge_thread(t); + } +#else + for (size_t t = 0; t < num_t; t++) merge_vertex_thread(t); + for (size_t t = 0; t < num_t; t++) merge_thread(t); +#endif + + size_t first_merge_error_line = 0; + std::string first_merge_error_message; + for (size_t t = 0; t < merge_error_lines.size(); t++) { + if (merge_error_lines[t] == 0) continue; + if (first_merge_error_line == 0 || + merge_error_lines[t] < first_merge_error_line) { + first_merge_error_line = merge_error_lines[t]; + first_merge_error_message = merge_error_messages[t]; + } + } + + if (first_merge_error_line != 0) { + attrib->vertices.clear(); + attrib->vertex_weights.clear(); + attrib->normals.clear(); + attrib->texcoords.clear(); + attrib->texcoord_ws.clear(); + attrib->colors.clear(); + attrib->skin_weights.clear(); + attrib->indices.clear(); + attrib->face_num_verts.clear(); + attrib->material_ids.clear(); + shapes->clear(); + if (materials) materials->clear(); + + if (err) { + std::stringstream ss; + ss << "Failed parse line(line " << first_merge_error_line << "). " + << first_merge_error_message << "\n"; + if (!err->empty()) { + (*err) += ss.str(); + } else { + (*err) = ss.str(); + } + } + return false; + } + + bool saw_any_vertex_color = false; + bool saw_missing_vertex_color = false; + for (size_t t = 0; t < num_t; t++) { + saw_any_vertex_color = + saw_any_vertex_color || (thread_saw_any_vertex_color[t] != 0); + saw_missing_vertex_color = + saw_missing_vertex_color || (thread_missing_vertex_color[t] != 0); + } + + if (saw_any_vertex_color && !saw_missing_vertex_color) { + attrib->colors.swap(all_colors); + } else { + attrib->colors.clear(); + } + + size_t actual_num_indices = 0; + size_t actual_num_faces = 0; + int greatest_v_idx = -1; + int greatest_vn_idx = -1; + int greatest_vt_idx = -1; + for (size_t t = 0; t < num_t; t++) { + actual_num_indices += written_index_counts[t]; + actual_num_faces += written_face_counts[t]; + opt_updateGreatestIndex(thread_greatest_v_idx[t], &greatest_v_idx); + opt_updateGreatestIndex(thread_greatest_vn_idx[t], &greatest_vn_idx); + opt_updateGreatestIndex(thread_greatest_vt_idx[t], &greatest_vt_idx); + } + + // Compact face arrays to remove gaps left by threads that wrote fewer + // indices/faces than pre-allocated (e.g. triangulation returning 0 for + // faces with out-of-bounds vertex indices). + if (num_t > 1) { + size_t dst_idx = 0; + size_t dst_face = 0; + for (size_t t = 0; t < num_t; t++) { + size_t src_idx = f_off[t]; + size_t src_face = face_off[t]; + size_t idx_count = written_index_counts[t]; + size_t fc_count = written_face_counts[t]; + if (dst_idx != src_idx && idx_count > 0) { + std::memmove(&attrib->indices[dst_idx], &attrib->indices[src_idx], + idx_count * sizeof(index_t)); + } + if (dst_face != src_face && fc_count > 0) { + std::memmove(&attrib->face_num_verts[dst_face], + &attrib->face_num_verts[src_face], + fc_count * sizeof(int)); + std::memmove(&attrib->material_ids[dst_face], + &attrib->material_ids[src_face], + fc_count * sizeof(int)); + std::memmove(&all_smoothing_group_ids[dst_face], + &all_smoothing_group_ids[src_face], + fc_count * sizeof(unsigned int)); + } + dst_idx += idx_count; + dst_face += fc_count; + } + } + + attrib->indices.resize(actual_num_indices); + attrib->face_num_verts.resize(actual_num_faces); + attrib->material_ids.resize(actual_num_faces); + all_smoothing_group_ids.resize(actual_num_faces); + + opt_appendOutOfBoundsWarnings( + warn, greatest_v_idx, greatest_vn_idx, greatest_vt_idx, + static_cast(attrib->vertices.size() / 3), + static_cast(attrib->normals.size() / 3), + static_cast(attrib->texcoords.size() / 2), + all_line_infos.size() + 1); + for (size_t deg = 0; deg < eof_pending_degenerate_faces; deg++) { + if (warn) { + (*warn) += "Degenerated face found\n."; + } + } + + // ---- Phase 5: construct shapes ---- + { + // Precompute prefix-sum of index offsets for O(1) slicing + const size_t total_faces = attrib->face_num_verts.size(); + std::vector idx_prefix(total_faces + 1); + idx_prefix[0] = 0; + for (size_t fi = 0; fi < total_faces; fi++) { + idx_prefix[fi + 1] = + idx_prefix[fi] + static_cast(attrib->face_num_verts[fi]); + } + + size_t face_count = 0; + basic_shape_t<> shape; + size_t face_prev_offset = 0; + bool shape_has_face_record = false; + bool have_active_shape_name = false; + + for (size_t t = 0; t < num_t; t++) { + const OptThreadData &td = thread_data[t]; + for (size_t si = 0; si < td.seq.size(); si++) { + const OptSeqEntry &entry = td.seq[si]; + if (entry.kind == OptSeqEntry::SEQ_META) { + const OptMetaCmd &meta = td.metas[entry.index]; + if (meta.type == OPT_CMD_O || meta.type == OPT_CMD_G) { + std::string name; + if (meta.type == OPT_CMD_O && meta.str_ptr) { + name.assign(meta.str_ptr, meta.str_len); + } else if (!meta.str_storage.empty()) { + name = meta.str_storage; + } else if (meta.str_ptr) { + name.assign(meta.str_ptr, meta.str_len); + } + while (!name.empty() && + (name.back() == '\r' || name.back() == '\n')) + name.pop_back(); + + if (face_count == 0) { + shape.name = name; + face_prev_offset = 0; + have_active_shape_name = true; + } else { + if (!have_active_shape_name) { + // faces before first group/object + basic_shape_t<> prev_shape; + prev_shape.mesh.num_face_vertices.assign( + attrib->face_num_verts.begin(), + attrib->face_num_verts.begin() + + static_cast(face_count)); + prev_shape.mesh.indices.assign( + attrib->indices.begin(), + attrib->indices.begin() + + static_cast(idx_prefix[face_count])); + prev_shape.mesh.material_ids.assign( + attrib->material_ids.begin(), + attrib->material_ids.begin() + + static_cast(face_count)); + prev_shape.mesh.smoothing_group_ids.assign( + all_smoothing_group_ids.begin(), + all_smoothing_group_ids.begin() + + static_cast(face_count)); + shapes->push_back(std::move(prev_shape)); + } else if (face_count > face_prev_offset) { + // push previous shape + basic_shape_t<> prev_shape; + prev_shape.name = shape.name; + prev_shape.mesh.num_face_vertices.assign( + attrib->face_num_verts.begin() + + static_cast(face_prev_offset), + attrib->face_num_verts.begin() + + static_cast(face_count)); + prev_shape.mesh.indices.assign( + attrib->indices.begin() + + static_cast(idx_prefix[face_prev_offset]), + attrib->indices.begin() + + static_cast(idx_prefix[face_count])); + prev_shape.mesh.material_ids.assign( + attrib->material_ids.begin() + + static_cast(face_prev_offset), + attrib->material_ids.begin() + + static_cast(face_count)); + prev_shape.mesh.smoothing_group_ids.assign( + all_smoothing_group_ids.begin() + + static_cast(face_prev_offset), + all_smoothing_group_ids.begin() + + static_cast(face_count)); + shapes->push_back(std::move(prev_shape)); + } + shape.name = name; + face_prev_offset = face_count; + have_active_shape_name = true; + } + shape_has_face_record = false; + } + } else { + // SEQ_FACE + shape_has_face_record = true; + face_count += command_written_faces[t][entry.index]; + } + } + } + + // Final shape + if (face_count > face_prev_offset || shape_has_face_record) { + basic_shape_t<> final_shape; + final_shape.name = shape.name; + final_shape.mesh.num_face_vertices.assign( + attrib->face_num_verts.begin() + + static_cast(face_prev_offset), + attrib->face_num_verts.begin() + + static_cast(face_count)); + final_shape.mesh.indices.assign( + attrib->indices.begin() + + static_cast(idx_prefix[face_prev_offset]), + attrib->indices.begin() + + static_cast(idx_prefix[face_count])); + final_shape.mesh.material_ids.assign( + attrib->material_ids.begin() + + static_cast(face_prev_offset), + attrib->material_ids.begin() + + static_cast(face_count)); + final_shape.mesh.smoothing_group_ids.assign( + all_smoothing_group_ids.begin() + + static_cast(face_prev_offset), + all_smoothing_group_ids.begin() + + static_cast(face_count)); + shapes->push_back(std::move(final_shape)); + } + } + + return true; +} + +// ---- LoadObjOpt (buffer version, public API) ---- + +bool LoadObjOpt(basic_attrib_t<> *attrib, + std::vector> *shapes, + std::vector *materials, + std::string *warn, std::string *err, + const char *buf, size_t buf_len, + const OptLoadConfig &config) { + return LoadObjOpt_internal(attrib, shapes, materials, warn, err, + buf, buf_len, std::string(), "", false, + config); +} + +// ---- LoadObjOpt (file version) ---- + +bool LoadObjOpt(basic_attrib_t<> *attrib, + std::vector> *shapes, + std::vector *materials, + std::string *warn, std::string *err, + const char *filename, + const char *mtl_basedir, + const OptLoadConfig &config) { + if (!filename) { + if (err) *err = "filename is null."; + return false; + } + + std::string filepath(filename); + + // Resolve material base directory + std::string baseDir; + if (mtl_basedir) { + baseDir = mtl_basedir; + } else { + // Extract directory from filename + size_t pos = filepath.find_last_of("/\\"); + if (pos != std::string::npos) { + baseDir = filepath.substr(0, pos + 1); + } + } + if (!baseDir.empty()) { +#ifndef _WIN32 + const char dirsep = '/'; +#else + const char dirsep = '\\'; +#endif + if (baseDir[baseDir.length() - 1] != dirsep) baseDir += dirsep; + } + +#ifdef TINYOBJLOADER_USE_MMAP + { + MappedFile mf; + if (mf.open(filepath.c_str())) { + return LoadObjOpt_internal(attrib, shapes, materials, warn, err, + mf.data, mf.size, baseDir, filepath, true, + config); + } + } +#endif + +#ifdef _WIN32 + std::ifstream ifs(LongPathW(UTF8ToWchar(filepath)).c_str(), + std::ios::binary | std::ios::ate); +#else + std::ifstream ifs(filepath.c_str(), std::ios::binary | std::ios::ate); +#endif + if (!ifs.is_open()) { + if (err) *err = "Cannot open file: " + filepath; + return false; + } + + std::streamsize fsize = ifs.tellg(); + ifs.seekg(0, std::ios::beg); + + if (fsize <= 0) { + return LoadObjOpt_internal(attrib, shapes, materials, warn, err, + "", static_cast(0), baseDir, filepath, + true, config); + } + + std::vector buf(static_cast(fsize)); + if (!ifs.read(buf.data(), fsize)) { + if (err) *err = "Failed to read file: " + filepath; + return false; + } + ifs.close(); + + // Parse the in-memory file buffer with baseDir for mtllib resolution. + return LoadObjOpt_internal(attrib, shapes, materials, warn, err, + buf.data(), static_cast(fsize), baseDir, + filepath, true, config); +} + +// ---- LoadObjOptTyped internals ---- + +static bool LoadObjOptTyped_internal(OptResult *result, + std::string *warn, std::string *err, + const char *buf, size_t buf_len, + const std::string &mtl_basedir, + const std::string &source_name, + bool enable_mtllib_loading, + const OptLoadConfig &config) { + using namespace opt_internal; + + ArenaAllocator &arena = result->arena; + OptAttrib &attrib = result->attrib; + result->valid = false; + + if (buf_len < 1) { + result->valid = true; + return true; + } + if (!buf) { + if (err) *err = "buf must not be null when buf_len > 0."; + return false; + } + + const char *work_buf = buf; + size_t work_len = buf_len; + std::vector buf_with_sentinel; + if (buf[buf_len - 1] != '\n') { + buf_with_sentinel.assign(buf, buf + buf_len); + buf_with_sentinel.push_back('\n'); + work_buf = buf_with_sentinel.data(); + work_len = buf_with_sentinel.size(); + } + + int num_threads = 1; +#ifdef TINYOBJLOADER_USE_MULTITHREADING + if (config.num_threads < 0) { + num_threads = static_cast(std::thread::hardware_concurrency()); + if (num_threads < 1) num_threads = 1; + } else if (config.num_threads > 1) { + num_threads = config.num_threads; + } + if (num_threads > kOptMaxThreads) num_threads = kOptMaxThreads; +#else +#endif + + // ---- Phase 1: find line boundaries ---- + std::vector all_line_infos; + +#if defined(TINYOBJLOADER_USE_SIMD) && \ + (defined(TINYOBJLOADER_SIMD_SSE2) || defined(TINYOBJLOADER_SIMD_AVX2) || \ + defined(TINYOBJLOADER_SIMD_NEON)) + { + std::vector nl_positions; + nl_positions.reserve(work_len / 64); + simd_find_newlines(work_buf, work_len, nl_positions); + simd_build_line_infos(work_buf, work_len, nl_positions, all_line_infos); + } +#else + { + all_line_infos.reserve(work_len / 64); + scalar_find_line_infos(work_buf, 0, work_len, all_line_infos); + } +#endif + + const size_t total_lines = all_line_infos.size(); + if (total_lines == 0) { + result->valid = true; + return true; + } + + // Fast buffer-level check for legacy-only tokens (replaces per-line scan) + if (opt_buffer_requires_legacy_fallback(work_buf, work_len)) { + basic_attrib_t<> tmp_attrib; + std::vector> tmp_shapes; + std::vector tmp_materials; + std::string input(buf, buf + buf_len); + std::istringstream iss(input); + attrib_t legacy_attrib; + std::vector legacy_shapes; + std::vector legacy_materials; + MaterialFileReader mat_reader(mtl_basedir); + MaterialReader *reader = + enable_mtllib_loading ? static_cast(&mat_reader) : NULL; + const bool ok = LoadObj(&legacy_attrib, &legacy_shapes, &legacy_materials, + warn, err, &iss, reader, config.triangulate, false); + if (!ok) return false; + ConvertLegacyResultToBasic(legacy_attrib, legacy_shapes, legacy_materials, + &tmp_attrib, &tmp_shapes, &tmp_materials); +#define TINYOBJ_COPY_VEC_TO_ARENA_(dst, src) \ + do { \ + if (!(src).empty()) { \ + (dst).allocate(arena, (src).size()); \ + std::memcpy((dst).data(), (src).data(), \ + (src).size() * sizeof((src)[0])); \ + } \ + } while (0) + TINYOBJ_COPY_VEC_TO_ARENA_(attrib.vertices, tmp_attrib.vertices); + TINYOBJ_COPY_VEC_TO_ARENA_(attrib.vertex_weights, tmp_attrib.vertex_weights); + TINYOBJ_COPY_VEC_TO_ARENA_(attrib.normals, tmp_attrib.normals); + TINYOBJ_COPY_VEC_TO_ARENA_(attrib.texcoords, tmp_attrib.texcoords); + TINYOBJ_COPY_VEC_TO_ARENA_(attrib.texcoord_ws, tmp_attrib.texcoord_ws); + TINYOBJ_COPY_VEC_TO_ARENA_(attrib.colors, tmp_attrib.colors); + TINYOBJ_COPY_VEC_TO_ARENA_(attrib.indices, tmp_attrib.indices); + TINYOBJ_COPY_VEC_TO_ARENA_(attrib.face_num_verts, tmp_attrib.face_num_verts); + TINYOBJ_COPY_VEC_TO_ARENA_(attrib.material_ids, tmp_attrib.material_ids); +#undef TINYOBJ_COPY_VEC_TO_ARENA_ + // Flatten per-shape smoothing_group_ids into attrib + { + size_t total_sg = 0; + for (size_t si = 0; si < tmp_shapes.size(); si++) + total_sg += tmp_shapes[si].mesh.smoothing_group_ids.size(); + if (total_sg > 0) { + attrib.smoothing_group_ids.allocate(arena, total_sg); + size_t off = 0; + for (size_t si = 0; si < tmp_shapes.size(); si++) { + const auto &sg = tmp_shapes[si].mesh.smoothing_group_ids; + if (!sg.empty()) { + std::memcpy(&attrib.smoothing_group_ids[off], sg.data(), + sg.size() * sizeof(sg[0])); + off += sg.size(); + } + } + } + } + result->shapes.clear(); + size_t idx_off = 0, face_off = 0; + for (size_t si = 0; si < tmp_shapes.size(); si++) { + OptShapeRange sr; + sr.name = tmp_shapes[si].name; + sr.face_offset = face_off; + sr.face_count = tmp_shapes[si].mesh.num_face_vertices.size(); + sr.index_offset = idx_off; + sr.index_count = tmp_shapes[si].mesh.indices.size(); + face_off += sr.face_count; + idx_off += sr.index_count; + result->shapes.push_back(std::move(sr)); + } + result->materials = tmp_materials; + result->valid = true; + return true; + } + + // ---- Phase 2: parse lines (compact storage) ---- + const size_t num_t = static_cast(num_threads); + std::vector thread_data(num_t); + +#ifdef TINYOBJLOADER_USE_MULTITHREADING + { + size_t lines_per_thread = total_lines / num_t; + std::vector workers; + workers.reserve(num_t); + + for (int t = 0; t < num_threads; t++) { + size_t start = static_cast(t) * lines_per_thread; + size_t end = (t == num_threads - 1) + ? total_lines + : (static_cast(t) + 1) * lines_per_thread; + + workers.emplace_back([&, t, start, end]() { + OptThreadData &td = thread_data[static_cast(t)]; + size_t est_lines = end - start; + td.v_pos.reserve(est_lines * 2); + td.faces.reserve(est_lines / 3); + td.seq.reserve(est_lines / 3); + OptFloatCache *tc = nullptr; +#ifndef TINYOBJLOADER_DISABLE_FAST_FLOAT + OptFloatCache thread_cache(config.float_cache_max_nodes, + config.fp32_cache); + if (config.float_cache) tc = &thread_cache; +#endif + for (size_t i = start; i < end; i++) { + opt_parseLineToThreadData(td, &work_buf[all_line_infos[i].pos], + all_line_infos[i].len, config.triangulate, + i + 1, tc); + if (td.error_line != 0) break; + } + }); + } + for (auto &w : workers) w.join(); + } +#else + { + OptThreadData &td = thread_data[0]; + td.v_pos.reserve(total_lines * 2); + td.faces.reserve(total_lines / 3); + td.seq.reserve(total_lines / 3); + OptFloatCache *tc = nullptr; +#ifndef TINYOBJLOADER_DISABLE_FAST_FLOAT + OptFloatCache thread_cache(config.float_cache_max_nodes, + config.fp32_cache); + if (config.float_cache) tc = &thread_cache; +#endif + for (size_t i = 0; i < total_lines; i++) { + opt_parseLineToThreadData(td, &work_buf[all_line_infos[i].pos], + all_line_infos[i].len, config.triangulate, + i + 1, tc); + if (td.error_line != 0) break; + } + } +#endif + + // Check for parse errors + size_t first_error_line = 0; + std::string first_error_message; + for (size_t t = 0; t < num_t; t++) { + if (thread_data[t].error_line == 0) continue; + if (first_error_line == 0 || thread_data[t].error_line < first_error_line) { + first_error_line = thread_data[t].error_line; + first_error_message = thread_data[t].error_message; + } + } + + // ---- Phase 3: process sequential material state ---- + // Compute v/vn/vt prefix sums across threads + std::vector v_prefix(num_t), vn_prefix(num_t), vt_prefix(num_t); + v_prefix[0] = vn_prefix[0] = vt_prefix[0] = 0; + for (size_t t = 1; t < num_t; t++) { + v_prefix[t] = v_prefix[t - 1] + thread_data[t - 1].num_v; + vn_prefix[t] = vn_prefix[t - 1] + thread_data[t - 1].num_vn; + vt_prefix[t] = vt_prefix[t - 1] + thread_data[t - 1].num_vt; + } + + std::map material_map; + std::vector initial_material_id(num_t, -1); + size_t eof_pending_degenerate_faces = 0; + size_t phase3_error_line = 0; + std::string phase3_error_message; + { + MaterialFileReader mat_file_reader(mtl_basedir); + std::set material_filenames; + std::vector *material_dst = &result->materials; + int running_material_id = -1; + size_t pending_degenerate_faces = 0; + + for (size_t t = 0; t < num_t; t++) { + if (phase3_error_line != 0) break; + initial_material_id[t] = running_material_id; + const OptThreadData &td = thread_data[t]; + + for (size_t si = 0; si < td.seq.size(); si++) { + const OptSeqEntry &entry = td.seq[si]; + + if (entry.kind == OptSeqEntry::SEQ_FACE) { + const OptFaceCmd &face = td.faces[entry.index]; + int rv = static_cast(v_prefix[t] + face.v_count_before); + int rvn = static_cast(vn_prefix[t] + face.vn_count_before); + int rvt = static_cast(vt_prefix[t] + face.vt_count_before); + + if (first_error_line != 0 && face.source_line >= first_error_line) continue; + + if (face.degenerate) { + pending_degenerate_faces++; + continue; + } + + for (size_t k = 0; k < face.face_vertex_count; k++) { + const opt_index_t &raw = face.face_indices()[k]; + int resolved_idx = -1; + if (!opt_validateAndResolveFaceIndexLikeLegacy( + raw.vertex_index, rv, false, source_name, + face.source_line, warn, &resolved_idx)) { + phase3_error_line = face.source_line; + phase3_error_message = "failed to parse `f' line (invalid vertex index)"; + break; + } + if (raw.texcoord_index != opt_index_t::kNotPresent) { + if (!opt_validateAndResolveFaceIndexLikeLegacy( + raw.texcoord_index, rvt, true, source_name, + face.source_line, warn, &resolved_idx)) { + phase3_error_line = face.source_line; + phase3_error_message = "failed to parse `f' line (invalid vertex index)"; + break; + } + } + if (raw.normal_index != opt_index_t::kNotPresent) { + if (!opt_validateAndResolveFaceIndexLikeLegacy( + raw.normal_index, rvn, true, source_name, + face.source_line, warn, &resolved_idx)) { + phase3_error_line = face.source_line; + phase3_error_message = "failed to parse `f' line (invalid vertex index)"; + break; + } + } + } + if (phase3_error_line != 0) break; + continue; + } + + // SEQ_META + OptMetaCmd &meta = thread_data[t].metas[entry.index]; + + if (first_error_line != 0 && meta.source_line >= first_error_line) continue; + + if (meta.type == OPT_CMD_G || meta.type == OPT_CMD_O) { + for (size_t deg = 0; deg < pending_degenerate_faces; deg++) { + if (warn) (*warn) += "Degenerated face found\n."; + } + pending_degenerate_faces = 0; + } + + if (meta.type == OPT_CMD_G && meta.group_name_empty) { + if (warn) { + (*warn) += "Empty group name. line: " + + std::to_string(meta.source_line) + "\n"; + } + continue; + } + + if (meta.type == OPT_CMD_MTLLIB) { + if (!enable_mtllib_loading) continue; + std::string line_rest; + if (meta.str_ptr && meta.str_len > 0) { + line_rest.assign(meta.str_ptr, meta.str_len); + } + std::vector filenames; + SplitString(line_rest, ' ', '\\', filenames); + RemoveEmptyTokens(&filenames); + if (filenames.empty()) { + if (warn) { + (*warn) += "Looks like empty filename for mtllib. Use default " + "material (line " + + std::to_string(meta.source_line) + ".)\n"; + } + continue; + } + bool found = false; + for (size_t s = 0; s < filenames.size(); s++) { + if (material_filenames.count(filenames[s]) > 0) { + found = true; + continue; + } + std::string warn_mtl, err_mtl; + bool ok = mat_file_reader(filenames[s], material_dst, &material_map, + &warn_mtl, &err_mtl); + if (warn && !warn_mtl.empty()) (*warn) += warn_mtl; + if (err && !err_mtl.empty()) (*err) += err_mtl; + if (ok) { + found = true; + material_filenames.insert(filenames[s]); + break; + } + } + if (!found && warn) { + (*warn) += "Failed to load material file(s). Use default material.\n"; + } + continue; + } + + if (meta.type == OPT_CMD_USEMTL) { + std::string mat_name; + if (meta.str_ptr && meta.str_len > 0) { + mat_name.assign(meta.str_ptr, meta.str_len); + } + while (!mat_name.empty() && + (mat_name.back() == '\r' || mat_name.back() == '\n')) { + mat_name.pop_back(); + } + std::map::const_iterator it = material_map.find(mat_name); + if (it != material_map.end()) { + meta.resolved_material_id = it->second; + } else { + meta.resolved_material_id = -1; + if (warn) (*warn) += "material [ '" + mat_name + "' ] not found in .mtl\n"; + } + running_material_id = meta.resolved_material_id; + continue; + } + } + + } + if (first_error_line == 0) { + eof_pending_degenerate_faces = pending_degenerate_faces; + } + } + + if (phase3_error_line != 0) { + if (err) { + (*err) += "Failed parse line(line " + + std::to_string(phase3_error_line) + "). " + + phase3_error_message + "\n"; + } + return false; + } + + if (first_error_line != 0) { + if (err) { + (*err) += "Failed parse line(line " + + std::to_string(first_error_line) + "). " + + first_error_message + "\n"; + } + return false; + } + + // ---- Phase 4: allocate arena arrays and merge ---- + size_t num_v = 0, num_vn = 0, num_vt = 0; + size_t total_idx = 0; // total index_t entries across all faces + size_t total_faces = 0; // total emitted faces + for (size_t t = 0; t < num_t; t++) { + num_v += thread_data[t].num_v; + num_vn += thread_data[t].num_vn; + num_vt += thread_data[t].num_vt; + total_idx += thread_data[t].num_f_indices; + total_faces += thread_data[t].num_f_faces; + } + + // Guard against size_t overflow in vertex/normal/texcoord allocation + if (num_v > SIZE_MAX / 3 || num_vn > SIZE_MAX / 3 || num_vt > SIZE_MAX / 2) { + if (err) { + (*err) += "Integer overflow in vertex/normal/texcoord count.\n"; + } + return false; + } + + // Determine which optional arrays are needed + bool any_color = false; + bool any_weight = false, any_texcoord_w = false, any_smoothing = false; + for (size_t t = 0; t < num_t; t++) { + if (thread_data[t].saw_any_color) any_color = true; + if (thread_data[t].saw_any_weight) any_weight = true; + if (thread_data[t].saw_any_texcoord_w) any_texcoord_w = true; + if (thread_data[t].saw_any_smoothing) any_smoothing = true; + } + + // Allocate from arena + attrib.vertices.allocate(arena, num_v * 3); + if (any_weight) { + attrib.vertex_weights.allocate(arena, num_v); + for (size_t i = 0; i < num_v; i++) + attrib.vertex_weights[i] = real_t(1.0); + } + attrib.normals.allocate(arena, num_vn * 3); + attrib.texcoords.allocate(arena, num_vt * 2); + if (any_texcoord_w) { + attrib.texcoord_ws.allocate(arena, num_vt); + } + attrib.indices.allocate(arena, total_idx); + attrib.face_num_verts.allocate(arena, total_faces); + attrib.material_ids.allocate(arena, total_faces); + for (size_t i = 0; i < total_faces; i++) + attrib.material_ids[i] = -1; + + // Smoothing group ids — only if any smoothing commands seen + TypedArray all_smoothing_group_ids; + if (any_smoothing) { + all_smoothing_group_ids.allocate(arena, total_faces); + } + + // Colors — allocate temp only if any vertex has color + TypedArray all_colors; + if (any_color) { + all_colors.allocate(arena, num_v * 3); + for (size_t i = 0; i < num_v * 3; i++) + all_colors[i] = real_t(1.0); + } + + // Compute per-thread offsets + std::vector v_off(num_t), n_off(num_t), t_off(num_t), f_off(num_t), + face_off(num_t); + v_off[0] = n_off[0] = t_off[0] = f_off[0] = face_off[0] = 0; + for (size_t t = 1; t < num_t; t++) { + v_off[t] = v_off[t - 1] + thread_data[t - 1].num_v; + n_off[t] = n_off[t - 1] + thread_data[t - 1].num_vn; + t_off[t] = t_off[t - 1] + thread_data[t - 1].num_vt; + f_off[t] = f_off[t - 1] + thread_data[t - 1].num_f_indices; + face_off[t] = face_off[t - 1] + thread_data[t - 1].num_f_faces; + } + + // Carry smoothing group state across thread boundaries + std::vector initial_smoothing_group_id(num_t, 0); + unsigned int running_smoothing_group_id = 0; + for (size_t t = 0; t < num_t; t++) { + initial_smoothing_group_id[t] = running_smoothing_group_id; + for (size_t mi = 0; mi < thread_data[t].metas.size(); mi++) { + if (thread_data[t].metas[mi].type == OPT_CMD_S) { + running_smoothing_group_id = thread_data[t].metas[mi].smoothing_group_id; + } + } + } + + // Per-thread face tracking for shape construction + std::vector> command_written_faces(num_t); + for (size_t t = 0; t < num_t; t++) { + command_written_faces[t].assign(thread_data[t].faces.size(), 0); + } + std::vector written_index_counts(num_t, 0); + std::vector written_face_counts(num_t, 0); + std::vector merge_error_lines(num_t, 0); + std::vector merge_error_messages(num_t); + std::vector thread_greatest_v_idx(num_t, -1); + std::vector thread_greatest_vn_idx(num_t, -1); + std::vector thread_greatest_vt_idx(num_t, -1); + std::vector thread_saw_any_vertex_color(num_t, 0); + std::vector thread_missing_vertex_color(num_t, 0); + + auto merge_vertex_thread = [&](size_t t) { + const OptThreadData &td = thread_data[t]; + // Bulk copy positions + if (!td.v_pos.empty()) { + std::memcpy(&attrib.vertices[v_off[t] * 3], td.v_pos.data(), + td.v_pos.size() * sizeof(real_t)); + } + // Copy weights + if (any_weight && td.saw_any_weight) { + for (size_t i = 0; i < td.num_v; i++) + attrib.vertex_weights[v_off[t] + i] = td.v_weight[i]; + } + // Copy colors + if (any_color) { + if (td.saw_any_color) { + std::memcpy(&all_colors[v_off[t] * 3], td.v_color.data(), + td.v_color.size() * sizeof(real_t)); + thread_saw_any_vertex_color[t] = 1; + if (td.saw_missing_color) thread_missing_vertex_color[t] = 1; + } else if (td.saw_missing_color) { + thread_missing_vertex_color[t] = 1; + } + } + // Bulk copy normals + if (!td.vn_data.empty()) { + std::memcpy(&attrib.normals[n_off[t] * 3], td.vn_data.data(), + td.vn_data.size() * sizeof(real_t)); + } + // Bulk copy texcoords + if (!td.vt_data.empty()) { + std::memcpy(&attrib.texcoords[t_off[t] * 2], td.vt_data.data(), + td.vt_data.size() * sizeof(real_t)); + } + if (any_texcoord_w && td.saw_any_texcoord_w) { + for (size_t i = 0; i < td.num_vt; i++) + attrib.texcoord_ws[t_off[t] + i] = td.vt_w[i]; + } + }; + + auto merge_thread = [&](size_t t) { + const OptThreadData &td = thread_data[t]; + size_t fc = f_off[t], fcc = face_off[t]; + int current_mat_id = initial_material_id[t]; + unsigned int current_smoothing_id = initial_smoothing_group_id[t]; + int greatest_v_idx = -1; + int greatest_vn_idx = -1; + int greatest_vt_idx = -1; + + // Reusable scratch buffer for resolved face indices + index_t resolved_inline[8]; + std::vector resolved_heap; + + for (size_t si = 0; si < td.seq.size(); si++) { + const OptSeqEntry &entry = td.seq[si]; + if (entry.kind == OptSeqEntry::SEQ_META) { + const OptMetaCmd &meta = td.metas[entry.index]; + if (meta.type == OPT_CMD_USEMTL) + current_mat_id = meta.resolved_material_id; + else if (meta.type == OPT_CMD_S) + current_smoothing_id = meta.smoothing_group_id; + continue; + } + // SEQ_FACE + const OptFaceCmd &face = td.faces[entry.index]; + if (face.degenerate) { + command_written_faces[t][entry.index] = 0; + continue; + } + // Compute running counts for index resolution + size_t vc = v_off[t] + face.v_count_before; + size_t nc = n_off[t] + face.vn_count_before; + size_t tc = t_off[t] + face.vt_count_before; + + // Use stack buffer for typical faces, heap only for large polygons + index_t *resolved_face; + if (face.face_vertex_count <= 8) { + resolved_face = resolved_inline; + } else { + resolved_heap.resize(face.face_vertex_count); + resolved_face = resolved_heap.data(); + } + for (size_t k = 0; k < face.face_vertex_count; k++) { + const opt_index_t &vi = face.face_indices()[k]; + index_t idx; + if (!opt_resolveIndexLikeLegacy(vi.vertex_index, + static_cast(vc), + &idx.vertex_index, false)) { + merge_error_lines[t] = face.source_line; + merge_error_messages[t] = + "failed to parse `f' line (invalid vertex index)"; + return; + } + opt_updateGreatestIndex(idx.vertex_index, &greatest_v_idx); + if (vi.texcoord_index == opt_index_t::kNotPresent) { + idx.texcoord_index = -1; + } else { + if (!opt_resolveIndexLikeLegacy(vi.texcoord_index, + static_cast(tc), + &idx.texcoord_index, true)) { + merge_error_lines[t] = face.source_line; + merge_error_messages[t] = + "failed to parse `f' line (invalid vertex index)"; + return; + } + if (idx.texcoord_index >= 0) + opt_updateGreatestIndex(idx.texcoord_index, &greatest_vt_idx); + } + if (vi.normal_index == opt_index_t::kNotPresent) { + idx.normal_index = -1; + } else { + if (!opt_resolveIndexLikeLegacy(vi.normal_index, + static_cast(nc), + &idx.normal_index, true)) { + merge_error_lines[t] = face.source_line; + merge_error_messages[t] = + "failed to parse `f' line (invalid vertex index)"; + return; + } + if (idx.normal_index >= 0) + opt_updateGreatestIndex(idx.normal_index, &greatest_vn_idx); + } + resolved_face[k] = idx; + } + size_t written_index_count = 0; + size_t written_face_count = 0; + if (config.triangulate) { + written_index_count = opt_triangulate_face( + attrib.vertices.data(), attrib.vertices.size(), + resolved_face, face.face_vertex_count, + &attrib.indices[fc]); + written_face_count = written_index_count / 3; + } else { + written_index_count = face.face_vertex_count; + written_face_count = 1; + for (size_t k = 0; k < face.face_vertex_count; k++) { + attrib.indices[fc + k] = resolved_face[k]; + } + } + for (size_t k = 0; k < written_face_count; k++) { + attrib.face_num_verts[fcc + k] = config.triangulate + ? 3 + : static_cast(face.emitted_face_verts); + attrib.material_ids[fcc + k] = current_mat_id; + if (any_smoothing) { + all_smoothing_group_ids[fcc + k] = current_smoothing_id; + } + } + command_written_faces[t][entry.index] = + static_cast(written_face_count); + fc += written_index_count; + fcc += written_face_count; + } + written_index_counts[t] = fc - f_off[t]; + written_face_counts[t] = fcc - face_off[t]; + thread_greatest_v_idx[t] = greatest_v_idx; + thread_greatest_vn_idx[t] = greatest_vn_idx; + thread_greatest_vt_idx[t] = greatest_vt_idx; + }; + +#ifdef TINYOBJLOADER_USE_MULTITHREADING + if (num_threads > 1) { + std::vector workers; + workers.reserve(num_t); + for (size_t t = 0; t < num_t; t++) { + workers.emplace_back([&, t]() { merge_vertex_thread(t); }); + } + for (auto &w : workers) w.join(); + workers.clear(); + for (size_t t = 0; t < num_t; t++) { + workers.emplace_back([&, t]() { merge_thread(t); }); + } + for (auto &w : workers) w.join(); + } else { + for (size_t t = 0; t < num_t; t++) merge_vertex_thread(t); + for (size_t t = 0; t < num_t; t++) merge_thread(t); + } +#else + for (size_t t = 0; t < num_t; t++) merge_vertex_thread(t); + for (size_t t = 0; t < num_t; t++) merge_thread(t); +#endif + + // Check merge errors + size_t first_merge_error_line = 0; + std::string first_merge_error_message; + for (size_t t = 0; t < merge_error_lines.size(); t++) { + if (merge_error_lines[t] == 0) continue; + if (first_merge_error_line == 0 || + merge_error_lines[t] < first_merge_error_line) { + first_merge_error_line = merge_error_lines[t]; + first_merge_error_message = merge_error_messages[t]; + } + } + + if (first_merge_error_line != 0) { + if (err) { + (*err) += "Failed parse line(line " + + std::to_string(first_merge_error_line) + "). " + + first_merge_error_message + "\n"; + } + return false; + } + + // Handle vertex colors: only keep if all vertices have color + { + bool saw_any = false, saw_missing = false; + for (size_t t = 0; t < num_t; t++) { + saw_any = saw_any || (thread_saw_any_vertex_color[t] != 0); + saw_missing = saw_missing || (thread_missing_vertex_color[t] != 0); + } + if (saw_any && !saw_missing) { + attrib.colors = all_colors; // share arena pointer + } + // else attrib.colors stays empty (default) + } + + // Compute actual sizes and compact/truncate + size_t actual_num_indices = 0; + size_t actual_num_faces = 0; + int greatest_v_idx = -1, greatest_vn_idx = -1, greatest_vt_idx = -1; + for (size_t t = 0; t < num_t; t++) { + actual_num_indices += written_index_counts[t]; + actual_num_faces += written_face_counts[t]; + opt_updateGreatestIndex(thread_greatest_v_idx[t], &greatest_v_idx); + opt_updateGreatestIndex(thread_greatest_vn_idx[t], &greatest_vn_idx); + opt_updateGreatestIndex(thread_greatest_vt_idx[t], &greatest_vt_idx); + } + + // Compact face arrays to remove gaps left by threads that wrote fewer + // indices/faces than pre-allocated (e.g. triangulation returning 0 for + // faces with out-of-bounds vertex indices). + if (num_t > 1) { + size_t dst_idx = 0; + size_t dst_face = 0; + for (size_t t = 0; t < num_t; t++) { + size_t src_idx = f_off[t]; + size_t src_face = face_off[t]; + size_t idx_count = written_index_counts[t]; + size_t fc_count = written_face_counts[t]; + if (dst_idx != src_idx && idx_count > 0) { + std::memmove(&attrib.indices[dst_idx], &attrib.indices[src_idx], + idx_count * sizeof(index_t)); + } + if (dst_face != src_face && fc_count > 0) { + std::memmove(&attrib.face_num_verts[dst_face], + &attrib.face_num_verts[src_face], + fc_count * sizeof(int)); + std::memmove(&attrib.material_ids[dst_face], + &attrib.material_ids[src_face], + fc_count * sizeof(int)); + if (any_smoothing) { + std::memmove(&all_smoothing_group_ids[dst_face], + &all_smoothing_group_ids[src_face], + fc_count * sizeof(unsigned int)); + } + } + dst_idx += idx_count; + dst_face += fc_count; + } + } + + attrib.indices.truncate(actual_num_indices); + attrib.face_num_verts.truncate(actual_num_faces); + attrib.material_ids.truncate(actual_num_faces); + if (any_smoothing) { + all_smoothing_group_ids.truncate(actual_num_faces); + attrib.smoothing_group_ids = all_smoothing_group_ids; + } + + opt_appendOutOfBoundsWarnings( + warn, greatest_v_idx, greatest_vn_idx, greatest_vt_idx, + static_cast(attrib.vertices.size() / 3), + static_cast(attrib.normals.size() / 3), + static_cast(attrib.texcoords.size() / 2), + all_line_infos.size() + 1); + for (size_t deg = 0; deg < eof_pending_degenerate_faces; deg++) { + if (warn) (*warn) += "Degenerated face found\n."; + } + + // ---- Phase 5: construct shape ranges (views, no copies) ---- + { + const size_t total_faces = attrib.face_num_verts.size(); + // Build prefix-sum for index offsets + TypedArray idx_prefix; + idx_prefix.allocate(arena, total_faces + 1); + idx_prefix[0] = 0; + for (size_t fi = 0; fi < total_faces; fi++) { + idx_prefix[fi + 1] = + idx_prefix[fi] + static_cast(attrib.face_num_verts[fi]); + } + + size_t face_count = 0; + std::string current_name; + size_t face_prev_offset = 0; + bool have_active_shape_name = false; + + auto emit_shape = [&](const std::string &name, size_t f_start, size_t f_end) { + if (f_end <= f_start) return; + OptShapeRange sr; + sr.name = name; + sr.face_offset = f_start; + sr.face_count = f_end - f_start; + sr.index_offset = idx_prefix[f_start]; + sr.index_count = idx_prefix[f_end] - idx_prefix[f_start]; + result->shapes.push_back(std::move(sr)); + }; + + for (size_t t = 0; t < num_t; t++) { + const OptThreadData &td = thread_data[t]; + for (size_t si = 0; si < td.seq.size(); si++) { + const OptSeqEntry &entry = td.seq[si]; + if (entry.kind == OptSeqEntry::SEQ_META) { + const OptMetaCmd &meta = td.metas[entry.index]; + if (meta.type == OPT_CMD_O || meta.type == OPT_CMD_G) { + std::string name; + if (meta.type == OPT_CMD_O && meta.str_ptr) { + name.assign(meta.str_ptr, meta.str_len); + } else if (!meta.str_storage.empty()) { + name = meta.str_storage; + } else if (meta.str_ptr) { + name.assign(meta.str_ptr, meta.str_len); + } + while (!name.empty() && + (name.back() == '\r' || name.back() == '\n')) + name.pop_back(); + + if (face_count == 0) { + current_name = name; + face_prev_offset = 0; + have_active_shape_name = true; + } else { + if (!have_active_shape_name) { + emit_shape(std::string(), 0, face_count); + } else if (face_count > face_prev_offset) { + emit_shape(current_name, face_prev_offset, face_count); + } + current_name = name; + face_prev_offset = face_count; + have_active_shape_name = true; + } + } + } else { + // SEQ_FACE + face_count += command_written_faces[t][entry.index]; + } + } + } + + // Final shape + if (face_count > face_prev_offset || face_count == 0) { + emit_shape(current_name, face_prev_offset, face_count); + } + } + + result->valid = true; + return true; +} + +// ---- LoadObjOptTyped (buffer version, public API) ---- + +OptResult LoadObjOptTyped(const char *buf, size_t buf_len, + std::string *warn, std::string *err, + const OptLoadConfig &config) { + OptResult result; + LoadObjOptTyped_internal(&result, warn, err, buf, buf_len, + std::string(), "", false, config); + return result; +} + +// ---- LoadObjOptTyped (file version) ---- + +OptResult LoadObjOptTyped(const char *filename, + std::string *warn, std::string *err, + const char *mtl_basedir, + const OptLoadConfig &config) { + OptResult result; + if (!filename) { + if (err) *err = "filename is null."; + return result; + } + + std::string filepath(filename); + std::string baseDir; + if (mtl_basedir) { + baseDir = mtl_basedir; + } else { + size_t pos = filepath.find_last_of("/\\"); + if (pos != std::string::npos) { + baseDir = filepath.substr(0, pos + 1); + } + } + if (!baseDir.empty()) { +#ifndef _WIN32 + const char dirsep = '/'; +#else + const char dirsep = '\\'; +#endif + if (baseDir[baseDir.length() - 1] != dirsep) baseDir += dirsep; + } + +#ifdef TINYOBJLOADER_USE_MMAP + { + MappedFile mf; + if (mf.open(filepath.c_str())) { + LoadObjOptTyped_internal(&result, warn, err, mf.data, mf.size, + baseDir, filepath, true, config); + return result; + } + } +#endif + +#ifdef _WIN32 + std::ifstream ifs(LongPathW(UTF8ToWchar(filepath)).c_str(), + std::ios::binary | std::ios::ate); +#else + std::ifstream ifs(filepath.c_str(), std::ios::binary | std::ios::ate); +#endif + if (!ifs.is_open()) { + if (err) *err = "Cannot open file: " + filepath; + return result; + } + + std::streamsize fsize = ifs.tellg(); + ifs.seekg(0, std::ios::beg); + + if (fsize <= 0) { + LoadObjOptTyped_internal(&result, warn, err, "", 0, + baseDir, filepath, true, config); + return result; + } + + std::vector file_buf(static_cast(fsize)); + if (!ifs.read(file_buf.data(), fsize)) { + if (err) *err = "Failed to read file: " + filepath; + return result; + } + ifs.close(); + + LoadObjOptTyped_internal(&result, warn, err, file_buf.data(), + static_cast(fsize), baseDir, filepath, + true, config); + return result; +} + #ifdef __clang__ #pragma clang diagnostic pop #endif From ed77593e5bed9df3dd62ef1aa0a0f704bd91d847 Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Fri, 22 May 2026 04:45:47 +0900 Subject: [PATCH 2/6] Harden optimized/stream parser per Copilot review Address the three review comments from Copilot on the take-over PR, plus a related robustness fix uncovered while doing so. - experimental/stream: fix `v x y z w r g b` (7-component) vertex parsing. The stream loader previously treated the weight `w` as red and shifted the colors. It now mirrors LoadObjOpt, keyed on the component count beyond xyz: +1 -> weight, +3 -> color (weight = r, legacy compat), +4 -> weight + color. - tests/tester.cc: include the canonical experimental/stream/stream_obj_loader.h instead of re-declaring StreamLoadConfig / LoadObjStreamExperimental, so the test cannot drift from the real API. - tests/tester.cc: make test_arena_adapter_overflow_guard conditional on TINYOBJLOADER_ENABLE_EXCEPTION (allocate() throws std::bad_alloc when exceptions are enabled, returns nullptr otherwise) so the test no longer terminates when built with exceptions. - tiny_obj_loader.h: guard the IMPLEMENTATION block against double inclusion within a single translation unit (TINYOBJLOADER_IMPLEMENTATION_DEFINED). Including the stream header after defining TINYOBJLOADER_IMPLEMENTATION now pulls in tiny_obj_loader.h a second time; without the guard this caused redefinition errors. - Add test_stream_loader_weighted_color_vertex_7_components regression test. Verified: `make check` passes under clang++ and g++, both with and without -DTINYOBJLOADER_ENABLE_EXCEPTION. Co-Authored-By: Claude Opus 4.7 (1M context) --- experimental/stream/stream_obj_loader.cc | 58 ++++++++++++++---------- tests/opt/loadobjopt_multithread.inc | 32 +++++++++++++ tests/tester.cc | 47 ++++++++----------- tiny_obj_loader.h | 7 ++- 4 files changed, 91 insertions(+), 53 deletions(-) diff --git a/experimental/stream/stream_obj_loader.cc b/experimental/stream/stream_obj_loader.cc index 7b4495b2..bb72fb8b 100644 --- a/experimental/stream/stream_obj_loader.cc +++ b/experimental/stream/stream_obj_loader.cc @@ -791,29 +791,41 @@ static bool ParseLineToEvent(size_t line_num, const std::string &line, return false; } - if (tokens.size() >= 4) { - real_t maybe_r = real_t(1.0); - if (ParseRealToken(tokens[3], &maybe_r)) { - if (tokens.size() == 4) { - event.has_vertex_weight = true; - event.vertex_weight = maybe_r; - } else { - real_t maybe_g = real_t(1.0); - if (!ParseRealToken(tokens[4], &maybe_g)) { - event.has_vertex_weight = true; - event.vertex_weight = maybe_r; - } else if (tokens.size() >= 6) { - real_t maybe_b = real_t(1.0); - if (ParseRealToken(tokens[5], &maybe_b)) { - event.has_vertex_weight = true; - event.vertex_weight = maybe_r; - event.has_color = true; - event.r = maybe_r; - event.g = maybe_g; - event.b = maybe_b; - } - } - } + // Match the legacy/opt loaders' weight + color handling, keyed on the + // number of components beyond `x y z`: + // +0 (v x y z) -> position only + // +1 (v x y z w) -> weight only + // +3 (v x y z r g b) -> color, weight = r (legacy compat) + // +4+ (v x y z w r g b)-> weight + color + const size_t extra = tokens.size() - 3; + if (extra == 1) { + real_t w = real_t(1.0); + if (ParseRealToken(tokens[3], &w)) { + event.has_vertex_weight = true; + event.vertex_weight = w; + } + } else if (extra == 3) { + real_t cr = real_t(1.0), cg = real_t(1.0), cb = real_t(1.0); + if (ParseRealToken(tokens[3], &cr) && ParseRealToken(tokens[4], &cg) && + ParseRealToken(tokens[5], &cb)) { + event.has_vertex_weight = true; + event.vertex_weight = cr; + event.has_color = true; + event.r = cr; + event.g = cg; + event.b = cb; + } + } else if (extra >= 4) { + real_t w = real_t(1.0), cr = real_t(1.0), cg = real_t(1.0), + cb = real_t(1.0); + if (ParseRealToken(tokens[3], &w) && ParseRealToken(tokens[4], &cr) && + ParseRealToken(tokens[5], &cg) && ParseRealToken(tokens[6], &cb)) { + event.has_vertex_weight = true; + event.vertex_weight = w; + event.has_color = true; + event.r = cr; + event.g = cg; + event.b = cb; } } diff --git a/tests/opt/loadobjopt_multithread.inc b/tests/opt/loadobjopt_multithread.inc index 43219c6b..7fc6f8f7 100644 --- a/tests/opt/loadobjopt_multithread.inc +++ b/tests/opt/loadobjopt_multithread.inc @@ -1722,6 +1722,38 @@ void test_stream_loader_colored_vertex_weight() { TEST_CHECK(legacy_attrib.colors == stream_attrib.colors); } +// `v x y z w r g b` (7 components) -> weight = w, color = (r, g, b). +// The legacy istream loader has a long-standing quirk here (it stops at 6 +// scalars, so it treats the 4th value as both weight and red and drops the +// 7th); the stream loader instead follows the corrected LoadObjOpt semantics. +void test_stream_loader_weighted_color_vertex_7_components() { + const std::string obj_text = + "v 0.0 0.0 0.0 0.5 0.1 0.2 0.3\n" + "v 1.0 0.0 0.0 0.6 0.4 0.5 0.6\n"; + + tinyobj::attrib_t stream_attrib; + std::vector stream_shapes; + std::vector stream_materials; + std::string stream_warn, stream_err; + std::istringstream stream(obj_text); + tinyobj::experimental_stream::StreamLoadConfig config; + bool stream_ok = tinyobj::experimental_stream::LoadObjStreamExperimental( + &stream_attrib, &stream_shapes, &stream_materials, &stream_warn, + &stream_err, &stream, NULL, config); + + TEST_CHECK(stream_ok == true); + TEST_CHECK(stream_attrib.vertex_weights.size() == 2); + TEST_CHECK(FloatEquals(0.5f, stream_attrib.vertex_weights[0])); + TEST_CHECK(FloatEquals(0.6f, stream_attrib.vertex_weights[1])); + TEST_CHECK(stream_attrib.colors.size() == 6); + TEST_CHECK(FloatEquals(0.1f, stream_attrib.colors[0])); + TEST_CHECK(FloatEquals(0.2f, stream_attrib.colors[1])); + TEST_CHECK(FloatEquals(0.3f, stream_attrib.colors[2])); + TEST_CHECK(FloatEquals(0.4f, stream_attrib.colors[3])); + TEST_CHECK(FloatEquals(0.5f, stream_attrib.colors[4])); + TEST_CHECK(FloatEquals(0.6f, stream_attrib.colors[5])); +} + void test_stream_loader_group_comment_matches_legacy() { const std::string obj_text = opt_test::BuildGroupCommentObj(); diff --git a/tests/tester.cc b/tests/tester.cc index f3ba3a4a..cadf1b4f 100644 --- a/tests/tester.cc +++ b/tests/tester.cc @@ -39,34 +39,9 @@ #include #include -namespace tinyobj { -namespace experimental_stream { -struct StreamLoadConfig { - bool triangulate; - bool default_vcols_fallback; - int num_threads; - size_t chunk_line_count; - - StreamLoadConfig() - : triangulate(true), - default_vcols_fallback(false), - num_threads(1), - chunk_line_count(4096) {} -}; - -bool LoadObjStreamExperimental( - attrib_t *attrib, std::vector *shapes, - std::vector *materials, std::string *warn, std::string *err, - std::istream *input, MaterialReader *readMatFn, - const StreamLoadConfig &config = StreamLoadConfig()); - -bool LoadObjStreamExperimental( - attrib_t *attrib, std::vector *shapes, - std::vector *materials, std::string *warn, std::string *err, - const char *filename, const char *mtl_basedir, - const StreamLoadConfig &config = StreamLoadConfig()); -} // namespace experimental_stream -} // namespace tinyobj +// Pull in the canonical experimental stream loader API instead of re-declaring +// it here, so this test cannot silently drift from the real header. +#include "../experimental/stream/stream_obj_loader.h" #ifdef _WIN32 #include // _mkdir @@ -4159,8 +4134,20 @@ void test_arena_adapter_overflow_guard() { tinyobj::arena_adapter adapter(&arena); // Request an allocation that would overflow size_t when multiplied by // sizeof(double)=8. SIZE_MAX / 8 + 1 overflows. - double *p = adapter.allocate(SIZE_MAX / sizeof(double) + 1); + const size_t overflow_n = SIZE_MAX / sizeof(double) + 1; +#ifdef TINYOBJLOADER_ENABLE_EXCEPTION + bool threw = false; + try { + double *p = adapter.allocate(overflow_n); + (void)p; + } catch (const std::bad_alloc &) { + threw = true; + } + TEST_CHECK(threw); +#else + double *p = adapter.allocate(overflow_n); TEST_CHECK(p == nullptr); +#endif } void test_loadobjopt_object_name_trimming() { @@ -4384,6 +4371,8 @@ TEST_LIST = { test_stream_loader_vertex_weight}, {"test_stream_loader_colored_vertex_weight", test_stream_loader_colored_vertex_weight}, + {"test_stream_loader_weighted_color_vertex_7_components", + test_stream_loader_weighted_color_vertex_7_components}, {"test_stream_loader_group_comment_matches_legacy", test_stream_loader_group_comment_matches_legacy}, {"test_stream_loader_weighted_vertex_comment_matches_legacy", diff --git a/tiny_obj_loader.h b/tiny_obj_loader.h index e2d0d191..72aa87ae 100644 --- a/tiny_obj_loader.h +++ b/tiny_obj_loader.h @@ -1188,7 +1188,12 @@ OptResult LoadObjOptTyped(const char *filename, #endif // TINY_OBJ_LOADER_H_ -#ifdef TINYOBJLOADER_IMPLEMENTATION +// Guard the implementation against double inclusion within a single +// translation unit (e.g. when another header that also `#include`s this file +// is pulled in after TINYOBJLOADER_IMPLEMENTATION is defined). +#if defined(TINYOBJLOADER_IMPLEMENTATION) && \ + !defined(TINYOBJLOADER_IMPLEMENTATION_DEFINED) +#define TINYOBJLOADER_IMPLEMENTATION_DEFINED #include #include #include From 253a659c7ac9e4a63b6dab9fbc06f77617ab24fa Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Fri, 22 May 2026 04:57:05 +0900 Subject: [PATCH 3/6] Document 7-component vertex (v x y z w r g b) as a non-standard extension Web research against the official Wavefront spec, Paul Bourke's vertex-color reference, and Wikipedia confirms there is no standard 7-component `v` line: the format defines only xyz / xyzw / xyzrgb, and weight and color are mutually exclusive. Per maintainer decision, keep the weight + color interpretation (4th value = weight, next three = RGB) in LoadObjOpt and the experimental stream loader, but document clearly that it is a tinyobjloader-specific extension and that the classic LoadObj / LoadObjWithCallback path instead caps at 6 components, so the loaders intentionally diverge for 7+ token vertices. Comment-only change; no behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) --- experimental/stream/stream_obj_loader.cc | 8 ++++++-- tests/opt/loadobjopt_multithread.inc | 8 +++++--- tiny_obj_loader.h | 21 +++++++++++++++++++-- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/experimental/stream/stream_obj_loader.cc b/experimental/stream/stream_obj_loader.cc index bb72fb8b..c152312c 100644 --- a/experimental/stream/stream_obj_loader.cc +++ b/experimental/stream/stream_obj_loader.cc @@ -791,12 +791,16 @@ static bool ParseLineToEvent(size_t line_num, const std::string &line, return false; } - // Match the legacy/opt loaders' weight + color handling, keyed on the - // number of components beyond `x y z`: + // Mirror LoadObjOpt's weight + color handling, keyed on the number of + // components beyond `x y z`: // +0 (v x y z) -> position only // +1 (v x y z w) -> weight only // +3 (v x y z r g b) -> color, weight = r (legacy compat) // +4+ (v x y z w r g b)-> weight + color + // The OBJ spec / common vertex-color extension only define xyz / xyzw / + // xyzrgb (weight and color are mutually exclusive); the +4 case is a + // tinyobjloader extension shared with LoadObjOpt. The classic LoadObj + // path caps at 6 and differs here. const size_t extra = tokens.size() - 3; if (extra == 1) { real_t w = real_t(1.0); diff --git a/tests/opt/loadobjopt_multithread.inc b/tests/opt/loadobjopt_multithread.inc index 7fc6f8f7..f3e91742 100644 --- a/tests/opt/loadobjopt_multithread.inc +++ b/tests/opt/loadobjopt_multithread.inc @@ -1723,9 +1723,11 @@ void test_stream_loader_colored_vertex_weight() { } // `v x y z w r g b` (7 components) -> weight = w, color = (r, g, b). -// The legacy istream loader has a long-standing quirk here (it stops at 6 -// scalars, so it treats the 4th value as both weight and red and drops the -// 7th); the stream loader instead follows the corrected LoadObjOpt semantics. +// 7-component vertices are not part of the OBJ spec or the common vertex-color +// extension (which define only xyz / xyzw / xyzrgb); tinyobjloader accepts them +// as an extension in LoadObjOpt and the stream loader (4th value = weight, next +// three = RGB). The classic LoadObj path caps at 6, so we assert explicit +// values here rather than comparing against it. void test_stream_loader_weighted_color_vertex_7_components() { const std::string obj_text = "v 0.0 0.0 0.0 0.5 0.1 0.2 0.3\n" diff --git a/tiny_obj_loader.h b/tiny_obj_loader.h index 72aa87ae..e7285fa5 100644 --- a/tiny_obj_loader.h +++ b/tiny_obj_loader.h @@ -6797,6 +6797,11 @@ static inline void parseV(real_t *x, real_t *y, real_t *z, real_t *w, // Extension: parse vertex with colors(6 items) // Return 3: xyz, 4: xyzw, 6: xyzrgb // `r`: red(case 6) or [w](case 4) +// NOTE: This classic path caps at 6 components per the de-facto vertex-color +// extension (xyz / xyzw / xyzrgb; weight and color are mutually exclusive) and +// ignores any further tokens. LoadObjOpt / the experimental stream loader +// additionally accept a non-standard 7-component `v x y z w r g b` (weight + +// color) form, so they diverge from this function for 7+ token vertices. static inline int parseVertexWithColor(real_t *x, real_t *y, real_t *z, real_t *r, real_t *g, real_t *b, const char **token, @@ -10949,7 +10954,13 @@ static bool opt_parseLine(OptCommand *command, const char *p, size_t p_len, command->vc_b = cb; } } else if (extra_components >= 4) { - // v x y z w r g b (7+ total) — weight + color + // v x y z w r g b (7+ total) — weight + color. + // NOTE: 7-component vertices are NOT part of the OBJ spec nor the common + // vertex-color extension, which define only xyz / xyzw / xyzrgb and treat + // weight and color as mutually exclusive. tinyobjloader accepts this as + // an extension: the 4th value is the weight, the next three are RGB. + // (The classic LoadObj / LoadObjWithCallback path instead caps at 6 and + // ignores any extra tokens, so it differs from LoadObjOpt here.) real_t vw = real_t(1.0), cr = r, cg = g, cb = b; const char *wc_cursor = extra_token; if (opt_tryParseFloatToken(&vw, &wc_cursor) && @@ -11727,7 +11738,13 @@ static void opt_parseLineToThreadData( td.v_color[coff+1] = extra[1]; td.v_color[coff+2] = extra[2]; } else if (nextra >= 4) { - // v x y z w r g b (7+ total) — weight + color + // v x y z w r g b (7+ total) — weight + color. + // NOTE: 7-component vertices are NOT part of the OBJ spec nor the common + // vertex-color extension, which define only xyz / xyzw / xyzrgb and treat + // weight and color as mutually exclusive. tinyobjloader accepts this as + // an extension: the 4th value is the weight, the next three are RGB. + // (The classic LoadObj / LoadObjWithCallback path instead caps at 6 and + // ignores any extra tokens, so it differs from LoadObjOpt here.) if (!td.saw_any_weight) { td.saw_any_weight = true; td.v_weight.resize(td.num_v, real_t(1.0)); From 810cfe6950135d8bb9df255dc851ff9f4bbec711 Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Fri, 22 May 2026 05:19:30 +0900 Subject: [PATCH 4/6] Make no-SIMD/no-threading/no-exception the tested default; polish dead code Ensure the library's default configuration (no SIMD, no multithreading, no exceptions) is the baseline that CI exercises, and clean up warnings/dead code introduced by the optimized parser. Default configuration: - tester.cc no longer force-defines TINYOBJLOADER_USE_MULTITHREADING, so the default `make check` build now runs with the library defaults (scalar, single-threaded, exception-free). The header already had no auto-#define of any feature macro, so consumers were already defaulting to all-off; this just makes the test build match. - tests/Makefile builds two binaries: `tester` (defaults) and `tester_features` (TINYOBJLOADER_USE_MULTITHREADING + _USE_SIMD + ENABLE_EXCEPTION). `make check` runs both, so CI covers the default and feature-enabled code paths. New `check_default` / `check_features` targets run a single config. - Document the opt-in macros (all disabled by default) in the header and tests/README. Polish / hardening: - Remove dead function opt_requires_legacy_fallback (a never-called duplicate of opt_line_start_is_legacy). - opt_tryParseDouble: drop the dead inner fast_float branch (unreachable in every configuration) and guard the whole function with TINYOBJLOADER_DISABLE_FAST_FLOAT, since it is only called from that fallback path. Removes two -Wunused-function warnings in the default build. - Guard the now-unused `cache` parameter in opt_parseLineToThreadData under TINYOBJLOADER_DISABLE_FAST_FLOAT (-Wunused-parameter). Verified: default, features, and TINYOBJLOADER_DISABLE_FAST_FLOAT builds all compile (clang++ and g++, C++11 and C++17) and pass the full suite. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/Makefile | 37 +++++++++++++++++++++---- tests/README.md | 9 ++++++ tests/tester.cc | 8 ++++-- tiny_obj_loader.h | 70 ++++++++++------------------------------------- 4 files changed, 60 insertions(+), 64 deletions(-) diff --git a/tests/Makefile b/tests/Makefile index 7427352c..57cccf61 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,13 +1,32 @@ -.PHONY: clean +.PHONY: clean check check_default check_features all obj-fuzz llvm-fuzz CXX ?= clang++ CXXFLAGS ?= -g -O1 EXTRA_CXXFLAGS ?= -std=c++11 -fsanitize=address -tester: tester.cc ../tiny_obj_loader.h ../experimental/stream/stream_obj_loader.cc ../experimental/stream/stream_obj_loader.h opt/loadobjopt_multithread.inc - $(CXX) $(CXXFLAGS) $(EXTRA_CXXFLAGS) -pthread -I.. -o tester tester.cc ../experimental/stream/stream_obj_loader.cc +# Optional feature macros exercised by the `tester_features` build. +FEATURE_FLAGS = -DTINYOBJLOADER_USE_MULTITHREADING \ + -DTINYOBJLOADER_USE_SIMD \ + -DTINYOBJLOADER_ENABLE_EXCEPTION -all: tester +SRCS = tester.cc ../experimental/stream/stream_obj_loader.cc +DEPS = tester.cc ../tiny_obj_loader.h \ + ../experimental/stream/stream_obj_loader.cc \ + ../experimental/stream/stream_obj_loader.h \ + opt/loadobjopt_multithread.inc + +# Default build: library defaults — no SIMD, no multithreading, no exceptions. +# (-pthread is required only because the experimental stream loader links +# std::thread; the library's own threading stays disabled.) +tester: $(DEPS) + $(CXX) $(CXXFLAGS) $(EXTRA_CXXFLAGS) -pthread -I.. -o tester $(SRCS) + +# Feature build: exercise the optional multithreading / SIMD / exception paths. +tester_features: $(DEPS) + $(CXX) $(CXXFLAGS) $(EXTRA_CXXFLAGS) $(FEATURE_FLAGS) -pthread -I.. \ + -o tester_features $(SRCS) + +all: tester tester_features obj-fuzz: $(MAKE) -C obj-fuzz @@ -15,10 +34,16 @@ obj-fuzz: llvm-fuzz: $(MAKE) -C llvm-fuzz -check: tester +# Run both configurations so CI covers the default and feature-enabled paths. +check: check_default check_features + +check_default: tester ./tester +check_features: tester_features + ./tester_features + clean: - rm -rf tester + rm -rf tester tester_features $(MAKE) -C obj-fuzz clean $(MAKE) -C llvm-fuzz clean diff --git a/tests/README.md b/tests/README.md index 7c76a523..b95dca0b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -4,6 +4,15 @@ $ make check +`make check` builds and runs the suite in two configurations: + +* `check_default` — library defaults: no SIMD, no multithreading, no + exceptions. +* `check_features` — `TINYOBJLOADER_USE_MULTITHREADING`, + `TINYOBJLOADER_USE_SIMD`, and `TINYOBJLOADER_ENABLE_EXCEPTION` all enabled. + +Run a single configuration with `make check_default` or `make check_features`. + Additional fuzz targets: $ make obj-fuzz diff --git a/tests/tester.cc b/tests/tester.cc index cadf1b4f..6152f3d0 100644 --- a/tests/tester.cc +++ b/tests/tester.cc @@ -1,6 +1,8 @@ -#ifndef TINYOBJLOADER_USE_MULTITHREADING -#define TINYOBJLOADER_USE_MULTITHREADING -#endif +// NOTE: Do not force-enable TINYOBJLOADER_USE_MULTITHREADING / _USE_SIMD / +// ENABLE_EXCEPTION here. The default `make check` build exercises the library +// defaults (no SIMD, no multithreading, no exceptions); the Makefile's +// `tester_features` target re-builds this file with those macros defined to +// cover the optional code paths. #ifndef TINYOBJLOADER_IMPLEMENTATION #define TINYOBJLOADER_IMPLEMENTATION #endif diff --git a/tiny_obj_loader.h b/tiny_obj_loader.h index e7285fa5..c2add9d6 100644 --- a/tiny_obj_loader.h +++ b/tiny_obj_loader.h @@ -746,11 +746,16 @@ bool ParseTextureNameAndOption(std::string *texname, texture_option_t *texopt, /// ==>>========= Optimized API (C++11 required) ============================ /// -/// Enable compile options: +/// Optional compile options (all DISABLED by default — define the macro to +/// opt in): /// TINYOBJLOADER_USE_MULTITHREADING - multi-threaded parsing /// TINYOBJLOADER_USE_SIMD - SIMD-accelerated line scanning +/// (SSE2/AVX2 on x86, NEON on ARM) +/// TINYOBJLOADER_ENABLE_EXCEPTION - throw std::bad_alloc on allocation +/// failure instead of returning nullptr /// -/// These features require C++11 or later. +/// With none of these defined, the parser runs single-threaded, scalar, and +/// exception-free. These features require C++11 or later. /// /// @@ -10098,40 +10103,14 @@ static inline void opt_appendOutOfBoundsWarnings(std::string *warn, } } +// Hand-written fallback double parser. Compiled only when fast_float is +// disabled (TINYOBJLOADER_DISABLE_FAST_FLOAT); otherwise callers use +// fast_float::from_chars directly and this function is not needed. +#ifdef TINYOBJLOADER_DISABLE_FAST_FLOAT static bool opt_tryParseDouble(const char *s, const char *s_end, double *result) { if (s >= s_end) return false; -#ifndef TINYOBJLOADER_DISABLE_FAST_FLOAT - // Use the already-embedded fast_float for high-performance parsing. - // Handle nan/inf with OBJ-compatible replacement values first. - { - const char *p = s; - if (p < s_end && (*p == '+' || *p == '-')) ++p; - if (p < s_end) { - char fc = *p; - if (fc >= 'A' && fc <= 'Z') fc += 32; - if (fc == 'n' || fc == 'i') { - const char *end_ptr; - if (detail_fp::tryParseNanInf(s, s_end, result, &end_ptr)) { - return true; - } - } - } - } - - double tmp; - auto r = fast_float::from_chars(s, s_end, tmp, - fast_float::chars_format::general | - fast_float::chars_format::allow_leading_plus); - if (r.ec == tinyobj_ff::ff_errc::ok) { - *result = tmp; - return true; - } - return false; -#else - // Fallback: hand-written float parser - // Handle nan/inf keywords with OBJ-compatible replacement values. { const char *p = s; @@ -10249,8 +10228,8 @@ static bool opt_tryParseDouble(const char *s, const char *s_end, (exponent ? std::ldexp(mantissa * std::pow(5.0, exponent), exponent) : mantissa); return true; -#endif // TINYOBJLOADER_DISABLE_FAST_FLOAT } +#endif // TINYOBJLOADER_DISABLE_FAST_FLOAT struct opt_index_t { int vertex_index, texcoord_index, normal_index; @@ -11426,27 +11405,6 @@ static void ConvertLegacyResultToBasic( } } -static bool opt_requires_legacy_fallback(const char *p, size_t len) { - if (!p || len == 0) return false; - while (len > 0 && (*p == ' ' || *p == '\t')) { - p++; - len--; - } - if (len == 0 || *p == '#' || *p == '\r' || *p == '\n') return false; - - if (len >= 3 && p[0] == 'v' && p[1] == 'w' && - (p[2] == ' ' || p[2] == '\t')) { - return true; - } - if (len >= 2 && - ((p[0] == 'l' && (p[1] == ' ' || p[1] == '\t')) || - (p[0] == 'p' && (p[1] == ' ' || p[1] == '\t')) || - (p[0] == 't' && (p[1] == ' ' || p[1] == '\t')))) { - return true; - } - return false; -} - /// Check if the first non-whitespace token on a line (starting at p, length /// rem) requires the legacy parser. static inline bool opt_line_start_is_legacy(const char *p, size_t rem) { @@ -11681,7 +11639,9 @@ static void opt_parseLineToThreadData( if (*token == '\n' || *token == '\r' || *token == '#' || *token == '\0') return; -#ifndef TINYOBJLOADER_DISABLE_FAST_FLOAT +#ifdef TINYOBJLOADER_DISABLE_FAST_FLOAT + (void)cache; // only consumed by the fast_float fast path below +#else const char *line_end = line_ptr + line_len; // ---- Fast path: vertex position (v x y z [w] [r g b]) ---- From e0a7d155af3e2d27524e0d17faf6687b8677e385 Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Fri, 22 May 2026 05:44:35 +0900 Subject: [PATCH 5/6] Add TINYOBJLOADER_DISABLE_FAST_FLOAT config to the test matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `make check` now builds and runs a third configuration, `check_nofastfloat`, which defines TINYOBJLOADER_DISABLE_FAST_FLOAT to exercise the hand-written fallback float parser (otherwise library defaults). CI invokes `make check`, so this fallback path — previously only verified by hand — is now covered automatically alongside the default and feature-enabled builds. Verified: all three configs (default, features, nofastfloat) build and pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/Makefile | 21 ++++++++++++++++----- tests/README.md | 7 +++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/Makefile b/tests/Makefile index 57cccf61..a77d0d75 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,4 +1,5 @@ -.PHONY: clean check check_default check_features all obj-fuzz llvm-fuzz +.PHONY: clean check check_default check_features check_nofastfloat all \ + obj-fuzz llvm-fuzz CXX ?= clang++ CXXFLAGS ?= -g -O1 @@ -26,7 +27,13 @@ tester_features: $(DEPS) $(CXX) $(CXXFLAGS) $(EXTRA_CXXFLAGS) $(FEATURE_FLAGS) -pthread -I.. \ -o tester_features $(SRCS) -all: tester tester_features +# fast_float-disabled build: exercise the hand-written fallback float parser +# (TINYOBJLOADER_DISABLE_FAST_FLOAT), otherwise library defaults. +tester_nofastfloat: $(DEPS) + $(CXX) $(CXXFLAGS) $(EXTRA_CXXFLAGS) -DTINYOBJLOADER_DISABLE_FAST_FLOAT \ + -pthread -I.. -o tester_nofastfloat $(SRCS) + +all: tester tester_features tester_nofastfloat obj-fuzz: $(MAKE) -C obj-fuzz @@ -34,8 +41,9 @@ obj-fuzz: llvm-fuzz: $(MAKE) -C llvm-fuzz -# Run both configurations so CI covers the default and feature-enabled paths. -check: check_default check_features +# Run all configurations so CI covers the default, feature-enabled, and +# fast_float-disabled code paths. +check: check_default check_features check_nofastfloat check_default: tester ./tester @@ -43,7 +51,10 @@ check_default: tester check_features: tester_features ./tester_features +check_nofastfloat: tester_nofastfloat + ./tester_nofastfloat + clean: - rm -rf tester tester_features + rm -rf tester tester_features tester_nofastfloat $(MAKE) -C obj-fuzz clean $(MAKE) -C llvm-fuzz clean diff --git a/tests/README.md b/tests/README.md index b95dca0b..e570f48d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -4,14 +4,17 @@ $ make check -`make check` builds and runs the suite in two configurations: +`make check` builds and runs the suite in three configurations: * `check_default` — library defaults: no SIMD, no multithreading, no exceptions. * `check_features` — `TINYOBJLOADER_USE_MULTITHREADING`, `TINYOBJLOADER_USE_SIMD`, and `TINYOBJLOADER_ENABLE_EXCEPTION` all enabled. +* `check_nofastfloat` — `TINYOBJLOADER_DISABLE_FAST_FLOAT` (exercises the + hand-written fallback float parser), otherwise defaults. -Run a single configuration with `make check_default` or `make check_features`. +Run a single configuration with `make check_default`, `make check_features`, +or `make check_nofastfloat`. Additional fuzz targets: From e31c0a1d25c53a49cc21e8d73db811746922fb1f Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Fri, 22 May 2026 06:29:52 +0900 Subject: [PATCH 6/6] README: document the optimized LoadObjOpt loader Replace the stale "Optimized loader" section (which referenced an old experimental loader and v0.9/v1.0 benchmark numbers) with a short usage guide for the in-header LoadObjOpt API: a compilable example, the opt-in multithreading/SIMD macros (off by default), and a note on the arena-backed LoadObjOptTyped variant and the experimental stream loader. Also add a "What's new" entry. Example verified to compile (-Wall -Wextra) and run against models/cornell_box.obj. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 46 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a6e59928..0550c7f7 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Old version is available as `v0.9.x` branch https://github.com/syoyo/tinyobjload ## What's new +* 22 May, 2026 : Added an optimized in-header loader `LoadObjOpt` with optional multithreading/SIMD (see [Optimized loader](#optimized-loader)). * 29 Jul, 2021 : Added Mapbox's earcut for robust triangulation. Also fixes triangulation bug(still there is some issue in built-in triangulation algorithm: https://github.com/tinyobjloader/tinyobjloader/issues/319). * 19 Feb, 2020 : The repository has been moved to https://github.com/tinyobjloader/tinyobjloader ! * 18 May, 2019 : Python binding!(See `python` folder. Also see https://pypi.org/project/tinyobjloader/) @@ -352,16 +353,45 @@ for (size_t s = 0; s < shapes.size(); s++) { ## Optimized loader -Optimized multi-threaded .obj loader is available at `experimental/` directory. -If you want absolute performance to load .obj data, this optimized loader will fit your purpose. -Note that the optimized loader uses C++11 thread and it does less error checks but may work most .obj data. +For large `.obj` files, tinyobjloader ships an optimized in-header loader, +`LoadObjOpt` (C++11 required). It parses the whole buffer in one pass and can +optionally use multiple threads and SIMD line scanning. It does fewer error +checks than the standard loader but handles most real-world `.obj` data. -Here is some benchmark result. Time are measured on MacBook 12(Early 2016, Core m5 1.2GHz). +The result reuses the standard `attrib` / `shapes.mesh.*` layout, so the +iteration code shown above works unchanged. -* Rungholt scene(6M triangles) - * old version(v0.9.x): 15500 msecs. - * baseline(v1.0.x): 6800 msecs(2.3x faster than old version) - * optimised: 1500 msecs(10x faster than old version, 4.5x faster than baseline) +```c++ +#define TINYOBJLOADER_IMPLEMENTATION +// Optional speed-ups (all OFF by default; require C++11): +//#define TINYOBJLOADER_USE_MULTITHREADING // multi-threaded parsing +//#define TINYOBJLOADER_USE_SIMD // SIMD (SSE2/AVX2/NEON) newline scan +#include "tiny_obj_loader.h" + +tinyobj::basic_attrib_t<> attrib; +std::vector> shapes; +std::vector materials; +std::string warn, err; + +tinyobj::OptLoadConfig config; +config.triangulate = true; +config.num_threads = -1; // -1 = hardware_concurrency, 0/1 = single-threaded. + // Effective only with TINYOBJLOADER_USE_MULTITHREADING. + +bool ok = tinyobj::LoadObjOpt(&attrib, &shapes, &materials, &warn, &err, + "large_scene.obj", /* mtl_basedir */ nullptr, + config); +if (!warn.empty()) std::cout << warn; +if (!ok) { std::cerr << err; return -1; } +// attrib.vertices/.normals/.texcoords and shapes[s].mesh.* match the standard +// loader — iterate exactly as in the example above. +``` + +A `LoadObjOptTyped` variant is also available; it returns an `OptResult` whose +arrays are backed by a single arena allocator and only allocates optional +arrays (vertex weights, texcoord `w`, colors, ...) when the input contains +them — handy when minimizing allocations matters. An experimental stream-based +loader lives under `experimental/stream/`. ## Python binding