// M0 T9a/b/c —— 全量解析 + 非退化谓词 + 直方图。见 docs/steps/M0-gr2-reader.md // gr2fuzz [assets-root] [--report out.json] [--list out.txt] #include "gr2/gr2.h" #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace { struct Hist { std::map m; void add(const std::string& k, uint64_t n = 1) { m[k] += n; } }; struct Fails { std::vector lines; void add(const std::string& path, const std::string& why) { lines.push_back(path + "\t" + why); } }; // 非退化谓词。返回空 = 通过;否则第一条失败原因。 std::string check_predicates(const gr2::File& f) { const auto& fi = f.file_info(); for (size_t s = 0; s < fi.skeletons.size(); ++s) { const auto& sk = fi.skeletons[s]; if (sk.bones.empty() || sk.bones.size() > 512) return "skeleton bone count out of [1,512]: " + std::to_string(sk.bones.size()); for (size_t i = 0; i < sk.bones.size(); ++i) { int32_t p = sk.bones[i].parent; if (p != -1 && (p < 0 || (size_t)p >= i)) return "bone parent not < self or -1 at " + std::to_string(i); for (float v : sk.bones[i].local_transform) if (!std::isfinite(v)) return "non-finite local_transform at bone " + std::to_string(i); for (float v : sk.bones[i].inverse_world) if (!std::isfinite(v)) return "non-finite inverse_world at bone " + std::to_string(i); } } for (size_t mi = 0; mi < fi.meshes.size(); ++mi) { const auto& me = fi.meshes[mi]; if (me.vertices.empty()) return "mesh " + std::to_string(mi) + " has 0 vertices"; for (uint32_t idx : me.indices) if (idx >= me.vertices.size()) return "mesh " + std::to_string(mi) + " index " + std::to_string(idx) + " >= vertex count " + std::to_string(me.vertices.size()); for (const auto& v : me.vertices) for (float c : v.pos) if (!std::isfinite(c)) return "mesh " + std::to_string(mi) + " non-finite position"; for (const auto& g : me.tri_groups) if (g.tri_first < 0 || g.tri_count < 0 || (int64_t)(g.tri_first + g.tri_count) * 3 > (int64_t)me.indices.size()) return "mesh " + std::to_string(mi) + " tri group out of range"; if (!me.rigid) { for (int32_t b : me.bone_bindings) if (b < 0) return "mesh " + std::to_string(mi) + " dangling bone binding"; for (const auto& v : me.vertices) { int wsum = 0; for (int k = 0; k < 4; ++k) { wsum += v.bone_weight[k]; if (!me.bone_bindings.empty() && v.bone_index[k] >= me.bone_bindings.size()) return "mesh " + std::to_string(mi) + " bone index out of range"; } // u8 权重和:3 显式 + 1 隐式的情况下允许补齐,故用宽松带 if (wsum < 200 || wsum > 262) // 255 ± ~10%(含隐式第 4 权重补齐余量) return "mesh " + std::to_string(mi) + " weight sum " + std::to_string(wsum) + " out of [200,262]"; } } } return {}; } void dump_report(const fs::path& out, uint64_t total, uint64_t ok, uint64_t crashed, uint64_t pred_fail, Hist& ver, Hist& comp, Hist& vkind, Hist& curve, Hist& weights, Hist& ssuse, Hist& selfchk, const Fails& crashes, const Fails& preds) { FILE* fp = std::fopen(out.string().c_str(), "wb"); if (!fp) { std::fprintf(stderr, "cannot write %s\n", out.string().c_str()); return; } auto obj = [&](const char* name, Hist& h, bool last) { std::fprintf(fp, " \"%s\": {", name); bool first = true; for (auto& [k, v] : h.m) { std::fprintf(fp, "%s\n \"%s\": %llu", first ? "" : ",", k.c_str(), (unsigned long long)v); first = false; } std::fprintf(fp, "\n }%s\n", last ? "" : ","); }; auto arr = [&](const char* name, const Fails& fl, bool last) { std::fprintf(fp, " \"%s\": [", name); for (size_t i = 0; i < fl.lines.size(); ++i) { std::string s = fl.lines[i]; std::string esc; for (char c : s) { if (c == '\\' || c == '"') esc += '\\'; esc += (c == '\t' ? ' ' : c); } std::fprintf(fp, "%s\n \"%s\"", i ? "," : "", esc.c_str()); } std::fprintf(fp, "\n ]%s\n", last ? "" : ","); }; std::fprintf(fp, "{\n"); std::fprintf(fp, " \"total\": %llu,\n \"ok\": %llu,\n \"crashed\": %llu,\n \"predicate_fail\": %llu,\n", (unsigned long long)total, (unsigned long long)ok, (unsigned long long)crashed, (unsigned long long)pred_fail); obj("format_version", ver, false); obj("section_compression", comp, false); obj("vertex_kind", vkind, false); obj("bone_weight_slots", weights, false); obj("curve_subtype", curve, false); obj("scaleshear_usage", ssuse, false); obj("bind_pose_self_check", selfchk, false); arr("crashes", crashes, false); arr("predicate_failures", preds, true); std::fprintf(fp, "}\n"); std::fclose(fp); } } // namespace #ifndef XRENDER_ASSET_ROOT #define XRENDER_ASSET_ROOT "." // 无编译期默认:从命令行传资产根目录 #endif int main(int argc, char** argv) { std::string root = XRENDER_ASSET_ROOT; fs::path report = "fuzz-report.json"; fs::path listout; for (int i = 1; i < argc; ++i) { if (!std::strcmp(argv[i], "--report") && i + 1 < argc) report = argv[++i]; else if (!std::strcmp(argv[i], "--list") && i + 1 < argc) listout = argv[++i]; else root = argv[i]; } if (!fs::exists(root)) { std::fprintf(stderr, "asset root not found: %s\n", root.c_str()); return 2; } uint64_t total = 0, ok = 0, crashed = 0, pred_fail = 0; Hist ver, comp, vkind, curve, weights, ssuse, selfchk; Fails crashes, preds; std::vector good_files; static const char* kK[] = {"PNT332", "PNT3322", "PNT332_Skinned", "PNT3322_Skinned", "Unknown"}; for (auto& e : fs::recursive_directory_iterator( root, fs::directory_options::skip_permission_denied)) { if (!e.is_regular_file()) continue; auto p = e.path(); std::string ext = p.extension().string(); for (auto& c : ext) c = (char)std::tolower((unsigned char)c); if (ext != ".gr2") continue; ++total; gr2::LoadError err; std::optional f; try { f = gr2::File::load_path(p.string(), &err); } catch (const std::exception& ex) { ++crashed; crashes.add(p.string(), std::string("exception: ") + ex.what()); continue; } catch (...) { ++crashed; crashes.add(p.string(), "exception: unknown"); continue; } if (!f) { ++crashed; crashes.add(p.string(), "load[" + err.stage + "]: " + err.message); continue; } ver.add(std::to_string(f->format_version())); for (const auto& s : f->sections()) { comp.add(gr2::compression_name(s.compression)); if (s.expanded_size && s.decompress_status != 0) { ++crashed; crashes.add(p.string(), "decompress status " + std::to_string(s.decompress_status)); } } const auto& fi = f->file_info(); // bind pose 自洽(T5):对每个 skeleton 取最好(最小)偏差,按量级分桶 for (size_t s = 0; s < fi.skeletons.size(); ++s) { if (fi.skeletons[s].bones.size() < 2) continue; // 单骨挂点跳过 double dev = f->bind_pose_self_check(int(s)); const char* bucket = dev < 1e-4 ? "<1e-4" : dev < 1e-3 ? "<1e-3" : dev < 1e-2 ? "<1e-2" : dev < 1e-1 ? "<1e-1" : ">=1e-1"; selfchk.add(bucket); if (dev >= 1e-1 && getenv("GR2_SELFCHK")) std::fprintf(stderr, "SELFCHK %.4f skel%zu bones=%zu %s\n", dev, s, fi.skeletons[s].bones.size(), p.string().c_str()); } for (const auto& me : fi.meshes) { vkind.add(kK[int(me.kind)]); if (!me.rigid) { weights.add(std::to_string(me.source_bone_weight_slots)); } } for (const auto& sk : fi.skeletons) { uint64_t ss = 0; for (const auto& b : sk.bones) if (b.lt_flags & 0x4) ++ss; ssuse.add(ss ? "has_scaleshear" : "no_scaleshear", 1); ssuse.add("scaleshear_bones", ss); } for (const auto& an : fi.animations) for (const auto& t : an.tracks) { curve.add("pos:" + t.pos_type); curve.add("rot:" + t.rot_type); curve.add("scale:" + t.scale_type); } std::string why; try { why = check_predicates(*f); } catch (...) { why = "predicate check threw"; } if (!why.empty()) { ++pred_fail; preds.add(p.string(), why); continue; } ++ok; good_files.push_back(p.string()); } dump_report(report, total, ok, crashed, pred_fail, ver, comp, vkind, curve, weights, ssuse, selfchk, crashes, preds); if (!listout.empty()) { FILE* fp = std::fopen(listout.string().c_str(), "wb"); if (fp) { for (auto& s : good_files) std::fprintf(fp, "%s\n", s.c_str()); std::fclose(fp); } } std::printf("gr2fuzz: total=%llu ok=%llu crashed=%llu predicate_fail=%llu\n", (unsigned long long)total, (unsigned long long)ok, (unsigned long long)crashed, (unsigned long long)pred_fail); std::printf(" report: %s\n", report.string().c_str()); if (!crashes.lines.empty()) { std::printf(" first crashes:\n"); for (size_t i = 0; i < crashes.lines.size() && i < 20; ++i) std::printf(" %s\n", crashes.lines[i].c_str()); } if (!preds.lines.empty()) { std::printf(" first predicate failures:\n"); for (size_t i = 0; i < preds.lines.size() && i < 20; ++i) std::printf(" %s\n", preds.lines[i].c_str()); } return (crashed == 0 && pred_fail == 0) ? 0 : 1; }