Skip to content

fix(material): embedded-texture preview + crash on rapid texture switch + live material lists#734

Open
fernandotonon wants to merge 4 commits into
masterfrom
fix/embedded-preview-and-material-list
Open

fix(material): embedded-texture preview + crash on rapid texture switch + live material lists#734
fernandotonon wants to merge 4 commits into
masterfrom
fix/embedded-preview-and-material-list

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the Material Editor texture-preview blanking, a crash when rapidly switching texture images, and stale Inspector material lists.

Preview resolution (getTexturePreviewPath)

  • Group-agnostic lookup. FBX imports load textures into a per-asset resource group; getByName(name) only searches the default group, so valid textures previewed blank. Now falls back to a group-agnostic scan of the resource map.
  • Embedded-FBX textures. Decode the original bytes from EmbeddedTextureCache (textures with no on-disk origin, e.g. Boss_diffuse.png inside Rumba Dancing.fbx).
  • Removed the convertToImage() GPU-readback fallback. For textures loaded via loadImage/loadRawData the GPU buffer can't be read back and convertToImage dereferenced a null internal buffer → EXC_BAD_ACCESS. That is a hardware fault a C++ try/catch cannot intercept, so it crashed the app reliably when switching back to an embedded texture.
  • Memoization + re-entrancy guard. The QML Image.source binding evaluated the getter twice per change and reloadPreview() called it again (3× per switch), each doing GPU + disk work. The preview is now driven imperatively and the resolved path is cached per texture name (invalidated when a generated image reuses a name).

Apply path (setTextureName)

  • Dangling-TUS crash. A prior apply/recompile destroyed+rebuilt Ogre's Technique/Pass/TUS objects, leaving the cached TextureUnitState* orphaned; the next switch dereferenced a TUS with a null parent Pass and crashed in TextureUnitState::retrieveTexture()Pass::getResourceGroup(). Now re-captures live pointers via updateTextureUnitList() before use.
  • RTSS stale-shader. The viewport renders via the MSN_SHADERGEN scheme; rebinding the TUS texture did not update the already-generated shader, so after a few switches the surface blanked and lighting glitched. Now invalidateMaterial + validateMaterial in the RTSS scheme and compile() so the program regenerates against the new binding.

Inspector material lists

  • SceneTreeModel::materialsChanged() emitted on rebuild (model load / node+entity created) and on selection change. The submesh material dropdown and material library list re-fetch instead of caching a stale set.

Testing

  • Built locally on macOS arm64 (QtMeshEditor target, ENABLE_STABLE_DIFFUSION=ON).
  • Verified with lldb that the two crash sites (convertToImage and retrieveTexture/getResourceGroup) no longer fault under rapid back-and-forth texture switching, including repeatedly returning to the embedded FBX texture.

Known follow-up

  • The bound texture is still occasionally dropped after several rapid switches (renders untextured but no crash, lighting stable). Tracked separately — not a regression introduced here.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Material lists and selection controls now automatically refresh when scene materials change
    • Texture previews display correctly and update dynamically when textures are modified or regenerated
    • Improved stability when recompiling materials to prevent crashes from outdated internal state

…material list

Texture preview + Material Editor texture-switching robustness:

- Preview resolution (getTexturePreviewPath): resolve textures group-
  agnostically (FBX imports load into a per-asset resource group, which
  getByName's default-group lookup missed → blank preview), and decode
  embedded-FBX textures from EmbeddedTextureCache. Removed the
  convertToImage() GPU buffer-readback fallback entirely: for textures
  loaded via loadImage/loadRawData the GPU buffer can't be read back and
  convertToImage faulted with EXC_BAD_ACCESS — a hardware fault a C++
  try/catch cannot intercept, which crashed the app when switching back
  to an embedded texture.

- Memoize resolved preview paths + re-entrancy guard: the QML Image
  source binding evaluated the getter twice per change and reloadPreview
  called it again, re-running GPU/disk work 3× per switch; the preview is
  now driven imperatively and cached per texture name (invalidated when a
  generated image reuses a name).

- setTextureName: rebuild the cached technique/pass/TUS pointers before
  use (a prior apply/recompile dangled the cached TUS → crash in
  TextureUnitState::retrieveTexture/Pass::getResourceGroup), then
  invalidate+validate the material in the RTSS scheme and recompile so the
  viewport stops serving the shader generated against the previous texture
  (stale-shader blanking + lighting glitches on repeated switches).

- Inspector material lists auto-refresh: SceneTreeModel emits
  materialsChanged() on rebuild (model load / node+entity created) and on
  selection change; the submesh material dropdown and the material library
  list re-fetch instead of caching a stale set.

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

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@fernandotonon, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 13 minutes and 50 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f09f982a-6944-4892-88ea-1801f4c44b9b

📥 Commits

Reviewing files that changed from the base of the PR and between 592f2d8 and 77aa9e4.

📒 Files selected for processing (4)
  • qml/SceneTreeNode.qml
  • qml/TexturePropertiesPanel.qml
  • src/MaterialEditorQML.cpp
  • src/MaterialEditorQML.h
📝 Walkthrough

Walkthrough

Adds a materialsChanged signal to SceneTreeModel, emitted on rebuild() and selection changes, with two new QML Connections listeners that re-fetch material lists. Separately, MaterialEditorQML gains memoized texture preview-path resolution with an embedded-texture cache branch, a re-entrancy guard, SD-completion cache invalidation, and setTextureName is hardened with pointer-cache refresh and RTSS regeneration.

Changes

materialsChanged Signal and QML Material List Refresh

Layer / File(s) Summary
SceneTreeModel materialsChanged signal
src/SceneTreeModel.h, src/SceneTreeModel.cpp
Declares materialsChanged signal; emits it when SelectionSet::selectionChanged fires and at the end of rebuild().
QML Connections listeners
qml/PropertiesPanel.qml, qml/SceneTreeNode.qml
Adds Connections blocks that call refreshMaterialList() and re-assign availableMaterials() respectively when materialsChanged fires.

Texture Preview Memoization and Embedded Texture Support

Layer / File(s) Summary
MaterialEditorQML preview cache header
src/MaterialEditorQML.h
Adds QHash include, clearTexturePreviewCache() public slot, computeTexturePreviewPath() private helper, m_previewPathCache mutable hash, and m_resolvingPreview re-entrancy guard.
setTextureName hardening and RTSS regeneration
src/MaterialEditorQML.cpp
Includes EmbeddedTextureCache.h; refreshes the cached texture-unit pointer before use; adds TMD/-specific pass and alpha-reject handling; forces RTSS invalidate/validate/compile after rebinding.
Memoized preview path resolution with embedded texture branch
src/MaterialEditorQML.cpp
Wraps preview-path lookup in a memoization cache with recursion guard; decodes embedded textures from EmbeddedTextureCache into PNG via QImage instead of convertToImage(); invalidates the cache entry on SD generation completion.
TexturePropertiesPanel imperative reload
qml/TexturePropertiesPanel.qml
Replaces declarative source: binding with source: "" and Component.onCompleted: reloadPreview() to avoid repeated getter evaluation during rapid changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • fernandotonon/QtMeshEditor#201: Modifies qml/TexturePropertiesPanel.qml texture preview handling and reloadPreview()/cache-bust logic, directly overlapping with this PR's shift to imperative reload.
  • fernandotonon/QtMeshEditor#514: Implements and populates EmbeddedTextureCache, which this PR depends on directly in the new embedded-texture preview branch of MaterialEditorQML.

Poem

🐇 When materials shift and textures gleam,
No stale pixel shall haunt the screen!
A signal fires, the lists refresh,
Embedded bytes decoded fresh.
With guards and caches, previews stay true—
Hop hop hooray, the UI's brand new! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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 three main fixes: embedded-texture preview support, crash on rapid texture switching, and live material list updates.
Description check ✅ Passed The description covers all required template sections with comprehensive technical details, testing verification, and known follow-ups, exceeding the template's basic requirements.
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 fix/embedded-preview-and-material-list

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: 592f2d8ee7

ℹ️ 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/MaterialEditorQML.cpp Outdated
Comment on lines +2002 to +2004
auto cached = m_previewPathCache.constFind(texName);
if (cached != m_previewPathCache.constEnd()) {
return cached.value();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scope preview-cache entries to the actual texture

When two loaded resources share the same texture name but come from different resource groups or origins—a case this patch explicitly tries to support by scanning all groups—the first resolved preview is reused for every later material with that basename. The cache is never cleared on loadMaterial()/model changes, and embedded previews only rewrite the generated <name>_embedded.png inside computeTexturePreviewPath(), which is skipped on a cache hit, so selecting another FBX with its own diffuse.png can show or export stale pixels from the earlier asset. Key the cache by resource group/origin or clear it whenever the current material/resource context changes.

Useful? React with 👍 / 👎.

Comment thread src/MaterialEditorQML.cpp Outdated
} catch (...) {
}

// NOTE: we deliberately do NOT call texPtr->convertToImage()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore PNG previews for TIM textures

For textures whose origin is a .tim file, the code above intentionally avoids returning the origin because QML cannot display TIM, but this new path now skips the previous convertToImage() PNG generation unless the texture happens to be in EmbeddedTextureCache. User-selected/imported TIM textures are loaded through PS1TIM::loadTimToOgreImage() and have no embedded-cache entry, so they now fall through to an empty or raw .tim path and the preview/Save Texture As flow regresses. Keep a safe CPU-side TIM conversion path instead of removing the fallback for all non-embedded textures.

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: 6

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

1098-1162: ⚡ Quick win

Add a breadcrumb for the texture rebinding operation.

This path is a user-facing texture switch plus RTSS/material regeneration, but it has no ui.action breadcrumb at the operation boundary.

🔎 Proposed fix
         if (!name.isEmpty() && name != "*Select a texture*") {
+            SentryReporter::addBreadcrumb(QStringLiteral("ui.action"),
+                QStringLiteral("Set material texture: %1").arg(name));
+
             // The on-screen mesh renders via RTSS (the viewport uses the

As per coding guidelines, “Add Sentry breadcrumbs for all user-facing actions and significant operations using SentryReporter::addBreadcrumb() with categories: 'ui.action' for toolbar/menu clicks, 'ai.tool_call' for MCP tools, 'file.import'/'file.export' for I/O”.

🤖 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/MaterialEditorQML.cpp` around lines 1098 - 1162, Add a Sentry breadcrumb
at the beginning of the texture rebinding operation to track user-facing texture
switches. At the start of the block where `!name.isEmpty() && name != "*Select a
texture*"` is checked, call SentryReporter::addBreadcrumb() with category
'ui.action' and a descriptive message that includes the texture name being
applied. This breadcrumb should be added before the call to
ensureTextureInMaterialGroup() to capture the entire operation including the
RTSS/material regeneration that follows through getCurrentTextureUnit(),
setTextureName(), and the material compilation steps.

Source: Coding guidelines

🤖 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 `@qml/SceneTreeNode.qml`:
- Around line 293-299: The Connections block with onMaterialsChanged in
SceneTreeNode causes each node instance to fetch availableMaterials()
redundantly when the signal fires, creating fan-out performance issues in large
trees. Move the material refresh logic (matFilter.allMaterials assignment and
treeModel.availableMaterials() call) from the per-node Connections to a single
parent or controller-level handler, or implement conditional logic to only
execute this fetch when an active submesh dropdown requires the materials data.
This centralizes the refresh point and eliminates redundant calls across all
tree nodes.

In `@qml/TexturePropertiesPanel.qml`:
- Around line 216-221: The texturePreview image control has its source cleared
to an empty string but its visible property may remain true, resulting in a
blank preview pane instead of showing the placeholder. When the source property
is set to empty in the texturePreview Image element, explicitly set the visible
property to false to ensure the placeholder is displayed. Alternatively, bind
the visible property to a condition that checks the image status so that
texturePreview only remains visible when it has a valid loaded image and becomes
hidden when the status is Image.Null.

In `@src/MaterialEditorQML.cpp`:
- Around line 2002-2016: The cache in the texture preview path resolution is too
broad and caches empty results. Modify the cache lookup and insertion to include
resource group information in the cache key alongside texName, so different
asset groups don't share cached previews. Additionally, check if the resolved
path from computeTexturePreviewPath() is empty before calling
m_previewPathCache.insert() to avoid caching transient misses, ensuring only
valid resolutions are cached in m_previewPathCache while allowing subsequent
calls to attempt resolution again.
- Around line 1118-1119: The issue is that updateTextureUnitList() calls
getCurrentPass() which reads from m_passMap, but if a prior compile operation
rebuilt the pass objects, m_passMap may contain stale pointers. Before calling
updateTextureUnitList() and later getCurrentTextureUnit(), first refresh or
rebuild the pass maps to ensure they contain valid pass object pointers. This
prevents dereferencing stale pointers when getCurrentPass() is invoked
internally by updateTextureUnitList().
- Around line 2074-2077: The texName variable can contain path separators or
relative path components like ".." which allows directory traversal and causes
failures when nested directories don't exist. Before using texName in the
filePath() call within the embedded PNG save operation, sanitize it by
extracting only the filename component (basename) and removing any directory
separators or dangerous path components to ensure the file is safely saved
within the intended texture_previews directory.
- Around line 1128-1134: After removing passes with tech->removePass(1) in the
while loop, the m_passMap cache may contain stale references to removed passes.
If the currently selected pass was not pass 0, the subsequent getCurrentPass()
call could return a dangling pointer. Reset the currently selected pass to pass
0 (the surviving pass) and clear or refresh the m_passMap to invalidate the
stale pass references before calling getCurrentPass() for the
setAlphaRejectSettings operation.

---

Nitpick comments:
In `@src/MaterialEditorQML.cpp`:
- Around line 1098-1162: Add a Sentry breadcrumb at the beginning of the texture
rebinding operation to track user-facing texture switches. At the start of the
block where `!name.isEmpty() && name != "*Select a texture*"` is checked, call
SentryReporter::addBreadcrumb() with category 'ui.action' and a descriptive
message that includes the texture name being applied. This breadcrumb should be
added before the call to ensureTextureInMaterialGroup() to capture the entire
operation including the RTSS/material regeneration that follows through
getCurrentTextureUnit(), setTextureName(), and the material compilation steps.
🪄 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: 1651606c-2fa4-42c2-aa78-eb9073ce8816

📥 Commits

Reviewing files that changed from the base of the PR and between 1f1ab9c and 592f2d8.

📒 Files selected for processing (7)
  • qml/PropertiesPanel.qml
  • qml/SceneTreeNode.qml
  • qml/TexturePropertiesPanel.qml
  • src/MaterialEditorQML.cpp
  • src/MaterialEditorQML.h
  • src/SceneTreeModel.cpp
  • src/SceneTreeModel.h

Comment thread qml/SceneTreeNode.qml
Comment thread qml/TexturePropertiesPanel.qml
Comment thread src/MaterialEditorQML.cpp Outdated
Comment thread src/MaterialEditorQML.cpp Outdated
Comment thread src/MaterialEditorQML.cpp Outdated
Comment thread src/MaterialEditorQML.cpp Outdated
…bedded safety, stale pass maps

CodeRabbit/Codex review fixes:

- setTextureName: rebuild m_techMap via updateTechniqueList() BEFORE
  updateTextureUnitList() (which reads m_passMap via getCurrentPass()) so a
  prior recompile can't leave it dereferencing freed Pass objects. In the
  PS1 TMD branch, after collapsing passes reset m_selectedPassIndex to 0 and
  refresh the maps, then apply alpha-reject to pass 0 directly instead of via
  the (now stale) cached pointer.

- Preview cache: key by material resource group + texture name (FBX imports
  reuse basenames across per-asset groups) and never cache empty/transient
  misses. SD generation clears the whole (group-keyed) cache.

- Preview filenames: sanitize to a single safe basename (strip dirs / "..")
  before writing embedded/TIM previews under texture_previews.

- TIM textures: restore a CPU-side PNG preview via PS1TIM::loadTimToOgreImage
  (the convertToImage path that previously handled this was removed because
  it faults on GPU-only buffers).

- SceneTreeNode: stop fanning out availableMaterials() to every node on
  materialsChanged — refetch only for the open submesh dropdown, and fetch
  lazily when a dropdown is opened.

- TexturePropertiesPanel: treat Image.Null (empty source) like Image.Error
  and show the placeholder so clearing the preview can't leave a blank pane.

- setTextureName: add the ui.action Sentry breadcrumb for the texture switch.

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

Copy link
Copy Markdown
Owner Author

Addressed all review feedback in 6524379:

  • Stale pass maps (Critical)updateTechniqueList() runs before updateTextureUnitList(); TMD-pass collapse resets the selected pass to 0 and refreshes maps before use.
  • Preview cache scoping (Codex + CodeRabbit) — cache keyed by material resource group + texture name; empty/transient misses no longer cached.
  • TIM preview regression (Codex + CodeRabbit) — restored a CPU-side PNG preview via PS1TIM::loadTimToOgreImage (no GPU convertToImage).
  • Filename traversalsafePreviewBaseName() sanitizes embedded/TIM preview filenames.
  • SceneTreeNode fan-out — material refetch gated to the open dropdown + lazy fetch on open.
  • Empty preview paneImage.Null now falls back to the placeholder.
  • Breadcrumb — added ui.action breadcrumb for the texture switch.

fernandotonon and others added 2 commits June 17, 2026 09:43
…ty gate

No behavior change — addresses the SonarCloud new-code maintainability gate
(cognitive complexity 71, deep nesting, init-statement smells) on PR #734:

- Decompose computeTexturePreviewPath() into small file-local helpers
  (findLoadedTexture, timPreviewUrl, embeddedPreviewUrl,
  previewUrlFromOgreTexture, previewOutPath, fileUrl) — flat control flow,
  no >3-level nesting.
- Split setTextureName()'s post-bind work into collapseTmdMaterialToSinglePass()
  and regenerateRtssShaders(), and flatten the method with an early return.
- Catch the specific Ogre::Exception instead of catch(...), and use an
  if-init-statement for the preview-cache lookup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@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