Skip to content

HDR: Slice A — environment loader & cubemap conversion (#467)#778

Merged
fernandotonon merged 6 commits into
masterfrom
feat/hdr-slice-a-467
Jun 29, 2026
Merged

HDR: Slice A — environment loader & cubemap conversion (#467)#778
fernandotonon merged 6 commits into
masterfrom
feat/hdr-slice-a-467

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds HDREnvironmentManager singleton: loads .hdr (stb) / .exr (tinyexr) equirectangular maps, CPU-bakes a 6-face float cubemap, registers it with Ogre::TextureManager, and caches bakes in-memory by SHA-1 of source bytes.
  • Adds pure-data HdrEquirect helpers (load, equirect→cubemap, face-mean utilities) for unit testing without GL.
  • Adds build deps: stb (Radiance HDR), tinyexr + miniz behind ENABLE_OPENEXR (default ON).

Parent epic: #466 · Closes #467

Out of scope for this slice (later PRs): IBL precompute (B), RTSS wiring (C), skybox + tonemap compositor (D), Material Editor controls (E), bundled HDRIs (F), CLI/MCP parity (G).

Test plan

  • xvfb-run -a ./UnitTests --gtest_filter="HdrEquirectLoaderTest.*:HDREnvironmentManagerTest.*" (8 tests)
  • CI unit-tests-linux green
  • No visual/editor change expected — pipeline is API + tests only until Slice C/D

Made with Cursor

Summary by CodeRabbit

  • New Features
    • Added HDR environment loading from absolute or bundled asset names, with automatic conversion into in-app cubemap environments.
    • Added support for common .hdr files, plus optional .exr loading when HDR OpenEXR support is enabled in the build.
    • Added in-memory caching so reloading the same environment is faster.
  • Bug Fixes
    • Improved handling of missing/invalid HDR files and better cleanup when switching environments to avoid stale cubemap textures.

Add HDREnvironmentManager and a pure-data HdrEquirect pipeline so .hdr/.exr
equirectangular maps bake to Ogre float cubemaps with in-memory SHA-1 caching.
No shader, GUI, or CLI surface yet — foundation for epic #466 IBL work.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f715ac05-188b-4e01-9229-ec74dee91e38

📥 Commits

Reviewing files that changed from the base of the PR and between 1a815dd and 1c16fa5.

📒 Files selected for processing (4)
  • src/HDR/HDREnvironmentManager.cpp
  • src/HDR/HDREnvironmentManager.h
  • src/HDR/HDREnvironmentManager_test.cpp
  • src/HDR/HdrEquirectLoader.cpp
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/HDR/HDREnvironmentManager.h
  • src/HDR/HDREnvironmentManager_test.cpp
  • src/HDR/HDREnvironmentManager.cpp
  • src/HDR/HdrEquirectLoader.cpp

📝 Walkthrough

Walkthrough

Adds HDR environment loading with CPU equirect-to-cubemap baking, optional OpenEXR support, a singleton Ogre cubemap manager, CMake dependency wiring, and tests for baking, hashing, loading, and texture lifecycle.

Changes

HDR Environment Pipeline

Layer / File(s) Summary
CMake dependency wiring
CMakeLists.txt, src/CMakeLists.txt
Fetches stb unconditionally, adds ENABLE_OPENEXR with conditional tinyexr setup, creates tinyexr_miniz when needed, and wires new HDR sources plus include/link behavior for the app and unit tests.
HdrEquirectLoader data and bake path
src/HDR/HdrEquirectLoader.h, src/HDR/HdrEquirectLoader.cpp, src/HDR/HdrEquirectLoader_test.cpp
Declares float RGB image/cubemap face types and public bake APIs, implements .hdr/.exr loading, SHA-1 hashing, direction/UV transforms, cubemap baking, and face-mean helpers, with tests for synthetic baking, hashing, and EXR round-trip loading.
HDREnvironmentManager singleton and Ogre cubemap
src/HDR/HDREnvironmentManager.h, src/HDR/HDREnvironmentManager.cpp, src/HDR/HDREnvironmentManager_test.cpp
Declares and implements the singleton manager, path resolution, cached environment loading, manual Ogre cubemap creation, state updates, and environmentChanged() emission, with tests for lifecycle, cubemap properties, cache reuse, texture replacement, and missing-file failure.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant HDREnvironmentManager
  participant HdrEquirectLoader
  participant OgreTextureManager

  Client->>HDREnvironmentManager: loadEnvironment(path)
  HDREnvironmentManager->>HdrEquirectLoader: sha1HexOfFile(resolvedPath)
  HDREnvironmentManager->>HdrEquirectLoader: loadFromFile + bakeEquirectToCubemap
  HDREnvironmentManager->>OgreTextureManager: remove old texture
  HDREnvironmentManager->>OgreTextureManager: create/load cube texture
  HDREnvironmentManager->>Client: environmentChanged()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #467: This PR directly implements the HDR environment loader and cubemap conversion slice, including .hdr/.exr loading, equirect-to-cubemap baking, singleton manager wiring, and the related tests.
  • #466: This PR appears to implement the HDR lighting epic slice referenced by the direct issue.

Poem

🐇 I hopped through HDR skies so bright,
Six cubemap faces now hold the light.
SHA-1 in paw, I found the way,
stb and tinyexr joined the play.
Ogre hums when the textures bloom —
A bunny baked sunrise into the room.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description has a summary and test plan, but it is missing the required Technical Details section from the template. Add a Technical Details section with bullets for the build changes, HDR loader and cubemap pipeline, and any applicable checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: HDR environment loading and cubemap conversion.
Linked Issues check ✅ Passed The changes cover the singleton loader, HDR/EXR support, cubemap bake, Ogre registration, caching, tests, and the API required by #467.
Out of Scope Changes check ✅ Passed The PR stays focused on HDR loading, cubemap conversion, build deps, and tests with no clearly unrelated code changes.
✨ 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/hdr-slice-a-467

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.

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

ℹ️ 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/HDREnvironmentManager.cpp Outdated
#endif

for (const QString& candidate : candidates) {
const QString canon = QDir(candidate).canonicalPath();

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 Resolve bundled HDR files as files

When the caller passes a bundled name such as studio.hdr, each candidate is a full file path, but QDir(candidate).canonicalPath() treats that path as a directory and returns empty unless there is a directory named studio.hdr. As a result loadEnvironment("studio.hdr") never finds files under media/hdri/; use QFileInfo(candidate).canonicalFilePath()/exists() for the candidate file instead.

Useful? React with 👍 / 👎.

Comment thread src/HDR/HDREnvironmentManager.cpp Outdated
if (texMgr.resourceExists(texName))
texMgr.remove(texName);

m_cubemap = texMgr.createManual(

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 Remove the previous cubemap before replacing it

When switching from one HDR to a different HDR, this overwrites m_cubemap after only removing a resource with the new texture name. The old HdrEnv_* resource remains registered in Ogre::TextureManager (and the destructor only removes the current texture), so repeatedly trying environments leaks large PF_FLOAT32_RGB cube maps/GPU memory until shutdown.

Useful? React with 👍 / 👎.

Comment thread src/HdrEquirectLoader.cpp Outdated
const float phi = std::atan2(dir[2], dir[0]);
const float theta = std::asin(std::max(-1.f, std::min(1.f, dir[1])));
u = phi / kTwoPi + 0.5f;
v = theta / kPi + 0.5f;

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 Flip the equirectangular V coordinate

For standard equirectangular HDRIs, the top row (v=0) is the north/+Y pole, but this maps dir[1] == +1 to v=1 and dir[1] == -1 to v=0. Real .hdr/.exr environments will therefore bake upside down, inverting sky/ground lighting; the existing spherical projection code in src/UvProject.cpp uses the opposite 0.5f - asin(...) convention.

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

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

79-91: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

This cache test is too timing-dependent to prove a cache hit.

secondMs < firstMs + 50 still passes when the second load is slower, and wall-clock checks are noisy in CI. Please assert a deterministic cache-hit signal instead, even if that means adding a small test seam around the bake cache.

🤖 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/HDREnvironmentManager_test.cpp` around lines 79 - 91, The cache test in
HDREnvironmentManager_test is relying on elapsed wall-clock timing, which is
flaky and does not deterministically prove a cache hit. Update the test to
assert a stable cache-hit signal instead by adding a small test seam around the
bake cache or by checking a direct cache-related observable from
loadEnvironment/HDR environment state, and keep the current symbols like
mgr->loadEnvironment, currentCacheKey, and HdrEquirect::sha1HexOfFile as the
main anchors for locating the logic.
🤖 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 `@CMakeLists.txt`:
- Around line 343-347: The stb FetchContent declaration is using a mutable
branch tag, which makes builds non-reproducible; update the FetchContent_Declare
block for stb to use an immutable release tag or commit SHA instead of master,
following the same pinning approach used by the tinyexr declaration.

In `@src/HDREnvironmentManager_test.cpp`:
- Around line 32-66: The HDR environment tests are repeating Ogre setup inline
instead of enforcing the standard Ogre test prerequisites in a fixture. Move the
shared initialization for HDREnvironmentManagerTest into SetUp() on an
Ogre-specific fixture and have it assert both tryInitOgre() and
canLoadMeshFiles() before any test body runs. Then remove the ad hoc setup from
LoadEnvironment_CreatesCubeMapTexture and ReloadSameFile_UsesBakeCache so the
Ogre-backed cases consistently fail loudly when prerequisites are missing.

In `@src/HDREnvironmentManager.cpp`:
- Around line 63-66: The macOS-specific HDRI path fallback in
HDREnvironmentManager should use the Qt OS guard convention instead of the OGRE
platform macro. Update the conditional around the candidate path construction in
the relevant block of HDREnvironmentManager to use Q_OS_MACOS so it matches the
repo’s platform-guarding rules and stays consistent with other platform-specific
code.
- Around line 136-189: HDREnvironmentManager::loadEnvironment() is only
breadcrumbing the success path and uses an unsupported category, so move the
SentryReporter::addBreadcrumb call to cover the full import attempt and switch
it to the repo’s allowed file-import category. Add breadcrumb logging for
failure returns in loadEnvironment() and ensure both success and failure cases
emit file.import with a clear message so imports are visible in Sentry and
existing dashboards.
- Around line 50-57: The absolute-path handling in
HDREnvironmentManager::loadEnvironment should stop immediately when an absolute
QFileInfo path does not exist instead of continuing into the bundled-name
lookup. Update the early path checks around QFileInfo::isAbsolute(),
QFileInfo::exists(), and the subsequent fileName-based fallback so that only
non-absolute inputs can search bundled HDRIs, preserving the contract for
missing absolute files.
- Around line 103-133: The cubemap construction in
HDREnvironmentManager::buildCubemap is mutating m_cubemap directly, which can
leave the old Ogre::TextureManager resource registered on cacheKey changes and
also clears the active environment on mid-build failures. Build the new texture
into a temporary Ogre::TexturePtr first, validate and fill all six faces there,
and only swap it into m_cubemap after load succeeds; on failure, clean up the
temporary texture and preserve the existing cubemap state.

In `@src/HdrEquirectLoader.cpp`:
- Around line 142-149: The filename-based EXR loading in loadExrTiny still
relies on LoadEXR with QFile::encodeName(path), which can break Unicode paths on
Windows. Update loadExrTiny to avoid passing a narrow encoded filename to
tinyexr: read the EXR via QFile and load from memory, or switch to a
Windows-wide path/opening approach, while keeping the existing error handling
and FloatImage population logic intact.

---

Nitpick comments:
In `@src/HDREnvironmentManager_test.cpp`:
- Around line 79-91: The cache test in HDREnvironmentManager_test is relying on
elapsed wall-clock timing, which is flaky and does not deterministically prove a
cache hit. Update the test to assert a stable cache-hit signal instead by adding
a small test seam around the bake cache or by checking a direct cache-related
observable from loadEnvironment/HDR environment state, and keep the current
symbols like mgr->loadEnvironment, currentCacheKey, and
HdrEquirect::sha1HexOfFile as the main anchors for locating the logic.
🪄 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: 1ed985c5-8ad3-43b1-94de-60c3b8cd6e13

📥 Commits

Reviewing files that changed from the base of the PR and between bd34380 and fdb5516.

📒 Files selected for processing (9)
  • CMakeLists.txt
  • media/hdri/.gitkeep
  • src/CMakeLists.txt
  • src/HDREnvironmentManager.cpp
  • src/HDREnvironmentManager.h
  • src/HDREnvironmentManager_test.cpp
  • src/HdrEquirectLoader.cpp
  • src/HdrEquirectLoader.h
  • src/HdrEquirectLoader_test.cpp

Comment thread CMakeLists.txt
Comment thread src/HDREnvironmentManager_test.cpp Outdated
Comment thread src/HDR/HDREnvironmentManager.cpp
Comment thread src/HDREnvironmentManager.cpp Outdated
Comment thread src/HDR/HDREnvironmentManager.cpp Outdated
Comment on lines +136 to +189
bool HDREnvironmentManager::loadEnvironment(const QString& pathOrBundledName)
{
QElapsedTimer timer;
timer.start();

const QString resolved = resolvePath(pathOrBundledName);
if (resolved.isEmpty())
return false;

const QString cacheKey = HdrEquirect::sha1HexOfFile(resolved);
if (cacheKey.isEmpty())
return false;

const qint64 fileSize = QFileInfo(resolved).size();

CachedBake* bake = nullptr;
const bool fromBakeCache = m_bakeCache.contains(cacheKey);
if (fromBakeCache) {
bake = &m_bakeCache[cacheKey];
} else {
HdrEquirect::FloatImage equirect;
QString loadError;
if (!HdrEquirect::loadFromFile(resolved, equirect, loadError))
return false;

const int faceSize = HdrEquirect::defaultFaceSizeForEquirect(equirect.width);
CachedBake entry;
entry.sourcePath = resolved;
entry.cacheKey = cacheKey;
entry.faceSize = faceSize;
QString bakeError;
if (!HdrEquirect::bakeEquirectToCubemap(equirect, faceSize, entry.faces, bakeError))
return false;
entry.textureName = QStringLiteral("HdrEnv_%1").arg(cacheKey.left(16));
m_bakeCache.insert(cacheKey, std::move(entry));
bake = &m_bakeCache[cacheKey];
}

QString ogreError;
if (!createOgreCubemap(cacheKey, bake->faces, ogreError))
return false;

m_currentPath = resolved;
m_cacheKey = cacheKey;
m_faceSize = bake->faceSize;

SentryReporter::addBreadcrumb(
QStringLiteral("render.hdr.load"),
QStringLiteral("path=%1 bytes=%2 faceSize=%3 loadMs=%4 cached=%5")
.arg(QFileInfo(resolved).fileName())
.arg(fileSize)
.arg(m_faceSize)
.arg(timer.elapsed())
.arg(fromBakeCache ? QStringLiteral("yes") : QStringLiteral("no")));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Instrument this import path with the repo’s breadcrumb contract.

loadEnvironment() is a user-facing file import, but only the success path is logged and the category render.hdr.load is outside the repo’s allowed set. Failed imports will disappear from Sentry, and the current category will miss the existing file.import dashboards. As per coding guidelines, all user-facing actions and significant operations must add a Sentry breadcrumb via SentryReporter::addBreadcrumb(category, message) using the established categories (ui.action, ai.tool_call, file.import, file.export).

🤖 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/HDREnvironmentManager.cpp` around lines 136 - 189,
HDREnvironmentManager::loadEnvironment() is only breadcrumbing the success path
and uses an unsupported category, so move the SentryReporter::addBreadcrumb call
to cover the full import attempt and switch it to the repo’s allowed file-import
category. Add breadcrumb logging for failure returns in loadEnvironment() and
ensure both success and failure cases emit file.import with a clear message so
imports are visible in Sentry and existing dashboards.

Source: Coding guidelines

Comment thread src/HdrEquirectLoader.cpp Outdated
fernandotonon and others added 3 commits June 29, 2026 11:08
FetchContent_MakeAvailable pulled in tinyexr's test build, which fails on
macOS with -Wpoison-system-directories. We only need the header + miniz.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Flip equirect V to match Radiance/UvProject pole convention
- Resolve bundled HDRI paths as files; reject missing absolute paths
- Release previous Ogre cubemap when switching environments
- Load EXR from memory (Unicode-safe); pin stb to commit SHA
- Add Ogre test fixture and cubemap-release regression test

Co-authored-by: Cursor <cursoragent@cursor.com>
Group HDREnvironmentManager and HdrEquirectLoader (plus tests) under
src/HDR/ with PS1-style HDR/… includes for a clearer epic layout.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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.

🧹 Nitpick comments (1)
src/HDR/HDREnvironmentManager_test.cpp (1)

83-94: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Avoid timing as the cache oracle.

EXPECT_LT(secondMs, firstMs + 50) is both loose and CI-sensitive; it can pass without proving a bake-cache hit. Prefer a deterministic observable/test hook for cache reuse, or assert a counter/stat emitted by the manager.

🤖 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/HDR/HDREnvironmentManager_test.cpp` around lines 83 - 94, The cache reuse
check in HDREnvironmentManager_test is relying on elapsed time, which is flaky
and doesn’t deterministically prove a cache hit. Replace the timing-based
EXPECT_LT on firstMs/secondMs with a stable assertion using an observable from
HDREnvironmentManager or HdrEquirect, such as a cache-hit counter, bake/reuse
stat, or another test hook that confirms the second loadEnvironment(path) call
reused the cached result.
🤖 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.

Nitpick comments:
In `@src/HDR/HDREnvironmentManager_test.cpp`:
- Around line 83-94: The cache reuse check in HDREnvironmentManager_test is
relying on elapsed time, which is flaky and doesn’t deterministically prove a
cache hit. Replace the timing-based EXPECT_LT on firstMs/secondMs with a stable
assertion using an observable from HDREnvironmentManager or HdrEquirect, such as
a cache-hit counter, bake/reuse stat, or another test hook that confirms the
second loadEnvironment(path) call reused the cached result.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2d346bd6-772d-4708-b8e6-ff338e4da71e

📥 Commits

Reviewing files that changed from the base of the PR and between fdb5516 and 1a815dd.

📒 Files selected for processing (8)
  • CMakeLists.txt
  • src/CMakeLists.txt
  • src/HDR/HDREnvironmentManager.cpp
  • src/HDR/HDREnvironmentManager.h
  • src/HDR/HDREnvironmentManager_test.cpp
  • src/HDR/HdrEquirectLoader.cpp
  • src/HDR/HdrEquirectLoader.h
  • src/HDR/HdrEquirectLoader_test.cpp
💤 Files with no reviewable changes (1)
  • src/HDR/HdrEquirectLoader.h
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/CMakeLists.txt
  • CMakeLists.txt

fernandotonon and others added 2 commits June 29, 2026 12:17
Address EXR load undefined-data (S836), RAII cleanup, deprecated isNull,
singleton ownership, const_cast on cubemap upload, and C-style array usage
so the PR passes the SonarCloud reliability gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy TinyEXR pixels into std::vector before free, use init-statement ifs,
and match project singleton NOSONAR pattern so S836 no longer blocks the gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon fernandotonon merged commit 2b93d62 into master Jun 29, 2026
21 checks passed
@fernandotonon fernandotonon deleted the feat/hdr-slice-a-467 branch June 29, 2026 21:33
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.

HDR: Slice A — HDR environment loader & cubemap conversion

1 participant