Skip to content

fix(fbx): embed all referenced textures via Video.Content (#508) - #514

Merged
fernandotonon merged 1 commit into
masterfrom
fix/508-fbx-embed-all-textures
May 14, 2026
Merged

fix(fbx): embed all referenced textures via Video.Content (#508)#514
fernandotonon merged 1 commit into
masterfrom
fix/508-fbx-embed-all-textures

Conversation

@fernandotonon

@fernandotonon fernandotonon commented May 13, 2026

Copy link
Copy Markdown
Owner

Summary

Test plan

  • `qtmesh convert Rumba\ Dancing.fbx -o out.fbx` produces an FBX containing 2 `Content` tags (was 0 before — only the index-discoverable diffuse landed, normal map silently dropped).
  • Re-importing from an isolated directory that has no Boss_*.png next to it loads both textures cleanly — no "MaterialProcessor: Failed to load normal map" warning.
  • `qtmesh optimize` output has the same shape (3.4MB, 2 Content tags).
  • EmbeddedTextureCache unit tests (5 standalone tests — round-trip, missing-key empty, replace-prior, reject-empty, clear).
  • Linux CI runs everything under Xvfb.

🤖 Generated with Claude Code

Summary by CodeRabbit

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 13, 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 36 minutes and 47 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ 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: 5098988b-833a-4d6f-85a7-c04a94d5abc0

📥 Commits

Reviewing files that changed from the base of the PR and between 6db950d and 1dfc3a3.

📒 Files selected for processing (8)
  • CLAUDE.md
  • src/Assimp/MaterialProcessor.cpp
  • src/CMakeLists.txt
  • src/EmbeddedTextureCache.cpp
  • src/EmbeddedTextureCache.h
  • src/EmbeddedTextureCache_test.cpp
  • src/FBX/FBXExporter.cpp
  • tests/CMakeLists.txt
📝 Walkthrough

Walkthrough

The PR implements a process-wide cache for embedded textures to enable robust FBX round-tripping. During import, MaterialProcessor captures texture payloads and stores them in EmbeddedTextureCache. During export, FBXExporter queries the cache first, then falls back to Ogre resource groups and filesystem probes, ensuring textures can be embedded even when not present on disk.

Changes

Embedded Texture Round-Trip Caching

Layer / File(s) Summary
EmbeddedTextureCache API and implementation
src/EmbeddedTextureCache.h, src/EmbeddedTextureCache.cpp
Define and implement a thread-safe cache namespace with two store overloads (one accepting std::vector<uint8_t>, one accepting raw const void* + byte count), retrieve returning a copy of cached bytes or empty on miss, and clear to reset state. Internal state guarded by mutex.
Capture embedded textures during import
src/Assimp/MaterialProcessor.cpp
Include EmbeddedTextureCache.h and stash embedded texture payloads (both compressed and raw RGB8) into the cache as they are loaded from the Assimp scene, preserving bytes that Ogre texture upload would discard.
Texture retrieval and fallback chain during export
src/FBX/FBXExporter.cpp
Include EmbeddedTextureCache.h, add helper functions (readStreamAll, findGroupForResource, probeFilesystemForResource), and enhance readOgreResourceBytes with staged fallback: cache lookup, then preferred/default/remaining Ogre resource groups, then direct filesystem probes of registered locations. Update writeTextureObjects comments to reflect the new strategy.
Unit tests for EmbeddedTextureCache
src/EmbeddedTextureCache_test.cpp
Verify store/retrieve round-trip, missing-key retrieval, key replacement, empty-input validation, and cache clearing with 5 standalone GoogleTest cases.
Build system updates
src/CMakeLists.txt, tests/CMakeLists.txt
Add EmbeddedTextureCache.cpp to SRC_FILES and HEADER_FILES for main build, and to TEST_SRC_FILES for test targets.
Documentation updates
CLAUDE.md
Document the new embedded texture strategy and the three-stage fallback order; introduce EmbeddedTextureCache as a process-lifetime pure-data store with clear() between unrelated imports; replace prior wording about Ogre-resource-only readability.

Sequence Diagram

sequenceDiagram
  participant Importer as Assimp Importer
  participant MatProc as MaterialProcessor
  participant Cache as EmbeddedTextureCache
  participant Exporter as FBXExporter
  participant ResGroup as Ogre ResourceGroups
  participant FS as Filesystem

  Importer->>MatProc: loadTexture(embedded bytes)
  MatProc->>Cache: store(textureName, bytes)
  
  Note over Cache: Stores embedded texture bytes<br/>indexed by texture name
  
  Exporter->>Cache: retrieve(textureName)
  alt Cache hit
    Cache-->>Exporter: cached bytes
  else Cache miss
    Exporter->>ResGroup: findResource(textureName)
    alt Resource found
      ResGroup-->>Exporter: texture bytes
    else Resource not found
      Exporter->>FS: probeFilesystem(textureName)
      FS-->>Exporter: texture bytes or empty
    end
  end
  
  Exporter->>Exporter: embed in Video.Content
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • fernandotonon/QtMeshEditor#311: Modifies embedded-texture and resource-byte handling in FBXExporter.cpp with overlapping logic for writing Video.Content blocks and Ogre resource resolution; this PR layers the EmbeddedTextureCache system and fallback chain on top of similar embedding patterns.

Poem

🐰 A rabbit hops through textures lost,
And caches what the import cost,
When FBX roams the export trail,
Those bytes come back—no round-trip fails!
From import's paw to export's sight,
The cache makes embedded textures right. 🎨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.32% 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 accurately summarizes the main change: fixing FBX texture embedding by including all referenced textures via Video.Content, directly addressing issue #508.
Description check ✅ Passed The description includes a clear Summary and Test plan sections covering the key changes and validation, though it lacks a dedicated Technical Details section from the template.
Linked Issues check ✅ Passed The code changes comprehensively implement the acceptance criteria from issue #508: embedded texture caching, material texture-unit-state walking for all texture slots, fallback mechanisms for resolution failures, and unit tests covering the cache behavior.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the stated objectives of resolving issue #508. The EmbeddedTextureCache implementation, MaterialProcessor integration, FBXExporter enhancements, and corresponding tests are all in-scope for the embedded texture round-trip fix.

✏️ 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 fix/508-fbx-embed-all-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: 01c045737e

ℹ️ 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/FBX/FBXExporter.cpp Outdated
Comment on lines +1791 to +1793
if (auto cached = EmbeddedTextureCache::retrieve(resourceName);
!cached.empty())
return cached;

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 Encode raw embedded textures before writing Video.Content

When MaterialProcessor::loadTexture hits aiTexture::mHeight > 0, it stores uncompressed texel bytes in EmbeddedTextureCache (raw pixel buffer), but this exporter path returns that cache entry verbatim and writes it as Video.Content. In that scenario, the FBX payload is not an encoded image stream (PNG/JPEG/etc.), so downstream FBX importers that decode Video.Content as an image file will fail or show corrupted textures. This regression is specific to models with uncompressed embedded textures; they were previously omitted, but are now embedded in an invalid format.

Useful? React with 👍 / 👎.

@fernandotonon
fernandotonon force-pushed the fix/508-fbx-embed-all-textures branch from 01c0457 to 6db950d Compare May 14, 2026 01:48

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

🧹 Nitpick comments (1)
src/EmbeddedTextureCache_test.cpp (1)

31-38: 💤 Low value

Clarify test name to reflect actual behavior.

The test name StoreRejectsEmptyKeyOrBytes implies validation or an exception, but the test verifies that retrieval returns empty (silent no-op). Consider renaming to EmptyKeyOrBytesResultInEmptyRetrieval or similar.

🤖 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/EmbeddedTextureCache_test.cpp` around lines 31 - 38, Rename the test to
reflect its actual behavior: change TEST(EmbeddedTextureCacheStandaloneTest,
StoreRejectsEmptyKeyOrBytes) to a name like
TEST(EmbeddedTextureCacheStandaloneTest, EmptyKeyOrBytesResultInEmptyRetrieval),
keeping the body unchanged (calls to EmbeddedTextureCache::clear(),
EmbeddedTextureCache::store("", {1,2,3}), EmbeddedTextureCache::store("y.png",
{}), and EXPECT_TRUE(EmbeddedTextureCache::retrieve("").empty()) /
EXPECT_TRUE(EmbeddedTextureCache::retrieve("y.png").empty())). Update only the
test identifier; do not alter EmbeddedTextureCache::store,
EmbeddedTextureCache::retrieve, or test assertions.
🤖 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 `@CLAUDE.md`:
- Line 202: Update the sentence describing EmbeddedTextureCache::clear() to be
explicit about timing: state that call EmbeddedTextureCache::clear() immediately
after completing an import session that has finished using
aiScene::GetEmbeddedTexture results (e.g., after MaterialProcessor::loadTexture
and all FBXExporter::readOgreResourceBytes consumers have finished), or before
starting a new, unrelated import session to release retained raw texture bytes
back to the OS; also note it should be invoked whenever memory usage from cached
textures becomes a concern (large scenes or batch imports).

In `@src/EmbeddedTextureCache_test.cpp`:
- Around line 5-47: Tests only cover the vector overload; add a unit that
exercises the raw-buffer store overload (call
EmbeddedTextureCache::store("raw.png", rawData, sizeof(rawData)) with a uint8_t
array, then retrieve and ASSERT_EQ size and check first/last bytes) and add a
basic concurrency test to validate thread-safety by launching multiple
std::thread writers that call
EmbeddedTextureCache::store("thread_"+std::to_string(id)+".png", data) in
parallel, join them, then retrieve each key and EXPECT_EQ the sizes/contents;
use EmbeddedTextureCache::clear() at test start and include <thread> in the test
file.

---

Nitpick comments:
In `@src/EmbeddedTextureCache_test.cpp`:
- Around line 31-38: Rename the test to reflect its actual behavior: change
TEST(EmbeddedTextureCacheStandaloneTest, StoreRejectsEmptyKeyOrBytes) to a name
like TEST(EmbeddedTextureCacheStandaloneTest,
EmptyKeyOrBytesResultInEmptyRetrieval), keeping the body unchanged (calls to
EmbeddedTextureCache::clear(), EmbeddedTextureCache::store("", {1,2,3}),
EmbeddedTextureCache::store("y.png", {}), and
EXPECT_TRUE(EmbeddedTextureCache::retrieve("").empty()) /
EXPECT_TRUE(EmbeddedTextureCache::retrieve("y.png").empty())). Update only the
test identifier; do not alter EmbeddedTextureCache::store,
EmbeddedTextureCache::retrieve, or test assertions.
🪄 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: 13214ce2-53c1-4cc0-9d3d-be4ff5cb7e70

📥 Commits

Reviewing files that changed from the base of the PR and between f84348f and 6db950d.

📒 Files selected for processing (8)
  • CLAUDE.md
  • src/Assimp/MaterialProcessor.cpp
  • src/CMakeLists.txt
  • src/EmbeddedTextureCache.cpp
  • src/EmbeddedTextureCache.h
  • src/EmbeddedTextureCache_test.cpp
  • src/FBX/FBXExporter.cpp
  • tests/CMakeLists.txt

Comment thread CLAUDE.md Outdated
Comment thread src/EmbeddedTextureCache_test.cpp
Round-tripping Rumba Dancing.fbx through any FBX-output qtmesh path
(convert / fix / optimize / anim --simplify) produced an FBX whose
re-import rendered without the normal map: the texture reference was
present in the FBX properties, but the `Video.Content` block was
missing, so a downstream loader that depends on the embedded payload
saw "missing normal map" and reverted to flat-lit geometry.

Root cause: Boss_normal.png was an inline texture inside the source
FBX. Assimp's importer extracts the raw bytes via
`scene->GetEmbeddedTexture(...)` and hands them to Ogre via
`TextureManager::loadImage` / `loadRawData`. Those calls feed the GPU
texture but discard the original bytes — the texture's `getOrigin()`
is empty, it lives only in the GPU cache, and the resource-group file
index has no entry for it. `FBXExporter::readOgreResourceBytes` then
hits all three failure paths simultaneously (`findGroupContainingResource`
throws ItemIdentityException, `resourceExists(group, name)` returns
false for every group, `getByName(name, AUTODETECT)` finds the texture
but the group's archives have no file). The Content block is gated on
non-empty bytes, so the exporter silently emits the reference without
the payload.

New pure-data module `EmbeddedTextureCache` (`src/EmbeddedTextureCache.{h,cpp}`)
is a thread-safe process-wide map from texture name → raw bytes.

`MaterialProcessor::loadTexture` stashes the compressed bytes the
moment `GetEmbeddedTexture` produces them (both the
`mHeight == 0`/compressed and uncompressed branches).

`FBXExporter::readOgreResourceBytes` queries the cache first, then
falls through to the existing resource-group / filesystem-probe paths.
A third fallback was added — walk every registered resource location
and probe disk — for textures that landed on disk *after* the group's
file index was built (also relevant to issue #508 secondary cases).

Diagnostic logging used during the debug session was removed before
commit.

Convert: source 6.3MB → output 3.4MB (was 2.4MB before the fix, missing
~1MB of normal-map payload). Output contains 2 `Content` tags, one per
texture. Re-importing the converted FBX from an isolated directory
that has no Boss_*.png next to it loads both textures cleanly — no
"MaterialProcessor: Failed to load normal map" warning. Same shape on
the optimize pipeline output (3.4MB, 2 Content tags).

Unit tests for the cache (round-trip, missing-key empty, replace-prior,
reject-empty, clear) are auto-discovered by the gtest glob and run on
Linux CI under Xvfb.

Closes #508.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fernandotonon
fernandotonon force-pushed the fix/508-fbx-embed-all-textures branch from 6db950d to 1dfc3a3 Compare May 14, 2026 02:11
@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit c0429b9 into master May 14, 2026
20 checks passed
@fernandotonon
fernandotonon deleted the fix/508-fbx-embed-all-textures branch May 14, 2026 02:32
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.

feat(fbx): embed all referenced textures (normal/PBR slots) in Video.Content on export, not just the eager-loaded diffuse

1 participant