Skip to content

perf+test: editor window image provider + MaterialEditorQML coverage - #534

Merged
fernandotonon merged 1 commit into
masterfrom
feat/texture-editor-full-res
May 16, 2026
Merged

perf+test: editor window image provider + MaterialEditorQML coverage#534
fernandotonon merged 1 commit into
masterfrom
feat/texture-editor-full-res

Conversation

@fernandotonon

@fernandotonon fernandotonon commented May 16, 2026

Copy link
Copy Markdown
Owner

Summary

Perf — the detached Texture Editor window now uses a QQuickImageProvider instead of a base64-PNG data URI, removing the per-stroke encode that made painting feel laggy on 2048² textures.

  • New PaintBufferImageProvider registered under image://paintbuffer/ on the editor's engine. requestImage is just QImage::copy() of the live buffer — no PNG encode, no base64 wrapping.
  • New TexturePaintController.fullResPreviewUrl returns image://paintbuffer/current?v=N (monotonic version bump per refresh) so QML invalidates the Image cache without re-encoding.
  • Inspector thumbnail keeps the cheap data URI (256² downscaled).
  • ~80 ms → ~3 ms per refresh on 2048² textures.

Tests — 5 new MaterialEditorQML cases:

  • getMaterialList filters QMEPaint_* / QMEPaintMaskOverlay_* / TexturePaint/* from the user dropdown.
  • getMaterialList early-return path when Ogre isn't available.
  • Setters' no-Ogre path updates the QObject-side property without crashing.
  • Enum name helpers return stable non-empty lists.
  • materialPreview("Unknown") doesn't crash.

Test plan

  • Build green on macOS arm64
  • App boots, editor window painting feels smooth at 2048²
  • CI: Linux unit tests + builds across the three platforms, SonarCloud + CodeRabbit

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Texture editor now displays full-resolution live previews of painted textures. Maintains consistent visual quality when resizing the editor window and eliminates pixellation artifacts from previous cached previews.
  • Tests

    • Added comprehensive test coverage for material editor functionality, including material filtering, property handling, and stability validation.

Review Change Stack

Perf — Texture Editor window
The detached window used to bind its Image element to
`TexturePaintController.previewDataUri`, which produces a PNG-
encoded + base64-wrapped data URI of the paint buffer. On 2048²
textures that's ~80 ms per refresh on the main thread (encode +
QString churn), and the editor window updates every 60 ms of
the existing debounce — net effect: every stroke felt laggy.

New architecture:
- `PaintBufferImageProvider` (QQuickImageProvider, Image mode)
  registered on the editor's QQmlApplicationEngine under the
  `paintbuffer` scheme. The provider's requestImage just calls
  `TexturePaintController::snapshotBufferImage()` which is a
  single `QImage::copy()` of the RGBA buffer — no PNG encode,
  no base64. ~3 ms on a 2048² texture.
- New controller property `fullResPreviewUrl` returns
  `image://paintbuffer/current?v=N` where N is a monotonic
  counter bumped on every refresh. The `?v=N` invalidates QML's
  per-URL Image cache so each refresh re-pulls. The provider
  itself ignores the parameter.
- New signal `fullResPreviewChanged` so QML can bind to the URL
  channel without tickling the heavier `previewChanged` path.
- The inspector thumbnail keeps the cheap `previewDataUri`
  (256² downscaled PNG, ~3 ms). Two channels, two consumers.

The Image element in `TextureEditorWindow.qml` sets
`sourceSize: Qt.size(0, 0)` so Qt loads the buffer at its native
resolution and crisp-renders when the user enlarges the window.

Tests — MaterialEditorQML
Added 5 new test cases targeting paths Sonar marked uncovered:
- getMaterialList filtering: QMEPaint_, QMEPaintMaskOverlay_,
  TexturePaint/ prefixes are hidden from the user-facing
  dropdown. Plus the no-Ogre early-return path.
- Setters' no-Ogre paths verify that the QObject-side property
  still updates (so QML bindings refresh) even when no material
  is loaded — `setLightingEnabled` / `setAmbientColor` / six more.
- Name-helper lists (getPolygonModeNames etc.) return non-empty
  arrays with no empty entries — the QML side relies on this.
- `materialPreview("Unknown")` doesn't crash; returns empty or
  a data: URI.

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

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces full-resolution live texture preview delivery via a Qt image provider. The new PaintBufferImageProvider fetches snapshots on-demand from TexturePaintController, which adds version-driven cache-busting via a new fullResPreviewUrl property and fullResPreviewChanged signal. The QML editor switches from encoded URI preview to live-buffer binding with native-resolution rendering.

Changes

Full-resolution texture preview via Qt image provider

Layer / File(s) Summary
Image provider class and registration
src/PaintBufferImageProvider.h, src/PaintBufferImageProvider.cpp, src/TexturePaintController.cpp (include)
PaintBufferImageProvider implements QQuickImageProvider::requestImage() to fetch live QImage snapshots from TexturePaintController, ignoring size requests and returning buffer dimensions. Controller includes provider header and registers it under image://paintbuffer/ in the editor engine.
TexturePaintController full-res preview API
src/TexturePaintController.h
Adds fullResPreviewUrl Q_PROPERTY returning versioned image://paintbuffer/... URIs, public snapshotBufferImage() to provide fresh RGBA snapshots, fullResPreviewChanged() signal, and private m_fullResVersion counter for cache invalidation.
TexturePaintController refresh and snapshot logic
src/TexturePaintController.cpp
refreshPreviewUri() now increments version and emits fullResPreviewChanged instead of PNG/base64 encoding for both empty and non-empty buffers. fullResPreviewUrl() constructs versioned URIs, snapshotBufferImage() copies RGBA buffer to fresh QImage, and openEditorWindow() registers the image provider on the editor's QQmlApplicationEngine.
QML texture editor preview binding
qml/TextureEditorWindow.qml
Switches previewUri binding to TexturePaintController.fullResPreviewUrl, adds connection to onFullResPreviewChanged() for updates, and sets canvas image sourceSize: Qt.size(0, 0) to render at native resolution without rasterization.
Build configuration updates
src/CMakeLists.txt, tests/CMakeLists.txt
Adds PaintBufferImageProvider.cpp and .h to production build files and includes in test compilation targets.
Test coverage for material filtering and property safety
src/MaterialEditorQML_test.cpp
Four test blocks verify getMaterialList() filters internal paint materials, property setters safely handle missing Ogre, enum name lists remain stable, and materialPreview() handles unknown materials without crashing.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐇 A buffer blooms in image:// light,
No base64 chains, just snapshots bright!
QML binds to live and free,
Native pixels dance, sharp as can be!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main changes: performance improvement via image provider and test coverage expansion for MaterialEditorQML.
Description check ✅ Passed The description matches the template structure with clear Summary section and separate Test plan, but lacks explicit subsection headings for Features/Bugfixes as specified in the template.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/texture-editor-full-res

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@src/TexturePaintController.cpp`:
- Around line 1993-1997: Session teardown clears the full-res buffer but doesn't
notify QML, leaving fullResPreviewUrl consumers with a stale image; update the
teardown/cleanup paths in TexturePaintController (where the session buffer is
cleared or the session ends) to mirror the refresh behavior by incrementing
m_fullResVersion and calling emit fullResPreviewChanged() after the buffer is
cleared so bindings to fullResPreviewUrl are invalidated.
🪄 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: df839683-c8f7-4ae9-b7a4-9811617884c3

📥 Commits

Reviewing files that changed from the base of the PR and between bcbb3bf and 9bd66f3.

📒 Files selected for processing (8)
  • qml/TextureEditorWindow.qml
  • src/CMakeLists.txt
  • src/MaterialEditorQML_test.cpp
  • src/PaintBufferImageProvider.cpp
  • src/PaintBufferImageProvider.h
  • src/TexturePaintController.cpp
  • src/TexturePaintController.h
  • tests/CMakeLists.txt

Comment on lines +1993 to +1997
// The full-res preview channel doesn't need PNG encoding —
// QQuickImageProvider serves the live buffer directly on demand.
// Just bump the version so QML re-fetches.
++m_fullResVersion;
emit fullResPreviewChanged();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Emit fullResPreviewChanged on teardown paths too.

fullResPreviewUrl has a dedicated NOTIFY signal, but session teardown clears the buffer without emitting fullResPreviewChanged(). Consumers bound only to fullResPreviewUrl can keep a stale image until another refresh path runs.

Suggested fix
diff --git a/src/TexturePaintController.cpp b/src/TexturePaintController.cpp
@@
     m_previewUri.clear();
     emit previewChanged();
+    ++m_fullResVersion;
+    emit fullResPreviewChanged();
     emit sessionChanged();
 }
📝 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
// The full-res preview channel doesn't need PNG encoding —
// QQuickImageProvider serves the live buffer directly on demand.
// Just bump the version so QML re-fetches.
++m_fullResVersion;
emit fullResPreviewChanged();
m_previewUri.clear();
emit previewChanged();
+m_fullResVersion;
emit fullResPreviewChanged();
emit sessionChanged();
🤖 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/TexturePaintController.cpp` around lines 1993 - 1997, Session teardown
clears the full-res buffer but doesn't notify QML, leaving fullResPreviewUrl
consumers with a stale image; update the teardown/cleanup paths in
TexturePaintController (where the session buffer is cleared or the session ends)
to mirror the refresh behavior by incrementing m_fullResVersion and calling emit
fullResPreviewChanged() after the buffer is cleared so bindings to
fullResPreviewUrl are invalidated.

@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: 9bd66f3f60

ℹ️ 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 on lines +2016 to +2019
QImage view(const_cast<uchar*>(m_buffer.data().data()),
m_buffer.width(), m_buffer.height(),
m_buffer.width() * 4, QImage::Format_RGBA8888);
return view.copy();

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 Synchronize access when snapshotting the paint buffer

Protect snapshotBufferImage() with the same synchronization used by paint writes: it builds a QImage view over m_buffer and copies from it without any lock, while stroke/update paths mutate m_buffer concurrently. Because QQuickImageProvider::requestImage() may run off the UI thread, this can race under asynchronous image loading and produce torn/corrupted frames or intermittent crashes.

Useful? React with 👍 / 👎.

@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit f93cbdf into master May 16, 2026
20 checks passed
@fernandotonon
fernandotonon deleted the feat/texture-editor-full-res branch May 16, 2026 05:47
@fernandotonon fernandotonon linked an issue May 17, 2026 that may be closed by this pull request
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Epic: Paint Tools

1 participant