CNA-BUG-041: ContentManager resolves a relative RootDirectory against the working directory, not TitleLocation, and its .cnb and loose tiers cannot see Android packaged assets
Evidence basis: source-verified at the pinned commit; executed for this entry (the Evidence section names exactly what was run); tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
TitleContainer now has consumers and the .xnb tier reaches Android packaged assets, but ContentManager never consults TitleLocation::Path, and its .cnb and loose tiers gate on std::filesystem::exists.
- Identifier
CNA-BUG-041- Category
- Bug
- Subsystem
- Content & XNB/CNB/CNJ
- Status
- Narrowed (partially fixed; describes only what survives)
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Medium (a triage suggestion, not a project priority)
- Evidence basis
- Reproduced: executed for this entry (the Evidence section names exactly what was run)
- Tests touching this area
- Yes: see Current tests
- Affected contract
- ContentManager::RootDirectory and ContentManager::Load path resolution (XNA resolves through TitleContainer.OpenStream, relative to the title location)
Expected behaviour
XNA 4.0's ContentManager.OpenStream combines RootDirectory with the asset name and opens it through TitleContainer.OpenStream, which resolves a relative path against the title location (the executable's directory), independent of the working directory. CNA's own TitleContainer::ResolveRealPath (TitleContainer.cpp) resolves through TitleLocation::getPathProperty(), which is the platform's GetBasePath(): on the SDL3 platform that is SDL_GetBasePath() (Sdl3SystemServices.cpp), the executable's directory, so there TitleContainer follows XNA's rule; CNA's docs/apple-platforms.md states that "Content is loaded relative to SDL_GetBasePath()". On Android every content tier should read the APK's packaged assets.
Actual behaviour at TARGET
BuildAssetPath joins rootDirectory_ and the asset name; ResolveExistingAssetPath, TryReadAssetBytes and ResolveAssetPath (ContentManager.cpp, ContentManager.hpp) treat a relative result as relative to the process working directory. No ContentManager code path reads TitleLocation or GetBasePath(), and nothing in the runtime changes the working directory. A game started from another directory (a shortcut with a different start-in folder, a terminal elsewhere, an app bundle) does not find its default Content root unless it sets an absolute one.
On Android only TryReadAssetBytes forwards a relative path to the platform file system, so the .xnb tier and the base OpenStream reach packaged assets. The .cnb tier and ResolveAssetPath (the .cnj and extension probing of the loose tier) test std::filesystem::exists, which cannot see APK assets, and the built-in Texture2D loose reader then opens through ImageLoader::Load's std::ifstream.
Fixed since first recorded: TitleContainer and TitleLocation have in-tree consumers (CnaCApiGameProperties.cpp), and the .xnb tier's Android route through Sdl3FileSystem::TryLoadFileIgnoringCase replaces the unreachable TitleContainer fallback the original finding described.
Source locations
modules/content/src/Xna/ContentManager.cpp— ContentManager::BuildAssetPath, ResolveExistingAssetPath and TryReadAssetBytesmodules/content/include/Microsoft/Xna/Framework/Content/ContentManager.hpp— ContentManager::Load (CNB tier) and ResolveAssetPath - std::filesystem::exists gatesmodules/runtime/src/TitleContainer.cpp— TitleContainer::ResolveRealPath / CombineTitlePath - title-relative resolutionmodules/runtime/src/TitleLocation.cpp— TitleLocation::DetectBasePathmodules/graphics/src/Internal/ImageLoader.cpp— ImageLoader::Load - reads through std::ifstreamdocs/apple-platforms.md— Storage locations - the base-path claim
Evidence
Checked by reading at 009d40f5; not run on Android or an Apple device; the working-directory divergence itself was executed (see below). Whether SDL_LoadFile (reached by TryReadAssetBytes' platform fallback) resolves a relative name inside an Apple app bundle depends on SDL's own I/O layer and was not checked, so the Apple leg of the .xnb tier may work; the .cnb and loose tiers never reach that fallback. The site's ContentManager guide documents the working-directory rule, and the content-runtime internals page hedges the Android .cnb question; this entry supplies the reading for both. The disagreement with TitleContainer exists only on the SDL3 platform: the SDL2, X11, Wayland, native Win32, headless and terminal platforms answer GetBasePath() through StandardFileSystem::GetBasePath (StandardFileSystem.cpp), which returns the working directory, so on those platforms TitleLocation and ContentManager agree with each other and both differ from XNA's executable-directory rule.
Independently observed as a separate finding (merged): Unlike the .xnb tier, which reads through the platform file system, the .cnb tier asks std::filesystem::exists and CnbDocument::ParseFile, which cannot see Android packaged assets.
Independent re-verification: Checked by reading at 009d40f5, plus one audit run for the central claim. A probe built from the TARGET ContentManager, TitleContainer and TitleLocation sources (headless platform, TitleLocation set by hand to the directory holding Content, working directory changed to /) showed TitleContainer::OpenStream finding the asset while ContentManager with the relative root "Content" threw ContentLoadException, so ContentManager resolves against the working directory. The SDL3 default, where SDL_GetBasePath supplies the executable directory, and the Android and Apple legs were not run. Whether SDL_LoadFile (reached by TryReadAssetBytes' platform fallback) resolves a relative name inside an Apple app bundle depends on SDL's own I/O layer and was not checked, so the Apple leg of the .xnb tier may work; the .cnb and loose tiers never reach that fallback. The site's ContentManager guide documents the working-directory rule, and the content-runtime internals page hedges the Android .cnb question; this entry supplies the reading for both. The disagreement with TitleContainer exists only on the SDL3 platform: the SDL2, X11, Wayland, native Win32, headless and terminal platforms answer GetBasePath() through StandardFileSystem::GetBasePath (StandardFileSystem.cpp), which returns the working directory, so on those platforms TitleLocation and ContentManager agree with each other and both differ from XNA's executable-directory rule.
Focused reproduction
Illustrative; the divergence was executed by the audit (see Evidence).
# A game installed as /opt/mygame/mygame with /opt/mygame/Content/ beside it.
cd /opt/mygame && ./mygame # content loads: working directory == executable directory
cd / && /opt/mygame/mygame # Load<T> throws ContentLoadException for every asset,
# although TitleContainer::OpenStream("Content/x.xnb") finds it (on the SDL3 platform, the only one that derives the title location from the executable's directory)
Current tests
UnicodeContentRootTests.cpp and ContentPathContainmentTests.cpp use absolute roots on a desktop host. No test runs ContentManager with a working directory different from the title location, and no Android or Apple content-path test runs in CnaTests.
Regression test
Set TitleLocation::setPathProperty to a scratch directory holding Content/x.xnb, x2.cnb and x3.png, change the working directory elsewhere, and require all three Load<T> calls with the relative root "Content" to succeed. An Android device test should load one asset per tier from the APK.
Blast radius
Every ContentManager with a relative root (the Game's own manager defaults to "Content") on a desktop host launched from another directory; on Android, every .cnb, .cnj and loose asset shipped inside the APK. Absolute roots, and .xnb content on Android, are unaffected. On the platforms whose base path is the working directory (SDL2, X11, Wayland, native Win32, headless, terminal) TitleLocation follows the working directory too, so a root built from it does not help there.
Workaround
On the SDL3 platform, set an absolute root built from the title location, for example content.setRootDirectoryProperty(TitleLocation::Path() + "Content") (SDL_GetBasePath() normally ends in a separator; check it on your targets). On the SDL2, X11, Wayland and native Win32 platforms TitleLocation::Path() is the working directory, so build the root from the executable's own directory by host means or start the game from its directory. On Android ship run-time content as .xnb.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Internals
- Content runtime: path helpers
- Known issues
- Bug index