Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions src/CLIPipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "MaterialPresetLibrary.h"
#include "TextureChannelPacker.h"
#include "NormalMapGenerator.h"
#include "MemoryEstimator.h"
#include "QtMeshCloudClient.h"
#include <OgreMaterialSerializer.h>
#include <QApplication>
Expand Down Expand Up @@ -601,6 +602,13 @@ void CLIPipeline::printUsage()
" --all Apply all extra fixes\n"
" (no flags) Standard import/export (joins vertices, smooths normals, optimizes)\n"
"\n"
" memory <file> [--json] [--budget <size>] [--token <t>] [--no-cloud]\n"
" Report per-mesh GPU bytes and per-texture VRAM bytes\n"
" --budget accepts e.g. 50MB, 1GB; exit 1 if exceeded\n"
" If --budget omitted and a token is set, the project's\n"
" memory_budget_mb is fetched from QtMesh Cloud rules.\n"
" --no-cloud opts out.\n"
"\n"
"Global options:\n"
" --help, -h Show this help\n"
" --version, -v Show version\n"
Expand Down Expand Up @@ -962,6 +970,7 @@ int CLIPipeline::run(int argc, char* argv[])
else if (cmd == "material") rc = cmdMaterial(argc, argv);
else if (cmd == "pack-textures") rc = cmdPackTextures(argc, argv);
else if (cmd == "normal-from-height") rc = cmdNormalFromHeight(argc, argv);
else if (cmd == "memory") rc = cmdMemory(argc, argv);

if (rc < 0) {
err() << "Error: Unknown command '" << cmd << "'" << Qt::endl;
Expand Down Expand Up @@ -3272,3 +3281,139 @@ int CLIPipeline::cmdScan(int argc, char* argv[])
return 1;
return scanExit;
}

namespace {

struct MemoryCmdArgs {
QString filePath;
QString tokenArg;
bool jsonOutput = false;
bool noCloud = false;
quint64 budgetBytes = 0;
bool budgetExplicit = false;
};

// Parse argv into MemoryCmdArgs. Returns:
// 0 = parsed ok, run command
// 1 = print usage + exit 2 (missing file / bad value)
// 2 = print "invalid budget" + exit 2
int parseMemoryArgs(int argc, char* argv[], MemoryCmdArgs& out)
{
int i = 1;
while (i < argc) {
const QString arg(argv[i]);
++i;
if (arg == "memory" || arg == "--cli") continue;
if (arg == "--json") { out.jsonOutput = true; continue; }
if (arg == "--no-cloud") { out.noCloud = true; continue; }
if (arg == "--token" && i < argc) {
out.tokenArg = QString::fromLocal8Bit(argv[i++]);
continue;
}
if (arg == "--budget" && i < argc) {
out.budgetBytes = MemoryEstimator::parseBudget(argv[i++]);
if (out.budgetBytes == 0) return 2;
out.budgetExplicit = true;
continue;
}
if (!arg.startsWith("-") && out.filePath.isEmpty()) {
out.filePath = arg;
}
}
return out.filePath.isEmpty() ? 1 : 0;
}

// Apply QtMesh Cloud's rules.memory_budget_mb when no explicit --budget was
// given. Mutates budgetBytes and budgetSource; never fails the command.
void applyCloudBudget(const QString& tokenArg, quint64& budgetBytes, QString& budgetSource)
{
const QString ingest = resolveIngestToken(tokenArg);
if (ingest.isEmpty()) return;

SentryReporter::addBreadcrumb(QStringLiteral("cli.memory"),
QStringLiteral("QtMesh Cloud fetchRules: requested"));
const auto rules = QtMeshCloudClient::fetchRules(ingest);
if (!rules.ok) {
err() << "Warning: Could not load QtMesh Cloud rules ("
<< rules.errorString << "). Continuing without remote budget." << Qt::endl;
SentryReporter::addBreadcrumb(QStringLiteral("cli.memory"),
QStringLiteral("QtMesh Cloud fetchRules: failed %1").arg(rules.errorString),
QStringLiteral("warning"));
return;
}

const QJsonObject rulesObj = rules.config.value("rules").toObject();
const double mb = rulesObj.value("memory_budget_mb").toDouble(0.0);
if (mb > 0.0) {
budgetBytes = static_cast<quint64>(mb * 1024.0 * 1024.0);
budgetSource = QStringLiteral("cloud:%1").arg(rules.source);
err() << "Note: Using QtMesh Cloud memory_budget_mb="
<< mb << " (source: " << rules.source << ")." << Qt::endl;
}
SentryReporter::addBreadcrumb(QStringLiteral("cli.memory"),
QStringLiteral("QtMesh Cloud fetchRules: ok source=%1 budget_mb=%2")
.arg(rules.source).arg(mb));
}

void emitMemoryReport(const SceneMemoryReport& report, const QFileInfo& fi,
const QString& budgetSource, bool jsonOutput)
{
if (jsonOutput) {
QJsonObject obj = MemoryEstimator::toJson(report);
obj["file"] = fi.fileName();
if (!budgetSource.isEmpty())
obj["budgetSource"] = budgetSource;
cliWrite(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Indented)));
} else {
cliWrite(MemoryEstimator::toText(report));
}
}

} // namespace

int CLIPipeline::cmdMemory(int argc, char* argv[])
{
// Parse: memory <file> [--json] [--budget <size>] [--token <t>] [--no-cloud]
MemoryCmdArgs cmdArgs;
const int parseRc = parseMemoryArgs(argc, argv, cmdArgs);
if (parseRc == 2) {
err() << "Error: Invalid --budget value. Use a positive size "
"(e.g. 50MB, 1GB) or omit --budget for unlimited." << Qt::endl;
return 2;
}
if (parseRc == 1) {
err() << "Error: No input file specified." << Qt::endl;
err() << "Usage: qtmesh memory <file> [--json] [--budget <size>] [--token <t>] [--no-cloud]"
<< Qt::endl;
return 2;
}

const QFileInfo fi(cmdArgs.filePath);
if (!fi.exists()) {
err() << "Error: File not found: " << cmdArgs.filePath << Qt::endl;
return 1;
}

// No explicit --budget: try QtMesh Cloud's memory_budget_mb (token gated).
QString budgetSource = cmdArgs.budgetExplicit ? QStringLiteral("cli") : QString();
if (!cmdArgs.budgetExplicit && !cmdArgs.noCloud)
applyCloudBudget(cmdArgs.tokenArg, cmdArgs.budgetBytes, budgetSource);

if (!initOgreHeadless()) return 1;

SentryReporter::addBreadcrumb("cli.memory",
QString("Memory .%1%2 source=%3").arg(
fi.suffix(),
cmdArgs.budgetBytes > 0 ? QString(" budget=%1B").arg(cmdArgs.budgetBytes) : QString(),
budgetSource.isEmpty() ? QStringLiteral("none") : budgetSource));

MeshImporterExporter::importer({fi.absoluteFilePath()}, 0);

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 | 🟠 Major | ⚡ Quick win

Add file.import breadcrumb for memory command file load

Line 3371 performs a significant file I/O operation, but it is only tracked under cli.memory. Add a file.import breadcrumb before the importer call for guideline compliance.

Suggested patch
+    SentryReporter::addBreadcrumb("file.import",
+        QString("Importing file %1").arg(fi.absoluteFilePath()));
     MeshImporterExporter::importer({fi.absoluteFilePath()}, 0);

As per coding guidelines: "All user-facing actions and significant operations must be tracked with SentryReporter::addBreadcrumb(category, message) ... 'file.import' / 'file.export' for I/O operations"

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
MeshImporterExporter::importer({fi.absoluteFilePath()}, 0);
SentryReporter::addBreadcrumb("file.import",
QString("Importing file %1").arg(fi.absoluteFilePath()));
MeshImporterExporter::importer({fi.absoluteFilePath()}, 0);
🤖 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/CLIPipeline.cpp` at line 3371, Before calling
MeshImporterExporter::importer, add a Sentry breadcrumb to record the file
import; specifically call SentryReporter::addBreadcrumb with category
"file.import" and a message containing the path (use fi.absoluteFilePath() or
the same string passed to importer) immediately before the
MeshImporterExporter::importer({fi.absoluteFilePath()}, 0) invocation so the I/O
operation is tracked per guidelines.

if (const auto& entities = Manager::getSingleton()->getEntities(); entities.isEmpty()) {
err() << "Error: Failed to load file: " << cmdArgs.filePath << Qt::endl;
return 1;
}

const SceneMemoryReport report = MemoryEstimator::estimateScene(cmdArgs.budgetBytes);
emitMemoryReport(report, fi, budgetSource, cmdArgs.jsonOutput);
return report.overBudget() ? 1 : 0;
}
3 changes: 3 additions & 0 deletions src/CLIPipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ class CLIPipeline {
/// source via Sobel filter. Headless equivalent of the GUI
/// "Generate Normal Map…" dialog.
static int cmdNormalFromHeight(int argc, char* argv[]);
/// Phase 6 slice A: estimate GPU memory & VRAM for a mesh file
/// (per-submesh + per-texture, optional --json, optional --budget).
static int cmdMemory(int argc, char* argv[]);

/// Map file extension to MeshImporterExporter format string.
static QString formatForExtension(const QString& path);
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ MaterialPresetLibrary.cpp
MeshLodController.cpp
TextureChannelPacker.cpp
NormalMapGenerator.cpp
MemoryEstimator.cpp
MeshValidator.cpp
AIChatManager.cpp
WelcomeScreenController.cpp
Expand Down Expand Up @@ -165,6 +166,7 @@ MaterialPresetLibrary.h
MeshLodController.h
TextureChannelPacker.h
NormalMapGenerator.h
MemoryEstimator.h
MeshValidator.h
AIChatManager.h
WelcomeScreenController.h
Expand Down
52 changes: 52 additions & 0 deletions src/MCPServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "MeshInfoOverlay.h"
#include "MeshValidator.h"
#include "MeshLodController.h"
#include "MemoryEstimator.h"
#include <QDebug>
#include <QFile>
#include <QDir>
Expand Down Expand Up @@ -430,6 +431,7 @@
{QStringLiteral("generate_auto_lods"), &MCPServer::toolGenerateAutoLods},
{QStringLiteral("remove_lods"), &MCPServer::toolRemoveLods},
{QStringLiteral("get_lod_info"), &MCPServer::toolGetLodInfo},
{QStringLiteral("get_memory_usage"), &MCPServer::toolGetMemoryUsage},
{QStringLiteral("list_files"), &MCPServer::toolListFiles},
{QStringLiteral("search_files"), &MCPServer::toolSearchFiles},
{QStringLiteral("read_file"), &MCPServer::toolReadFile},
Expand Down Expand Up @@ -2732,6 +2734,40 @@
return makeSuccessResult(lines.join("\n"));
}

// NOSONAR(cpp:S5817) — ToolHandler is a non-const member-fn pointer (matching
// every other tool method in this class); marking just this one const would
// break the registry signature in MCPServer.h.
QJsonObject MCPServer::toolGetMemoryUsage(const QJsonObject &args)

Check warning on line 2740 in src/MCPServer.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This function should be declared "const".

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ4aZGgjN8V99na96WFx&open=AZ4aZGgjN8V99na96WFx&pullRequest=494
{
try {
if (const Manager* mgr = Manager::getSingletonPtr(); !mgr)
return makeErrorResult("Error: Manager not available");

quint64 budget = 0;
if (args.contains("budget")) {
const QString spec = args.value("budget").toString();
if (!spec.isEmpty()) {
budget = MemoryEstimator::parseBudget(spec);
if (budget == 0)
return makeErrorResult(
QString("Invalid budget '%1' — use e.g. '50MB', '1GB'").arg(spec));
}
}

SceneMemoryReport report = MemoryEstimator::estimateScene(budget);

// Human-readable text goes in the standard `content` field; machine
// consumers (LLM tool wrappers, CI scripts) read the structured
// `memory` payload alongside it.
QJsonObject result = makeSuccessResult(MemoryEstimator::toText(report));
result["memory"] = MemoryEstimator::toJson(report);
return result;
} catch (Ogre::Exception& e) {
return makeErrorResult(
QString("Ogre error: %1").arg(QString::fromStdString(e.getFullDescription())));
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Helper methods

QJsonObject MCPServer::toolListFiles(const QJsonObject &args)
Expand Down Expand Up @@ -4068,6 +4104,22 @@
);
}

// get_memory_usage
{
QJsonObject props;
props["budget"] = QJsonObject{{"type", "string"},
{"description", "Optional memory budget (e.g. '50MB', '1GB'). When the report exceeds the budget the response flags 'overBudget' under the structured 'memory' field."}};
appendTool(
"get_memory_usage",
"Report estimated GPU memory for every loaded mesh (vertex + index buffers) "
"and VRAM for every resident texture. The response includes a human-readable summary "
"in the standard content field and a structured 'memory' object with per-mesh, "
"per-texture, totals, and optional budget fields for machine consumers. "
"Use to spot heavy meshes/textures before exporting to a memory-constrained target.",
props
);
}

// delete_entity
{
QJsonObject props;
Expand Down
1 change: 1 addition & 0 deletions src/MCPServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ private slots:
QJsonObject toolGenerateAutoLods(const QJsonObject &args);
QJsonObject toolRemoveLods(const QJsonObject &args);
QJsonObject toolGetLodInfo(const QJsonObject &args);
QJsonObject toolGetMemoryUsage(const QJsonObject &args);
QJsonObject toolListFiles(const QJsonObject &args);
QJsonObject toolSearchFiles(const QJsonObject &args);
QJsonObject toolReadFile(const QJsonObject &args);
Expand Down
Loading
Loading