[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/SeedeXR/makehuman/master/benchmarks/bench_core.cpp [Back]  [Original]

// SPDX-License-Identifier: AGPL-3.0-or-later
//
// Compares mh::core against the measured Python baseline in
// benchmarks/baseline_python.json (see memory/project_context.md section 6).

#include "makehuman/core/Modifier.h"
#include "makehuman/core/ObjReader.h"
#include "makehuman/core/Proxy.h"
#include "makehuman/core/RenderMesh.h"
#include "makehuman/core/Subdivider.h"
#include "makehuman/core/Target.h"
#include "makehuman/core/TargetIndex.h"
#include "makehuman/io/BvhReader.h"
#include "makehuman/rig/Skeleton.h"
#include "makehuman/rig/Skinning.h"
#include "makehuman/rig/VertexWeights.h"

#include 
#include 
#include 
#include 
#include 
#include 
#include 

namespace {

using Clock = std::chrono::steady_clock;

struct Result {
    std::string label;
    double medianMs{};
    double baselineMs{};
};

template 
double medianMs(Fn&& fn, int repeat) {
    std::vector samples;
    samples.reserve(static_cast(repeat));
    for (int i = 0; i < repeat; ++i) {
        const auto t0 = Clock::now();
        fn();
        const auto t1 = Clock::now();
        samples.push_back(std::chrono::duration(t1 - t0).count());
    }
    std::ranges::sort(samples);
    return samples[samples.size() / 2];
}

}  // namespace

int main() {
    const std::filesystem::path base = std::filesystem::path(MH_DATA_DIR) / "3dobjs" / "base.obj";
    if (!std::filesystem::exists(base)) {
        std::fprintf(stderr, "base.obj not found at %s\n", base.string().c_str());
        return 1;
    }

    std::vector results;

    // Baselines are the medians measured from the Python reference on
    // 2026-08-29 (macOS 26.6.2 arm64, Python 3.14.6, numpy 2.5.1).
    results.push_back({"load base.obj (parse + adjacency + normals)",
                       medianMs([&] { (void)mh::core::loadObj(base); }, 5), 211.8});

    auto mesh = mh::core::loadObj(base);
    if (!mesh) {
        std::fprintf(stderr, "load failed: %s\n", mesh.error().message().c_str());
        return 1;
    }

    results.push_back({"calcNormals full mesh", medianMs([&] { mesh->calcNormals(); }, 20), 5.18});
    results.push_back(
        {"calcFaceNormals only", medianMs([&] { mesh->calcFaceNormals(); }, 50), 0.68});
    results.push_back(
        {"calcVertexNormals only", medianMs([&] { mesh->calcVertexNormals(); }, 50), 1.69});
    results.push_back({"buildAdjacency", medianMs([&] { mesh->buildAdjacency(); }, 10), 0.0});
    results.push_back(
        {"calcVertexTangents", medianMs([&] { mesh->calcVertexTangents(); }, 20), 0.0});

    // The reference's nearest equivalent is updateIndexBuffer (unweld + group
    // sort) at 3.43 ms -- but it does not triangulate, so ours does more work.
    results.push_back({"RenderMesh::build (unweld + triangulate + sort)",
                       medianMs([&] { (void)mh::core::RenderMesh::build(*mesh); }, 10), 3.43});

    // Face hiding. The point of splitting build() from setFaceMask() is that
    // hiding geometry must not re-unweld: compare this against the line above.
    {
        auto rm = mh::core::RenderMesh::build(*mesh);
        std::vector visible(mesh->vertexCount(), 1U);
        for (size_t v = 0; v < visible.size(); v += 7)
            visible[v] = 0U;
        const auto fm = mesh->faceMaskForVisibleVertices(visible);
        results.push_back({"RenderMesh::setFaceMask (hide, no unweld)",
                           medianMs([&] { (void)rm.setFaceMask(*mesh, *fm); }, 20), 1.85});
        results.push_back({"Mesh::faceMaskForVisibleVertices",
                           medianMs([&] { (void)mesh->faceMaskForVisibleVertices(visible); }, 50),
                           0.0});
    }

    // Skeleton: parse 163 bones + 326 joint clouds, then place every bone.
    {
        results.push_back({"loadSkeleton (163 bones, JSON)",
                           medianMs(
                               [&] {
                                   (void)mh::rig::loadSkeleton(std::filesystem::path(MH_DATA_DIR) /
                                                               "rigs" / "default.mhskel");
                               },
                               10),
                           0.0});
        auto sk =
            mh::rig::loadSkeleton(std::filesystem::path(MH_DATA_DIR) / "rigs" / "default.mhskel");
        if (sk) {
            const auto wpath = std::filesystem::path(MH_DATA_DIR) / "rigs" / "default_weights.mhw";
            results.push_back(
                {"loadWeights (.mhw, 139 bones / 57k entries)",
                 medianMs([&] { (void)mh::rig::loadWeights(wpath, mesh->vertexCount()); }, 5),
                 0.0});
            if (auto vw = mh::rig::loadWeights(wpath, mesh->vertexCount())) {
                results.push_back({"VertexWeights::compile (4 influences, 19158 verts)",
                                   medianMs([&] { (void)vw->compile(*sk, 4); }, 20), 0.0});
            }
            if (auto vw2 = mh::rig::loadWeights(wpath, mesh->vertexCount())) {
                (void)sk->updateJoints(mesh->coord());
                (void)sk->buildRestMatrices();
                const auto cw = vw2->compile(*sk, 4);
                const auto sm = mh::rig::computeSkinningMatrices(*sk, {});
                std::vector skinned;
                results.push_back(
                    {"computeSkinningMatrices (163 bones)",
                     medianMs([&] { (void)mh::rig::computeSkinningMatrices(*sk, {}); }, 50), 0.0});
                results.push_back(
                    {"skinPositions (LBS, 19158 verts x 4)",
                     medianMs([&] { (void)mh::rig::skinPositions(mesh->coord(), cw, sm, skinned); },
                              20),
                     0.0});
                // DQS is what the app now does by default, so the number that
                // matters per frame is this one and not the LBS line above.
                // Neither has a Python baseline and neither can: the reference
                // has no DQS path (`legacy/python/shared/skeleton.py:605-622`
                // accumulates matrices and says "linear blend skinning").
                results.push_back(
                    {"skinPositionsDqs (DQS, 19158 verts x 4)",
                     medianMs(
                         [&] { (void)mh::rig::skinPositionsDqs(mesh->coord(), cw, sm, skinned); },
                         20),
                     0.0});
            }
            results.push_back({"Skeleton::buildRestMatrices (163 bones)",
                               medianMs(
                                   [&] {
                                       (void)sk->updateJoints(mesh->coord());
                                       (void)sk->buildRestMatrices();
                                   },
                                   50),
                               0.0});
            results.push_back({"Skeleton::updateJoints (rig follows the mesh)",
                               medianMs([&] { (void)sk->updateJoints(mesh->coord()); }, 50), 0.0});
        }
    }

    // BVH import: 212 joints x 60 frames of facial pose units.
    results.push_back({"readBvh (face-poseunits, 212 joints x 60 frames)",
                       medianMs(
                           [&] {
                               (void)mh::io::readBvh(std::filesystem::path(MH_DATA_DIR) /
                                                     "poseunits" / "face-poseunits.bvh");
                           },
                           5),
                       0.0});

    // Catmull-Clark: reference build 202.30 ms, update_coords 7.64 ms,
    // calcNormals on the subdivided mesh 20.57 ms.
    results.push_back({"Subdivider::build (Catmull-Clark topology+geometry)",
                       medianMs([&] { (void)mh::core::Subdivider::build(*mesh); }, 5), 202.30});

    // The path the application actually takes: helper cages and joint cubes are
    // never subdivided, so 13,378 faces instead of 18,486. No Python figure --
    // the reference's masked timing was never captured.
    {
        const auto mask = mesh->staticFaceMask();
        results.push_back({"Subdivider::build (masked -- the app path)",
                           medianMs([&] { (void)mh::core::Subdivider::build(*mesh, mask); }, 5),
                           0.0});
    }

    auto sd = mh::core::Subdivider::build(*mesh);
    if (sd) {
        results.push_back({"Subdivider::refresh (geometry + normals)",
                           medianMs([&] { sd->refresh(*mesh); }, 10), 7.64 + 20.57});
    }

    // Targets: the reference measured 106.22 ms to text-parse 200, 4.86 ms to
    // apply 200 at 0.5, and 0.04 ms for a single application.
    std::vector targetPaths;
    {
        const auto dir = std::filesystem::path(MH_DATA_DIR) / "targets";
        if (std::filesystem::exists(dir)) {
            for (const auto& e : std::filesystem::recursive_directory_iterator(dir)) {
                if (e.path().extension() == ".target") targetPaths.push_back(e.path());
            }
            std::ranges::sort(targetPaths);
        }
    }

    // The number that matters for opening a saved .mhm: a worst-case character
    // reaches 364 targets, and they load concurrently. Fresh library per run --
    // prewarming an already-warm cache would measure nothing.
    if (targetPaths.size() >= 364) {
        const auto root = std::filesystem::path(MH_DATA_DIR) / "targets";
        std::vector rel;
        rel.reserve(364);
        for (size_t i = 0; i < 364; ++i)
            rel.push_back(std::filesystem::relative(targetPaths[i], root).string());
        results.push_back({"TargetLibrary::prewarm (364 = worst-case character)",
                           medianMs(
                               [&] {
                                   mh::core::TargetLibrary lib(root);
                                   lib.prewarm(rel);
                               },
                               5),
                           0.0});
    }

    std::vector loaded;
    if (targetPaths.size() >= 200) {
        const std::span first200{targetPaths.data(), 200};
        results.push_back({"load 200 targets (text parse)",
                           medianMs(
                               [&] {
                                   for (const auto& p : first200)
                                       (void)mh::core::loadTarget(p);
                               },
                               3),
                           106.22});

        for (const auto& p : first200) {
            if (auto t = mh::core::loadTarget(p)) loaded.push_back(std::move(*t));
        }

        results.push_back({"apply 200 targets @0.5 (full stack rebuild)",
                           medianMs(
                               [&] {
                                   mesh->resetToOriginal();
                                   for (const auto& t : loaded)
                                       mh::core::applyTarget(t, *mesh, 0.5F);
                               },
                               20),
                           4.86});

        results.push_back({"apply 1 target (slider delta)",
                           medianMs([&] { mh::core::applyTarget(loaded[0], *mesh, 0.5F); }, 500),
                           0.04});
        mesh->resetToOriginal();

        // The whole shipped set. Measured DIRECTLY from the reference at
        // 3225.63 ms -- not extrapolated from the 200-target figure, which
        // would give ~680 ms and be badly wrong: the first 200 targets hold
        // 196,644 sparse entries while all 1,280 hold 6,147,800 (31x the data
        // for 6.4x the files, because macrodetails alone is 106 MB of 126 MB).
        results.push_back({"load ALL 1280 targets (text parse)",
                           medianMs(
                               [&] {
                                   for (const auto& p : targetPaths)
                                       (void)mh::core::loadTarget(p);
                               },
                               2),
                           3225.63});
    }

    // Index all 1,280 targets by filename. The reference walks the tree with
    // TargetsCrawler; not separately timed there, so no baseline.
    results.push_back({"TargetIndex::build (index 1280 targets)",
                       medianMs([&] { (void)mh::core::TargetIndex::build(MH_DATA_DIR); }, 3), 0.0});

    // The full character rebuild: reset to the morph base and replay the stack.
    // The reference's equivalent is applyAllTargets (human.py:1147-1209).
    {
        const auto tIdx = mh::core::TargetIndex::build(MH_DATA_DIR);
        std::vector mods;
        for (const char* f : {"modeling_modifiers.json", "measurement_modifiers.json",
                              "bodyshapes_modifiers.json"}) {
            auto m = mh::core::loadModifiers(std::filesystem::path(MH_DATA_DIR) / "modifiers" / f);
            if (m) mods.insert(mods.end(), m->begin(), m->end());
        }
        if (tIdx.componentCount() > 0 && !mods.empty()) {
            results.push_back(
                {"loadModifiers (291 modifiers)",
                 medianMs(
                     [&] {
                         for (const char* f :
                              {"modeling_modifiers.json", "measurement_modifiers.json",
                               "bodyshapes_modifiers.json"}) {
                             (void)mh::core::loadModifiers(std::filesystem::path(MH_DATA_DIR) /
                                                           "modifiers" / f);
                         }
                     },
                     5),
                 0.0});

            mh::core::Human human(&tIdx, mods);
            results.push_back({"Human::rebuildStack (all 291 modifiers)",
                               medianMs([&] { human.rebuildStack(); }, 50), 0.0});

            mh::core::TargetLibrary lib(MH_DATA_DIR);
            human.applyStack(*mesh, lib);  // warm the target cache
            results.push_back({"Human::applyStack (character rebuild)",
                               medianMs([&] { human.applyStack(*mesh, lib); }, 100), 0.0});
            mesh->resetToOriginal();
        }
    }

    // Proxy fitting: the per-frame cost of keeping clothes/eyes on the body.
    {
        const auto pxyPath = std::filesystem::path(MH_DATA_DIR) / "eyes/high-poly/high-poly.mhclo";
        if (std::filesystem::exists(pxyPath)) {
            if (auto pxy = mh::core::loadProxy(pxyPath)) {
                std::vector fitted;
                results.push_back({"loadProxy (1064-vertex eye proxy)",
                                   medianMs([&] { (void)mh::core::loadProxy(pxyPath); }, 10), 0.0});
                results.push_back(
                    {"fitProxy (1064 verts, per frame)",
                     medianMs([&] { (void)mh::core::fitProxy(*pxy, mesh->coord(), fitted); }, 200),
                     0.0});
            }
        }
    }

    auto rm = mh::core::RenderMesh::build(*mesh);
    results.push_back({"RenderMesh::refreshPositions (morph hot path)",
                       medianMs([&] { rm.refreshPositions(*mesh); }, 50), 0.0});

    // Smoothing is on by default, so the interactive mesh is the subdivided one:
    // 80,252 render verts vs 21,833. This is the number that decides whether
    // dirty-range tracking is worth building -- see memory/todo.md, M2.
    if (sd) {
        auto rmSub = mh::core::RenderMesh::build(sd->mesh());
        results.push_back({"RenderMesh::refreshPositions (SUBDIVIDED -- the 60fps case)",
                           medianMs([&] { rmSub.refreshPositions(sd->mesh()); }, 50), 0.0});
    }

    if (sd) {
        std::printf("subdiv: %zu verts, %zu faces, %zu edges\n", sd->mesh().vertexCount(),
                    sd->mesh().faceCount(), sd->edgeCount());
    }
    std::printf("render: %zu verts (unwelded), %zu indices, %zu groups\n", rm.vertexCount(),
                rm.indexCount(), rm.groupRanges().size());
    std::printf("mesh: %zu verts, %zu faces, %zu uvs, maxValence %u\n\n", mesh->vertexCount(),
                mesh->faceCount(), mesh->uvCount(), static_cast(mesh->maxValence()));
    std::printf("%-46s %12s %12s %10s\n", "operation", "C++ (ms)", "python (ms)", "speedup");
    std::printf("%s\n", std::string(84, '-').c_str());
    for (const Result& r : results) {
        if (r.baselineMs > 0.0) {
            std::printf("%-46s %11.2f %11.2f %9.1fx\n", r.label.c_str(), r.medianMs, r.baselineMs,
                        r.baselineMs / r.medianMs);
        } else {
            std::printf("%-46s %11.2f %11s %10s\n", r.label.c_str(), r.medianMs, "-", "-");
        }
    }
    std::printf("\nbaseline: benchmarks/baseline_python.json (2026-08-29)\n");
    return 0;
}

Web Proxy Viewer  |  New URL  |  Original Page