Skip to content

fix(scan): stabilize scan --fix + better output + FSAA default - #325

Merged
fernandotonon merged 2 commits into
masterfrom
feat/fbx-embed-textures
Apr 28, 2026
Merged

fix(scan): stabilize scan --fix + better output + FSAA default#325
fernandotonon merged 2 commits into
masterfrom
feat/fbx-embed-textures

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Apr 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fix qtmesh scan --fix crashes in the redundant-keyframe FBX fix path (Ogre headless init + safer material handling + empty-track guard).
  • Prevent FBX simplify fixes from being applied when the rewritten file would be larger (keeps original; reports as [skipped]).
  • Improve scan output/summary: [fixed]/[skipped] tags, bytes saved + keys removed totals, and treat fix-skips as passed while still counted in Skipped.
  • Default viewport FSAA to 0 when the setting is unset (users can opt in via settings).

Test plan

  • UnitTests --gtest_filter=OgreWidgetTest.FsaaDefaultsToZeroWhenUnset (added)
  • UnitTests --gtest_filter=ScanEngine* (existing coverage)
  • Manual: QT_QPA_PLATFORM=offscreen QtMeshEditor scan . --fix on EidolaEngine assets (verified during dev)

Made with Cursor

Summary by CodeRabbit

Release Notes

  • New Features

    • Skeleton-only FBX export capability
    • Animation analysis with --analyze flag for skeleton metadata and duration lists
    • Redundant keyframe auto-fix during scanning with metrics (bytes saved, keyframes removed)
  • Bug Fixes

    • Prevented crashes when processing materials without required technique/pass structures
    • Enhanced import reliability with retry logic and improved error messaging
  • Improvements

    • FSAA disabled by default; users must explicitly configure it
    • Redundant keyframe detection enabled by default (40% threshold)
    • Expanded default scan exclusion patterns to filter common build/cache directories
    • Enhanced scan reporting with fix status tagging and impact summaries

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@fernandotonon has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 44 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ebc73f9c-58a5-4d19-992f-a1c8429b74c8

📥 Commits

Reviewing files that changed from the base of the PR and between f349021 and 2166e59.

📒 Files selected for processing (13)
  • src/AnimationMerger.cpp
  • src/Assimp/Importer.cpp
  • src/Assimp/MaterialProcessor.cpp
  • src/CLIPipeline.cpp
  • src/FBX/FBXExporter.cpp
  • src/FBX/FBXExporter.h
  • src/FBX/FBXExporter_test.cpp
  • src/OgreWidget.cpp
  • src/OgreWidget_test.cpp
  • src/ScanConfig.h
  • src/ScanEngine.cpp
  • src/ScanEngine.h
  • src/ScanEngine_test.cpp
📝 Walkthrough

Walkthrough

This PR introduces skeleton-only FBX export functionality, expands the scan engine with redundant keyframe detection and auto-fixing, enhances the CLI animation pipeline with analysis and export capabilities, and hardens import/material processing with robustness improvements.

Changes

Cohort / File(s) Summary
Animation Processing
src/AnimationMerger.cpp
Adds guard to skip track recreation when simplification results in zero remaining keyframes, preventing empty tracks in output animations.
Skeleton-Only FBX Export
src/FBX/FBXExporter.h, src/FBX/FBXExporter.cpp, src/FBX/FBXExporter_test.cpp
Introduces skeleton-only export path via new exportSkeletonOnlyFBX API; builder conditionally omits geometry/materials/textures, optimizes animation curve payloads with per-channel time arrays, and adds test coverage.
Import & Material Robustness
src/Assimp/Importer.cpp, src/Assimp/MaterialProcessor.cpp
Assimp importer adds case-insensitive FBX suffix checks and retry logic on load failure; MaterialProcessor adds ensureFirstPass utility to prevent crashes when materials have zero techniques/passes.
Scan Engine Core
src/ScanConfig.h, src/ScanEngine.h, src/ScanEngine.cpp
Expands redundant keyframe detection with auto-fix pipeline supporting FBX headless simplification and re-export; adds bytesSaved/keysRemoved tracking to findings/results; updates config defaults to enable 40% threshold detection and exclude additional build directories.
Scan Engine Tests
src/ScanEngine_test.cpp
Updates applyFixes calls to include scanRoot parameter; adds extension include-pattern filtering tests and dry-run/fixability assertions.
CLI Pipeline
src/CLIPipeline.cpp
Adds animation-only export path for skeleton/animation reconstruction; implements qtmesh anim --analyze [--json] for metadata/duration reporting; hardens include/exclude pattern parsing; improves scan output with severity tagging, byte/keyframe summaries, and skipped-finding suppression.
UI & Settings
src/OgreWidget.cpp, src/OgreWidget_test.cpp
Changes FSAA default from opt-in (value 4) to opt-out (value 0); adds test verifying zero-FSAA resolution when setting is unset.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI Pipeline
    participant FB as FBXExporter
    participant FBD as FBXDocumentBuilder
    participant FBX as FBX File

    CLI->>FB: exportSkeletonOnlyFBX(skeleton, path)
    FB->>FBD: buildSkeletonOnly(skeleton)
    FBD->>FBD: Create bone models & bind pose
    FBD->>FBD: Optimize animation curves<br/>(per-channel time arrays)
    FBD->>FBD: Skip geometry/materials/textures
    FBD->>FB: Return optimized document
    FB->>FBX: Write FBX with skeleton+animations
    FBX-->>CLI: Success
Loading
sequenceDiagram
    participant CLI as CLI/Scan
    participant SE as ScanEngine
    participant AM as AnimationMerger
    participant FBD as FBXDocumentBuilder
    participant FBX as FBX Export

    CLI->>SE: applyFixes(config, scanRoot, asset, findings)
    SE->>SE: Detect redundant keyframes
    alt Redundant Keys Found & Fixable
        SE->>AM: simplifyAnimation(animation)
        AM->>AM: Remove consecutive duplicates
        AM->>AM: Skip tracks with zero keys
        SE->>FBD: buildSkeletonOnly + simplify
        FBD->>FBX: Re-export smaller FBX
        SE->>SE: Record bytesSaved, keysRemoved
        SE-->>CLI: finding.skipped=false, marked FIXED
    else Dry-Run or Size Growth
        SE-->>CLI: finding.skipped=true
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Poem

🐰 Hop through the scan, find those redundant keys!
Simplify skeletons with digital ease,
Export without mesh, just bones and their dance,
Cleaner and faster—the scanner's last glance!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.36% 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 'fix(scan): stabilize scan --fix + better output + FSAA default' directly and specifically summarizes the main changes: scan stability fixes, improved output, and FSAA default behavior.
Description check ✅ Passed The description covers both required sections (Summary and Technical Details via bullets), clearly explains the fix objectives, includes test plans, and addresses all major changes in the PR.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fbx-embed-textures

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f349021ac2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/CLIPipeline.cpp
return 0;
}

if (analyzeMode) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve redundant-keyframe analysis for --analyze

This new early analyzeMode return bypasses the existing if (analyzeMode || simplifyMode) block that computes redundant keyframes and emits the detailed analysis JSON/text. As a result, qtmesh anim <file> --analyze now only prints skeleton metadata, and options like --preset, --tolerance, --rotation-tolerance-deg, and --animation are effectively ignored despite still being documented in usage, which is a behavioral regression for CLI users and automation that depend on the previous analysis output.

Useful? React with 👍 / 👎.

Comment thread src/CLIPipeline.cpp
Comment on lines +1439 to +1440
if (!skel && !animOnlySkeletons.isEmpty())
skel = animOnlySkeletons.first();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle anim-only simplify without null entity dereference

These lines allow cmdAnim to proceed with a valid skeleton even when no mesh entity was imported (animation-only input), but the simplify branch later still unconditionally dereferences entity (refreshAvailableAnimationState() / getParentSceneNode()). That means qtmesh anim <anim-only-file> --simplify can now crash with a null-pointer dereference; simplify needs the same isAnimOnlyInput export handling already added for resample/decimate/rename.

Useful? React with 👍 / 👎.

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/CLIPipeline.cpp (1)

1829-1837: ⚠️ Potential issue | 🔴 Critical

Null pointer dereference when --simplify is used on animation-only files.

If the input file is animation-only (no mesh geometry), entity will be nullptr at line 1448, but the simplify code path at lines 1829-1837 unconditionally dereferences it. This will cause a crash.

The resample and decimate modes correctly guard against this with if (isAnimOnlyInput) checks (lines 1602, 1663), but simplify mode lacks this protection.

🐛 Proposed fix to handle animation-only inputs in simplify mode
         // simplifyMode
         int totalRemoved = 0;
         int animsProcessed = 0;
         for (const auto& name : animNames) {
             if (!animationFilter.isEmpty() && animationFilter.toStdString() != name)
                 continue;
             int removed = AnimationMerger::simplifyAnimation(skel.get(), name, tol);
             totalRemoved += removed;
             ++animsProcessed;
         }

-        entity->refreshAvailableAnimationState();
-
         QFileInfo outFi(outputPath);
-        auto* node = entity->getParentSceneNode();
-        int result = MeshImporterExporter::exporter(node, outFi.absoluteFilePath(), formatForExtension(outputPath));
-        if (result != 0) {
-            SentryReporter::captureMessage(QString("CLI anim: simplify export failed (.%1)").arg(outFi.suffix()), "error");
-            err() << "Error: Export failed." << Qt::endl;
-            return 1;
+        if (isAnimOnlyInput) {
+            QString exportErr;
+            if (!exportAnimOnly(skel, outFi.absoluteFilePath(), &exportErr)) {
+                SentryReporter::captureMessage(QString("CLI anim: simplify export failed (anim-only)"), "error");
+                err() << "Error: Export failed: " << exportErr << Qt::endl;
+                return 1;
+            }
+        } else {
+            entity->refreshAvailableAnimationState();
+            auto* node = entity->getParentSceneNode();
+            int result = MeshImporterExporter::exporter(node, outFi.absoluteFilePath(), formatForExtension(outputPath));
+            if (result != 0) {
+                SentryReporter::captureMessage(QString("CLI anim: simplify export failed (.%1)").arg(outFi.suffix()), "error");
+                err() << "Error: Export failed." << Qt::endl;
+                return 1;
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CLIPipeline.cpp` around lines 1829 - 1837, The simplify branch
unconditionally dereferences entity (calling entity->getParentSceneNode() and
refreshAvailableAnimationState()) which crashes for animation-only inputs; add
the same guard used in resample/decimate: check isAnimOnlyInput before touching
entity — if isAnimOnlyInput, avoid dereferencing entity (either call
MeshImporterExporter::exporter with a nullptr node if supported or route to the
existing animation-only export path), otherwise proceed to call
entity->refreshAvailableAnimationState() and node =
entity->getParentSceneNode(); ensure MeshImporterExporter::exporter is invoked
with the appropriate node value and keep the existing error reporting
(SentryReporter::captureMessage / err()) on non-zero result.
🧹 Nitpick comments (4)
src/Assimp/MaterialProcessor.cpp (1)

59-67: Add Sentry breadcrumbs for these material import branches.

These changed paths perform significant import-time operations (normal-map load/apply attempts and skip/early-return conditions) but only write Ogre logs. Please add SentryReporter::addBreadcrumb(...) events for observability consistency.

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

Also applies to: 73-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Assimp/MaterialProcessor.cpp` around lines 59 - 67, Add Sentry
breadcrumbs for the normal-map load/apply and skip branches by calling
SentryReporter::addBreadcrumb("file.import", ...) at each significant point:
when the normal map fails to load (the branch that logs "Failed to load normal
map" using normalFilename and materialName), when a normalTexPtr exists and you
are about to apply it (before the log "Applying RTSS normal map" using
normalFilename and materialName), and also mirror this for the later branch
around lines 73-76; place these calls near the existing
Ogre::LogManager::getSingleton().logMessage invocations, include concise
messages referencing the materialName and normalFilename, and do not change
behavior of ensureFirstPass(existingMaterial) or
applyRTSSNormalMap(existingMaterial, normalTexPtr->getName()).
src/FBX/FBXExporter.cpp (1)

2119-2145: Add a file.export breadcrumb for the new skeleton-only path.

This is a new user-visible export flow, so failures here will be much harder to correlate without a breadcrumb.

Suggested change
 bool FBXExporter::exportSkeletonOnlyFBX(const Ogre::Skeleton* skeleton, const QString& filePath)
 {
     if (!skeleton || filePath.isEmpty())
         return false;
+
+    SentryReporter::addBreadcrumb(
+        "file.export",
+        QString("Export skeleton-only FBX: %1").arg(filePath));
 
     std::ofstream out(filePath.toStdString(), std::ios::binary);

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

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/FBX/FBXExporter.cpp` around lines 2119 - 2145, The exportSkeletonOnlyFBX
function lacks a Sentry breadcrumb; add
SentryReporter::addBreadcrumb("file.export", ...) calls to record this new
user-visible export flow: one breadcrumb at function start (include the filePath
and tag it as "skeleton-only" and maybe "start") and another on failure before
each early return/error log (e.g., when !skeleton, filePath.isEmpty(), out not
open, and when builder.buildSkeletonOnly fails) so failures can be correlated;
use the function name exportSkeletonOnlyFBX and the filePath in the breadcrumb
messages to make them unique and actionable.
src/FBX/FBXExporter_test.cpp (1)

2303-2342: Tighten this skeleton-only test a bit more.

Right now it only proves that bones/animations exist. It won't catch regressions that accidentally start exporting mesh-only objects again, and the fixture never freezes an explicit bind pose before export.

Suggested test hardening
     auto* child = skel->createBone("Child", 1);
     child->setPosition(0, 1, 0);
     root->addChild(child);
+    skel->setBindingPose();

     auto* anim = skel->createAnimation("wave", 1.0f);
     auto* track = anim->createNodeTrack(0, child);
@@
     auto animStacks = objects->findAll("AnimationStack");
     auto animCurves = objects->findAll("AnimationCurve");
     EXPECT_GE(animStacks.size(), 1);
     EXPECT_GE(animCurves.size(), 1);
+    EXPECT_TRUE(objects->findAll("Geometry").empty());
+    EXPECT_TRUE(objects->findAll("Material").empty());
+    EXPECT_TRUE(objects->findAll("Deformer").empty());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/FBX/FBXExporter_test.cpp` around lines 2303 - 2342, The test currently
only checks that bones and animations exist after calling
FBXExporter::exportSkeletonOnlyFBX(skel.get(), outPath) and will not catch
regressions that export mesh data or fail to capture the skeleton bind pose;
before calling exportSkeletonOnlyFBX, freeze the skeleton's bind pose on the
skel instance (use the appropriate Ogre Skeleton API to save/apply the binding
pose for the bones created with createBone and the animation created with
createAnimation) and after parseFBX(outPath.toStdString()) assert that the
parsed Objects block (found via findTopLevel) contains no "Model" or
"Geometry"/mesh-related entries and that only bone/node and animation objects
exist (tighten EXPECT_GE checks to exact counts or explicit absence assertions)
so the test fails if mesh data is exported or the bind pose was not preserved.
src/CLIPipeline.cpp (1)

1316-1316: Duplicate --analyze flag parsing.

The --analyze flag is parsed twice: once at line 1316 and again at line 1344. This redundancy won't cause incorrect behavior but adds confusion to the code.

♻️ Proposed fix to remove duplicate
         if (arg == "--list") { listMode = true; continue; }
-        if (arg == "--analyze") { analyzeMode = true; continue; }
+        if (arg == "--analyze") { analyzeMode = true; continue; }
         if (arg == "--json") { jsonOutput = true; continue; }
...
         if (arg == "--simplify") { simplifyMode = true; continue; }
-        if (arg == "--analyze")  { analyzeMode  = true; continue; }
         if (arg == "--preset" && i + 1 < argc) {

Remove the second occurrence at line 1344.

Also applies to: 1344-1344

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CLIPipeline.cpp` at line 1316, The command-line parsing contains a
duplicate branch that checks if (arg == "--analyze") { analyzeMode = true;
continue; } — keep the first occurrence (the one at the top of the
argument-parsing block) and remove the second duplicate to avoid redundancy;
search for the repeated check in the argument parsing loop (look for the
variable arg and analyzeMode in CLIPipeline.cpp) and delete the later if-block
so analyzeMode is only set once.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Assimp/Importer.cpp`:
- Around line 88-103: The retry path currently ORs in additionalFlags
(lightFlags |= additionalFlags) which can re-enable the post-process bits that
caused the initial failure; change this to only merge a curated safe subset:
define a SAFE_FALLBACK_FLAGS mask containing only known-safe aiProcess_* bits
(e.g., aiProcess_Triangulate, aiProcess_ValidateDataStructure,
aiProcess_LimitBoneWeights, aiProcess_PopulateArmatureData,
aiProcess_GlobalScale and optionally aiProcess_ConvertToLeftHanded) and replace
the OR with lightFlags |= (additionalFlags & SAFE_FALLBACK_FLAGS) so
importer.ReadFile(path, lightFlags) uses only the permitted fallback flags
(referencing lightFlags, additionalFlags, SAFE_FALLBACK_FLAGS,
convertToLeftHanded, and importer.ReadFile).

In `@src/Assimp/MaterialProcessor.cpp`:
- Around line 64-67: ensure the code checks the result of
ensureFirstPass(existingMaterial) before calling applyRTSSNormalMap: capture the
return value from ensureFirstPass (or the pass pointer it returns) and only call
applyRTSSNormalMap(existingMaterial, normalTexPtr->getName()) when
ensureFirstPass indicates success; if ensureFirstPass fails, skip the RTSS
normal-map application (and optionally log or handle the failure) to avoid
dereferencing a missing pass.

In `@src/FBX/FBXExporter.cpp`:
- Around line 477-487: buildSkeletonOnly currently casts away const and calls
reset() on the caller's Ogre::Skeleton, mutating external state; instead
preserve the const contract by not modifying the input: set m_skeletonOnly and
m_hasSkeleton as before, but do not const_cast the incoming pointer — either
store it as a const pointer (e.g. keep a const Ogre::Skeleton* m_skeletonConst)
or make an internal copy/clone of the skeleton and call reset() on that copy,
then assign m_skeleton to the internal copy; update references to m_skeleton
accordingly (or add/use m_skeletonConst) so export logic uses the
non-mutating/internal copy while leaving the caller's skeleton unchanged.

In `@src/OgreWidget_test.cpp`:
- Around line 251-260: This test modifies global QSettings by removing
ViewportSettingsKeys::fsaaSamples() without restoring it; update
TEST_F(OgreWidgetTest, FsaaDefaultsToZeroWhenUnset) to save the original
presence and value of that key (using QSettings::contains and value), perform
the remove and assertions (including widget->rebuildRenderWindow()), then
restore the original state at the end of the test (re-set the key if it existed
or remove it if it did not) so QSettings state is isolated and order-independent
for other tests.

In `@src/ScanConfig.h`:
- Around line 54-57: The comment for redundantKeyframesPctThreshold in
ScanConfig.h is out of sync with the default value; update the comment to
accurately state that the check is enabled by default at 40% (or change the
default to 0 to keep it disabled). Locate the redundantKeyframesPctThreshold
declaration and either edit the comment to read that redundant-keyframe warnings
are enabled by default and trigger at >=40%, or set
redundantKeyframesPctThreshold to 0.0 if you prefer to keep the check disabled
by default; ensure the comment and the variable default stay consistent.

---

Outside diff comments:
In `@src/CLIPipeline.cpp`:
- Around line 1829-1837: The simplify branch unconditionally dereferences entity
(calling entity->getParentSceneNode() and refreshAvailableAnimationState())
which crashes for animation-only inputs; add the same guard used in
resample/decimate: check isAnimOnlyInput before touching entity — if
isAnimOnlyInput, avoid dereferencing entity (either call
MeshImporterExporter::exporter with a nullptr node if supported or route to the
existing animation-only export path), otherwise proceed to call
entity->refreshAvailableAnimationState() and node =
entity->getParentSceneNode(); ensure MeshImporterExporter::exporter is invoked
with the appropriate node value and keep the existing error reporting
(SentryReporter::captureMessage / err()) on non-zero result.

---

Nitpick comments:
In `@src/Assimp/MaterialProcessor.cpp`:
- Around line 59-67: Add Sentry breadcrumbs for the normal-map load/apply and
skip branches by calling SentryReporter::addBreadcrumb("file.import", ...) at
each significant point: when the normal map fails to load (the branch that logs
"Failed to load normal map" using normalFilename and materialName), when a
normalTexPtr exists and you are about to apply it (before the log "Applying RTSS
normal map" using normalFilename and materialName), and also mirror this for the
later branch around lines 73-76; place these calls near the existing
Ogre::LogManager::getSingleton().logMessage invocations, include concise
messages referencing the materialName and normalFilename, and do not change
behavior of ensureFirstPass(existingMaterial) or
applyRTSSNormalMap(existingMaterial, normalTexPtr->getName()).

In `@src/CLIPipeline.cpp`:
- Line 1316: The command-line parsing contains a duplicate branch that checks if
(arg == "--analyze") { analyzeMode = true; continue; } — keep the first
occurrence (the one at the top of the argument-parsing block) and remove the
second duplicate to avoid redundancy; search for the repeated check in the
argument parsing loop (look for the variable arg and analyzeMode in
CLIPipeline.cpp) and delete the later if-block so analyzeMode is only set once.

In `@src/FBX/FBXExporter_test.cpp`:
- Around line 2303-2342: The test currently only checks that bones and
animations exist after calling FBXExporter::exportSkeletonOnlyFBX(skel.get(),
outPath) and will not catch regressions that export mesh data or fail to capture
the skeleton bind pose; before calling exportSkeletonOnlyFBX, freeze the
skeleton's bind pose on the skel instance (use the appropriate Ogre Skeleton API
to save/apply the binding pose for the bones created with createBone and the
animation created with createAnimation) and after
parseFBX(outPath.toStdString()) assert that the parsed Objects block (found via
findTopLevel) contains no "Model" or "Geometry"/mesh-related entries and that
only bone/node and animation objects exist (tighten EXPECT_GE checks to exact
counts or explicit absence assertions) so the test fails if mesh data is
exported or the bind pose was not preserved.

In `@src/FBX/FBXExporter.cpp`:
- Around line 2119-2145: The exportSkeletonOnlyFBX function lacks a Sentry
breadcrumb; add SentryReporter::addBreadcrumb("file.export", ...) calls to
record this new user-visible export flow: one breadcrumb at function start
(include the filePath and tag it as "skeleton-only" and maybe "start") and
another on failure before each early return/error log (e.g., when !skeleton,
filePath.isEmpty(), out not open, and when builder.buildSkeletonOnly fails) so
failures can be correlated; use the function name exportSkeletonOnlyFBX and the
filePath in the breadcrumb messages to make them unique and actionable.
🪄 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: a3ab770b-7dad-46fc-a398-9cb1096b1b57

📥 Commits

Reviewing files that changed from the base of the PR and between 586e5ee and f349021.

📒 Files selected for processing (13)
  • src/AnimationMerger.cpp
  • src/Assimp/Importer.cpp
  • src/Assimp/MaterialProcessor.cpp
  • src/CLIPipeline.cpp
  • src/FBX/FBXExporter.cpp
  • src/FBX/FBXExporter.h
  • src/FBX/FBXExporter_test.cpp
  • src/OgreWidget.cpp
  • src/OgreWidget_test.cpp
  • src/ScanConfig.h
  • src/ScanEngine.cpp
  • src/ScanEngine.h
  • src/ScanEngine_test.cpp

Comment thread src/Assimp/Importer.cpp
Comment on lines +88 to +103
if ((!scene || !scene->mRootNode) &&
(pathEndsWithInsensitive(path, ".fbx") || pathEndsWithInsensitive(path, ".fbxa"))) {
unsigned int lightFlags = aiProcess_Triangulate |
aiProcess_ValidateDataStructure |
aiProcess_LimitBoneWeights |
aiProcess_PopulateArmatureData |
aiProcess_GlobalScale;
if (convertToLeftHanded)
lightFlags |= aiProcess_ConvertToLeftHanded;
lightFlags |= additionalFlags;
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false);
scene = importer.ReadFile(path, lightFlags);
m_sceneUpAxis = 1;
if (scene && scene->mMetaData)
scene->mMetaData->Get("UpAxis", m_sceneUpAxis);
}

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

Keep the retry path genuinely “light.”

lightFlags |= additionalFlags can immediately re-enable the same post-process bits that caused the first import to fail, so the fallback stops being a real fallback as soon as a caller supplies extra Assimp flags. This should carry through only a known-safe subset, not the whole caller mask.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Assimp/Importer.cpp` around lines 88 - 103, The retry path currently ORs
in additionalFlags (lightFlags |= additionalFlags) which can re-enable the
post-process bits that caused the initial failure; change this to only merge a
curated safe subset: define a SAFE_FALLBACK_FLAGS mask containing only
known-safe aiProcess_* bits (e.g., aiProcess_Triangulate,
aiProcess_ValidateDataStructure, aiProcess_LimitBoneWeights,
aiProcess_PopulateArmatureData, aiProcess_GlobalScale and optionally
aiProcess_ConvertToLeftHanded) and replace the OR with lightFlags |=
(additionalFlags & SAFE_FALLBACK_FLAGS) so importer.ReadFile(path, lightFlags)
uses only the permitted fallback flags (referencing lightFlags, additionalFlags,
SAFE_FALLBACK_FLAGS, convertToLeftHanded, and importer.ReadFile).

Comment on lines +64 to 67
// Some materials can exist without any techniques/passes (e.g. partially loaded
// script materials). Ensure a valid pass exists before RTSS touches it.
(void)ensureFirstPass(existingMaterial);
applyRTSSNormalMap(existingMaterial, normalTexPtr->getName());

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

Guard RTSS application when pass creation fails.

At Line 66 the return from ensureFirstPass(existingMaterial) is ignored, but Line 67 still calls applyRTSSNormalMap. If pass creation/retrieval fails, this can reintroduce the crash path you’re trying to eliminate.

Suggested fix
-                (void)ensureFirstPass(existingMaterial);
-                applyRTSSNormalMap(existingMaterial, normalTexPtr->getName());
+                Ogre::Pass* existingPass = ensureFirstPass(existingMaterial);
+                if (!existingPass) {
+                    Ogre::LogManager::getSingleton().logMessage(
+                        "MaterialProcessor: Skipping RTSS normal map for existing material '" + materialName +
+                        "' because no valid pass could be ensured");
+                } else {
+                    applyRTSSNormalMap(existingMaterial, normalTexPtr->getName());
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Assimp/MaterialProcessor.cpp` around lines 64 - 67, ensure the code
checks the result of ensureFirstPass(existingMaterial) before calling
applyRTSSNormalMap: capture the return value from ensureFirstPass (or the pass
pointer it returns) and only call applyRTSSNormalMap(existingMaterial,
normalTexPtr->getName()) when ensureFirstPass indicates success; if
ensureFirstPass fails, skip the RTSS normal-map application (and optionally log
or handle the failure) to avoid dereferencing a missing pass.

Comment thread src/FBX/FBXExporter.cpp
Comment on lines +477 to +487
bool buildSkeletonOnly(const Ogre::Skeleton* skeleton)
{
if (!skeleton) return false;
m_skeletonOnly = true;
m_hasSkeleton = true;
m_skeleton = const_cast<Ogre::Skeleton*>(skeleton);
m_entity = nullptr;
m_mesh = nullptr;

m_skeleton->reset();

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

Avoid mutating caller state in a const export API.

buildSkeletonOnly() takes a const Ogre::Skeleton*, then casts away constness and calls reset(). That means exporting can snap the caller’s skeleton back to bind pose as a side effect, which is surprising and leaks state through a const API.

Minimal fix
     m_skeletonOnly = true;
     m_hasSkeleton = true;
     m_skeleton = const_cast<Ogre::Skeleton*>(skeleton);
     m_entity = nullptr;
     m_mesh = nullptr;
-
-    m_skeleton->reset();
 
     m_w.writeHeader();
📝 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
bool buildSkeletonOnly(const Ogre::Skeleton* skeleton)
{
if (!skeleton) return false;
m_skeletonOnly = true;
m_hasSkeleton = true;
m_skeleton = const_cast<Ogre::Skeleton*>(skeleton);
m_entity = nullptr;
m_mesh = nullptr;
m_skeleton->reset();
bool buildSkeletonOnly(const Ogre::Skeleton* skeleton)
{
if (!skeleton) return false;
m_skeletonOnly = true;
m_hasSkeleton = true;
m_skeleton = const_cast<Ogre::Skeleton*>(skeleton);
m_entity = nullptr;
m_mesh = nullptr;
m_w.writeHeader();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/FBX/FBXExporter.cpp` around lines 477 - 487, buildSkeletonOnly currently
casts away const and calls reset() on the caller's Ogre::Skeleton, mutating
external state; instead preserve the const contract by not modifying the input:
set m_skeletonOnly and m_hasSkeleton as before, but do not const_cast the
incoming pointer — either store it as a const pointer (e.g. keep a const
Ogre::Skeleton* m_skeletonConst) or make an internal copy/clone of the skeleton
and call reset() on that copy, then assign m_skeleton to the internal copy;
update references to m_skeleton accordingly (or add/use m_skeletonConst) so
export logic uses the non-mutating/internal copy while leaving the caller's
skeleton unchanged.

Comment thread src/OgreWidget_test.cpp
Comment on lines +251 to +260
TEST_F(OgreWidgetTest, FsaaDefaultsToZeroWhenUnset)
{
QSettings settings;
settings.remove(ViewportSettingsKeys::fsaaSamples());

EXPECT_NO_THROW(widget->rebuildRenderWindow());
app->processEvents();

EXPECT_EQ(widget->fsaaSamples(), 0u);
}

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

Keep QSettings state isolated in this test.

At Line 254, the key is removed but never restored. This can leak config across tests and create order-dependent failures.

💡 Suggested test-isolation fix
 TEST_F(OgreWidgetTest, FsaaDefaultsToZeroWhenUnset)
 {
     QSettings settings;
-    settings.remove(ViewportSettingsKeys::fsaaSamples());
+    const QString key = ViewportSettingsKeys::fsaaSamples();
+    const bool hadOriginal = settings.contains(key);
+    const QVariant originalValue = settings.value(key);
+    settings.remove(key);
+    settings.sync();

     EXPECT_NO_THROW(widget->rebuildRenderWindow());
     app->processEvents();

     EXPECT_EQ(widget->fsaaSamples(), 0u);
+
+    if (hadOriginal)
+        settings.setValue(key, originalValue);
+    else
+        settings.remove(key);
+    settings.sync();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OgreWidget_test.cpp` around lines 251 - 260, This test modifies global
QSettings by removing ViewportSettingsKeys::fsaaSamples() without restoring it;
update TEST_F(OgreWidgetTest, FsaaDefaultsToZeroWhenUnset) to save the original
presence and value of that key (using QSettings::contains and value), perform
the remove and assertions (including widget->rebuildRenderWindow()), then
restore the original state at the end of the test (re-set the key if it existed
or remove it if it did not) so QSettings state is isolated and order-independent
for other tests.

Comment thread src/ScanConfig.h
Fix scan --fix crashes in Ogre/Assimp paths, prevent FBX simplify from bloating files, and enrich scan output with [fixed]/[skipped] tags plus saved-bytes and keys-removed summaries. Default viewport FSAA to 0 when unset and add a regression test.

Made-with: Cursor
FBX animation curve channels may be emitted with a single key when the channel is constant. Relax the AnimationCurves test accordingly.

Also replace a `strlen`-based suffix length with `std::string_view` sizing for Sonar.

Made-with: Cursor
@fernandotonon
fernandotonon force-pushed the feat/fbx-embed-textures branch from f349021 to 2166e59 Compare April 28, 2026 07:01
@sonarqubecloud

Copy link
Copy Markdown

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