CNA-BUG-121: ContentManager::Load<T> never calls the virtual OpenStream, so ResourceContentManager::Load<T> ignores its resource family
Evidence basis: source-verified at the pinned commit; 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.
OpenStream is documented as the seam every .xnb load goes through, but the public Load template reads files with TryReadAssetBytes; only the protected ReadAsset calls it, so an OpenStream override is bypassed.
- Identifier
CNA-BUG-121- Category
- Bug
- Subsystem
- Content & XNB/CNB/CNJ
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Medium (a triage suggestion, not a project priority)
- Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- ContentManager::Load and the protected virtual ContentManager::OpenStream; ResourceContentManager constructed with a System::Resources::ResourceManager
Expected behaviour
In XNA 4.0 Load<T> calls ReadAsset<T>, which reads through the virtual OpenStream, so a derived manager (ResourceContentManager, a pack-file or archive manager) serves the public Load<T> from its own source. CNA's header says the same of OpenStream: "every .xnb load goes through it, so an override changes where compiled content comes from", and ResourceContentManager's class comment calls it "what makes a game that embeds its compiled content work without shipping a Content directory".
Actual behaviour at TARGET
Load<T> (ContentManager.hpp) reads the .xnb tier with TryReadAssetBytes(ResolveExistingAssetPath(BuildAssetPath(name) + ".xnb")); OpenStream is called only from ReadAsset<T>. A ResourceContentManager (ResourceContentManager.cpp) has an empty root, so its Load<T>("x") looks for x.xnb in the working directory, then the .cnb and loose tiers, and never asks its ResourceManager: it throws ContentLoadException, or loads an unrelated same-named file from the working directory.
Source locations
modules/content/include/Microsoft/Xna/Framework/Content/ContentManager.hpp— ContentManager::Load (XNB tier via TryReadAssetBytes), OpenStream and ReadAsset documentationmodules/content/src/Xna/ContentManager.cpp— ContentManager::OpenStream - called only by ReadAssetmodules/content/src/Xna/ResourceContentManager.cpp— ResourceContentManager::OpenStream overridemodules/content/include/Microsoft/Xna/Framework/Content/ResourceContentManager.hpp— class documentation
Evidence
Checked by reading at 009d40f5; not executed. XNA's call chain taken from the decompiled XNA 4.0 ContentManager. The site's ContentManager guide and XNB guide already state that the Load<T> ladder does not route through OpenStream; CNA's own header documentation says the opposite.
Focused reproduction
Illustrative; not compiled.
System::Resources::ResourceManager resources = /* family holding "level1" as a byte[] XNB */;
ResourceContentManager content(&services, &resources);
auto level = content.Load<LevelData>("level1");
// XNA: read from the resource family. CNA: searches ./level1.xnb, .cnb and loose files,
// then throws ContentLoadException.
Current tests
ResourceContentManagerTests.cpp and ContentRuntimeContractTests.cpp drive OpenStream and ReadAsset<T> directly; no test calls the public Load<T> on a manager that overrides OpenStream.
Regression test
In ContentRuntimeContractTests, call Load<ContractValue> on the ProbeContentManager and require openStreamCalls == 1 and the served value; in ResourceContentManagerTests, require Load<ResourceValue>("fixture") to return the resource's value with no content directory present, and a second call to hit the cache.
Blast radius
Every ContentManager subclass that overrides OpenStream (resource families, pack files, archives) and every port that calls Load<T> on one. Plain managers are unaffected: the base OpenStream and Load<T> read the same files.
Workaround
In the derived manager, expose a public method that calls the protected ReadAsset<T>(name, {}) and caches the result, and call it instead of Load<T>.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Internals
- Content runtime: inconsistencies
- Known issues
- Bug index