Skip to content

feat(perf): draw-call analysis + merge suggestions (Phase 6 slice B) - #497

Merged
fernandotonon merged 3 commits into
masterfrom
feat/phase6-slice-b-drawcall-analysis
May 12, 2026
Merged

feat(perf): draw-call analysis + merge suggestions (Phase 6 slice B)#497
fernandotonon merged 3 commits into
masterfrom
feat/phase6-slice-b-drawcall-analysis

Conversation

@fernandotonon

@fernandotonon fernandotonon commented May 12, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 6 slice B (#261) — builds on Slice A (#494) using the same shape:

  • DrawCallAnalyzer (new): pure-data analyzer. Groups every entity by the materials its SubEntities use, counts one draw call per SubEntity, and produces ranked merge suggestions (clusters where N≥2 entities share a material — saving N−1 draw calls per cluster).
  • MeshInfoOverlay: appends Draws: 5 (save 3 by merging) to the floating mesh-info panel.
  • MCP tool analyze_draw_calls: returns the formatted summary plus a structured drawCalls JSON payload.
  • CLI qtmesh analyze <file> [--json]: headless/CI surface.
  • Run Validation checklist (added in 552a811 after user feedback): the validator now folds the new draw-call analyzer plus the slice-A memory estimator into its flow, and emits one feedback row per dimension instead of a bare No issues found. line.

Closes the draw-call analysis overlay + merge suggestions acceptance criterion of #261. One-click merge is deliberately deferred — it's a write operation that touches the undo system, material/skeleton remapping, and submesh combination, none of which the analysis itself needs.

Run Validation feedback before / after

Before (single line on success): ✔ No issues found.

After (one row per analyzed dimension):

✔ Geometry: 1,234 triangle(s) across 3 submesh(es), no degenerate faces
✔ UVs: all finite, all within ±10 range
ℹ Draws: 5 across 2 material(s) — save 3 by merging entities that share a material
ℹ GPU: ~245 KB of vertex + index buffers (612 vert / 1,234 tri)

Errors / warnings keep their existing presentation, just with a Geometry: / UVs: prefix for consistency. A new info icon (ℹ blue) covers neutral observations that aren't pass/fail.

Sample CLI output

$ qtmesh analyze media/models/ninja.mesh
File: ninja.mesh
Draw Call Analysis
==================
Entities:       1
Submeshes:      2
Draw calls:     2
Unique mats:    1
After merges:   2 (saves 0)

Materials:
  BaseWhite  submeshes=2  entities=1

No merge opportunities (each material is used by at most one entity).

Test plan

  • 12 DrawCallAnalyzerTest.* pure-data tests (cluster grouping, suggestion filtering, merge-savings arithmetic, JSON/text serialisation, null-entity handling). Run on every CI build.
  • MeshInfoOverlayIntegrationTest.FormatStatsIncludesDrawCalls — overlay's Draws: line appears for valid entities.
  • MeshValidatorTest.DoValidateValidMeshReportsChecklist — the validator emits Geometry/UVs/Draws/GPU rows for a clean mesh, none of them error/warning.
  • MeshValidatorTest.DoValidateDetectsDegeneratesAndUvProblems — still catches the three failure modes after the row-prefix rename.
  • Manual: qtmesh analyze on robot.mesh / ninja.mesh produces correct text and JSON; exit 0.
  • Manual: app launches, "Run Validation" now lists all four dimensions.
  • App + UnitTests builds clean on macOS arm64 (Qt 6.9.3 / Ogre 14.5.x).
  • Wired into both src/CMakeLists.txt (main editor) and tests/CMakeLists.txt (MaterialEditorQML test executables) — slice A regression check.

🤖 Generated with Claude Code

Builds the second pillar of the optimization pipeline on top of Slice
A's reporting scaffold (#494).

DrawCallAnalyzer (new) — pure-data analyzer.  Takes any list of
Ogre::Entity* and produces a DrawCallReport:
- totals: entities, submeshes, draw calls (1 per SubEntity), unique
  materials, projected draw-call count after merging, total savings;
- clusters: every material plus the entities that use it and the
  draw-call savings unlocked by merging them;
- suggestions: clusters with >=2 entities, ranked by savings.

Surfaced through:
- MeshInfoOverlay — appends "Draws: N (save K by merging)" so the
  overlay shows the merge potential at a glance, in sync with the
  current selection.
- MCP tool analyze_draw_calls — returns the formatted summary in the
  standard content field plus a structured `drawCalls` payload (the
  same shape as get_memory_usage from slice A).
- CLI subcommand `qtmesh analyze <file> [--json]` for headless /
  CI workflows.

The one-click merge action listed in #261's slice B scope is
deliberately deferred: it is a write operation that touches the
undo system, material/skeleton remapping, and submesh combination
logic. The analysis alone (the harder pure-data work) is the bulk
of the value — the merge suggestion in the JSON already tells users
or automation which entities to combine.

Tests: 12 DrawCallAnalyzerTest cases cover the byte math, suggestion
filtering, JSON/text serialisation, and null-entity handling. The
new MeshInfoOverlay test confirms the "Draws:" line appears. Ogre-
backed paths follow the existing tryInitOgre() pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds DrawCallAnalyzer (data model + analysis) to compute per-material draw-call costs and merge suggestions; integrates it into a new CLI analyze subcommand, an MCP/HTTP tool, the MeshInfoOverlay and MeshValidator, and includes unit and integration tests plus build updates.

Changes

Draw-Call Analysis Feature

Layer / File(s) Summary
DrawCallAnalyzer Data Model & Core Logic
src/DrawCallAnalyzer.h, src/DrawCallAnalyzer.cpp
Data structs (MaterialCluster, MergeSuggestion, DrawCallReport) and static analysis API; clusters submeshes by material, computes mergeSavings(), stable-sorts suggestions, and serializes to JSON/text.
DrawCallAnalyzer Unit Tests
src/DrawCallAnalyzer_test.cpp
GTest suite covering buildSuggestions, mergeSavings, analyze behavior with empty/null entities, and toJson/toText output shape and content.
CLI Subcommand Integration
src/CLIPipeline.h, src/CLIPipeline.cpp, src/main.cpp
Adds analyze <file> [--json] subcommand, declares cmdAnalyze, wires usage and dispatch, and implements file import + analysis with exit codes 0/1/2 and optional JSON output.
MCP Server Tool Integration
src/MCPServer.h, src/MCPServer.cpp
Registers analyze_draw_calls tool, implements toolAnalyzeDrawCalls to analyze the current scene and return text plus structured drawCalls JSON, and advertises the tool in buildToolsList().
Mesh Info Overlay Integration
src/MeshInfoOverlay.cpp, src/MeshInfoOverlay_test.cpp
formatStats() now appends Draws: line with optional (save … by merging) suffix when analysis reports draw calls; integration test checks presence of Draws:.
Mesh Validator & Tests / UI
src/MeshValidator.cpp, src/MeshValidator_test.cpp, qml/PropertiesPanel.qml
Validator now emits a checklist with geometry, UVs, draws info, and GPU info rows; UI renders info rows with an info icon/color and tests updated to expect checklist-style output and new wording.
Build Configuration
src/CMakeLists.txt, tests/CMakeLists.txt
Adds DrawCallAnalyzer.cpp/.h to main and test source lists so analyzer is built into both targets.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant MCPServer
  participant MeshInfoOverlay
  participant MeshValidator
  participant DrawCallAnalyzer
  CLI->>DrawCallAnalyzer: analyze(entities from imported file)
  MCPServer->>DrawCallAnalyzer: analyzeScene()
  MeshInfoOverlay->>DrawCallAnalyzer: analyze(validEntities)
  MeshValidator->>DrawCallAnalyzer: analyze(validEntities)
  DrawCallAnalyzer->>DrawCallAnalyzer: cluster by material (collect submesh counts, entity names)
  DrawCallAnalyzer->>DrawCallAnalyzer: compute mergeSavings per cluster
  DrawCallAnalyzer->>CLI: toJson(report) / toText(report)
  DrawCallAnalyzer->>MCPServer: QJsonObject + text content
  DrawCallAnalyzer->>MeshInfoOverlay: QString formatted report
  DrawCallAnalyzer->>MeshValidator: DrawCallReport for checklist row
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I counted draws and clustered threads,
Materials, meshes, all in beds.
From CLI, MCP, overlay too,
I whisper savings just for you.
Hop—merge surfaces, make scenes less red.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: draw-call analysis and merge suggestions as part of Phase 6 slice B. It is specific, concise, and directly reflects the primary feature additions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description is comprehensive, well-structured, and includes all required information with examples and test plan.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/phase6-slice-b-drawcall-analysis

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/CLIPipeline.cpp`:
- Around line 3452-3456: The analyze import step performs file I/O but only logs
"cli.analyze"; add a file.import breadcrumb immediately before calling
MeshImporterExporter::importer so the operation is tracked: call
SentryReporter::addBreadcrumb("file.import", QString("Import
%1").arg(fi.absoluteFilePath())) (or a similar message referencing
fi.suffix()/fi.absoluteFilePath()) right before
MeshImporterExporter::importer({fi.absoluteFilePath()}, 0) so Sentry records the
file import action.
- Around line 3468-3470: The text output path is missing the analyzed filename;
when writing human-readable text you should include the report.file value so
text and JSON outputs are consistent—either update
DrawCallAnalyzer::toText(const Report& report) to prepend/format report.file
into the returned string, or change the caller (where
cliWrite(DrawCallAnalyzer::toText(report)) is used) to call cliWrite by
composing the filename and the text (e.g., format "filename: " +
DrawCallAnalyzer::toText(report)); ensure you reference the Report's file member
when assembling the output so CI logs can correlate entries with files.

In `@src/DrawCallAnalyzer.cpp`:
- Around line 9-11: The file uses QSet<QString> (symbol: QSet<QString>) but
doesn't explicitly include <QSet>, relying on transitive includes; add an
explicit include for <QSet> in the header includes at the top of
DrawCallAnalyzer.cpp (alongside existing includes like <QHash> and <QJsonArray>)
so the code compiles robustly across configurations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aac99eab-bf3d-4d68-9de0-712abc114330

📥 Commits

Reviewing files that changed from the base of the PR and between 4a4292a and c60fc5b.

📒 Files selected for processing (12)
  • src/CLIPipeline.cpp
  • src/CLIPipeline.h
  • src/CMakeLists.txt
  • src/DrawCallAnalyzer.cpp
  • src/DrawCallAnalyzer.h
  • src/DrawCallAnalyzer_test.cpp
  • src/MCPServer.cpp
  • src/MCPServer.h
  • src/MeshInfoOverlay.cpp
  • src/MeshInfoOverlay_test.cpp
  • src/main.cpp
  • tests/CMakeLists.txt

Comment thread src/CLIPipeline.cpp
Comment thread src/CLIPipeline.cpp
Comment thread src/DrawCallAnalyzer.cpp
fernandotonon and others added 2 commits May 12, 2026 12:09
…497)

Quality Gate already passed; this clears the open issues:

CodeRabbit
- Added a `file.import` Sentry breadcrumb in cmdAnalyze immediately
  before the import call, matching the project convention for I/O
  operations (CLAUDE.md guidance).
- Text output now prepends `File: <name>` so it mirrors the JSON's
  `file` field — same correlation in CI logs.
- Added explicit #include <QSet> in DrawCallAnalyzer.cpp instead of
  relying on transitive includes.

SonarCloud
- S5276 (size_t → unsigned int): switched the SubEntity iteration in
  analyze() to size_t to match Ogre::Entity::getNumSubEntities()'s
  return type.
- S5276 (qsizetype → int): cast Container::size() at the assignment
  site in mergeSavings() and the uniqueMaterials accumulator.
- S5350: const Ogre::SceneNode* in the analyzeScene scene-graph walk.
- S6004: drawReport now declared inside an if-init-statement in
  MeshInfoOverlay::formatStats.

The remaining MCPServer.cpp:2776 S5817 (`should be const`) is the
same intentional non-const tool method covered by the existing
NOSONAR comment in slice A. MCPServer.cpp:3394 S1116 is a pre-existing
Q_UNUSED(args); in toolGetPivotMode that Sonar mis-attributed to the
new code window — not touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#497)

User feedback on the slice B PR: "Run Validation" today only performs
the three geometry/UV checks and reports a bare "No issues found." line
when they all pass, hiding the scope of the analysis from the user.

This commit:

- Folds the new draw-call (slice B) and GPU-memory (slice A) analyses
  into the validator's flow, so a single click reports everything we
  can derive without exporting the mesh.
- Replaces the single-line success message with a per-dimension
  checklist. Every check now emits its own row in the issues list —
  errors and warnings stay as before, passing checks become "ok"
  rows ("Geometry: 1,234 triangle(s) across 3 submesh(es), no
  degenerate faces"), and neutral observations become a new "info"
  row type ("Draws: 5 — save 3 by merging", "GPU: ~245 KB of vertex
  + index buffers").
- QML icon palette extended for the new "info" type (ℹ blue).

Why include this on slice B's branch instead of a separate PR:
the validator change is what makes the slice B feature land in the
single place users already know to click for asset health, and it
shares the analyzers introduced in slice B (DrawCallAnalyzer) and
slice A (MemoryEstimator).

Tests: refreshed two MeshValidatorTest cases to assert the new
checklist shape (geometry / UVs / draws / GPU rows present, none
of them error/warning on a clean mesh). Existing
DoValidateDetectsDegeneratesAndUvProblems narrowed its substring
match to the still-present words ("non-finite", "extreme values")
to match the new "Geometry: " / "UVs: " row prefixes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/MeshValidator.cpp`:
- Around line 178-180: The vertex total is double-counting when multiple
submeshes share the same Ogre::VertexData (vd); change the accumulation logic so
you only add vd->vertexCount once per unique Ogre::VertexData pointer: create a
local container (e.g., std::unordered_set<const Ogre::VertexData*>) in the scope
that computes totalVerts, check if vd is already present before doing totalVerts
+= static_cast<int>(vd->vertexCount), and insert vd when first seen; keep the
existing ++totalSubmeshes and totalTris logic unchanged and include the
necessary header for the chosen container.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cff896e6-cc6b-4af2-bb47-a3a808c36360

📥 Commits

Reviewing files that changed from the base of the PR and between cc88b7e and 552a811.

📒 Files selected for processing (3)
  • qml/PropertiesPanel.qml
  • src/MeshValidator.cpp
  • src/MeshValidator_test.cpp

Comment thread src/MeshValidator.cpp
Comment on lines +178 to +180
++totalSubmeshes;
totalVerts += static_cast<int>(vd->vertexCount);
totalTris += static_cast<int>(id->indexCount / 3);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid double-counting shared vertex data in the vertex total

Line 179 adds vd->vertexCount per submesh. When multiple submeshes share the same Ogre::VertexData, the reported vertex count is inflated (and the GPU summary at Line 373 becomes misleading).

Suggested fix
+#include <QSet>
...
-    for (Ogre::Entity* entity : targets) {
+    for (Ogre::Entity* entity : targets) {
+        QSet<const Ogre::VertexData*> countedVertexData;
         Ogre::MeshPtr mesh = entity->getMesh();
         if (!mesh) continue;
...
-            totalVerts += static_cast<int>(vd->vertexCount);
+            if (!countedVertexData.contains(vd)) {
+                countedVertexData.insert(vd);
+                totalVerts += static_cast<int>(vd->vertexCount);
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MeshValidator.cpp` around lines 178 - 180, The vertex total is
double-counting when multiple submeshes share the same Ogre::VertexData (vd);
change the accumulation logic so you only add vd->vertexCount once per unique
Ogre::VertexData pointer: create a local container (e.g.,
std::unordered_set<const Ogre::VertexData*>) in the scope that computes
totalVerts, check if vd is already present before doing totalVerts +=
static_cast<int>(vd->vertexCount), and insert vd when first seen; keep the
existing ++totalSubmeshes and totalTris logic unchanged and include the
necessary header for the chosen container.

@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit f9c93d3 into master May 12, 2026
20 checks passed
@fernandotonon
fernandotonon deleted the feat/phase6-slice-b-drawcall-analysis branch May 12, 2026 19:12
@coderabbitai coderabbitai Bot mentioned this pull request May 20, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant