Shader library pruning drops shaders still referenced by incrementally-cooked packages (missing shader resources / uncooked shader map in cooked builds)

Some of our automated LoadMap tests are failing randomly due to missing shader assets. We used a coding agent to comb through the run and build logs, and made some diagnostic runs, and it identified a potential issue with the shader library pruning. We want to verify if this is a legit issue/fix and not just a hallucination on the AI’s part.

Proposed fix

In FEditorShaderCodeArchive::UpdateOplogPackages(), when a committed package has no oplog data, mark it as referenced by both oplog and staging so the prune step cannot discard its shaders:

void FEditorShaderCodeArchive::UpdateOplogPackages(UE::Cook::Artifact::FUpdateOplogPackagesContext& Context)
{
	...
	for (TPair<FName, FShaderMapAssetAssociations::FAssociatedAssetData>& Pair : AssetMap.ViewAssets())
	{
		...
		if (PackageName.IsNone())
		{
			...
		}
		else
		{
			const FOplogPackageData* OplogData = OplogPackages.Find(PackageName);
			if (OplogData)
			{
				...
			}
			// ENGINE_MOD_BEGIN
			else
			{
				// The package is neither in this cook's referenced set nor in the persistent oplog. Leaving the asset
				// unreferenced here would cause Prune() to drop every shadermap associated with it, so the asset would
				// silently lose its shaders in the deployed build. We cannot positively determine that the asset is
				// stale (the oplog may be incomplete, e.g. for shader libraries seeded from a shared/source cache),
				// so conservatively keep it referenced by both oplog and staging.
				Pair.Value.bReferencedByOplog = true;
				Pair.Value.bReferencedByStaging = true;
			}
			// ENGINE_MOD_END
		}
	}
}

The other fix it proposed is to set bIncrementalCookPruningEnabled to false, as the comment indicates it might be unstable.

Here’s the AI analysis of the issue:

Summary

In projects that use incremental cooking (-CookIncremental, IoStore/-ZenStore), a cook can produce a package whose materials reference shader maps that are absent from the published shader library. The cook itself reports no error; the failure only appears at runtime/package-validation time when the cooked assets are serialized and the missing hash is looked up.

Symptoms (runtime / packaged build log)

LogShaders: Error: Missing shader resource for hash ‘<HASH>’ for shader platform ‘PCD3D_SM6’

in the shader library while serializing asset <ASSET>

LogMaterial: Error: Tried to access an uncooked shader map ID in a cooked application. Material: <MATERIAL>

And in-game the material falls back to the default material. The affected assets are intermittent/rotating between cooks (not a fixed asset), which is the tell that this is a pruning/reference-tracking bug rather than a bad asset.

Note that the cook log shows no Failed to compile Material … error for these cases — the shaders compiled fine; they were simply pruned from the library after being cooked.

Root cause

During incremental cook, the shader library prune step decides which shader maps to keep based on oplog package reference data (FOplogPackageData::ReferenceType, default Unreferenced). FEditorShaderCodeArchive::UpdateOplogPackages() walks the committed packages for the platform and marks each package’s reference type.

When a package has no oplog entry (OplogData == nullptr), the else branch failed to mark the package as referenced. Such a package’s shaders were therefore treated as unreferenced by the prune step and were dropped from the archive, even though the package is committed and will be serialized in the cooked build. This matches the observed behavior: shaders referenced only by packages without oplog data vanish, and the affected set changes as the incremental oplog changes between builds.

Relevant code path:

FEditorShaderCodeArchive::InitializeReferenceTracking() — sets up tracking

FEditorShaderCodeArchive::UpdateOplogPackages() — assigns reference type per committed package

FEditorShaderCodeArchive::Prune() — removes shaders not referenced by oplog/staging

FOplogPackageData (CoreUObject/Public/Cooker/CookArtifact.h) — ReferenceType defaults to Unreferenced

[Attachment Removed]

Steps to Reproduce

  1. Cook with IoStore + incremental cooking enabled (project with a global shader library + a class-based shader library).
  2. Force a package’s shader map to be re-cooked while its oplog entry is absent/skipped. We don’t know what caused it to be absent in the first place, so we added code to manually skipped a specific asset just for repro.
  3. Serialize the cooked asset / run package validation and load the cooked results.
  4. Observe Missing shader resource for hash '...' ... while serializing asset ..., and at runtime Tried to access an uncooked shader map ID in a cooked application.

[Attachment Removed]

The change proposed by your AI causes a bloat problem but is otherwise okay. If we don’t drop the shaders of truly no-longer-cooked packages then they remain on disk and over time as packages are deleted or become unreferenced and the shaderlibrary grows unnecessarily to contain these orphaned shaders. That does not cause any behavior problem however because nothing reads those shaders outside of the incremental cook, which just loads them without interpreting them and saves them back to the shader library each cook.

Despite not causing a behavior problem we don’t want to take this change into the engine, because we want to avoid the bloat.

The missing op for a package still in use in the runtime is unexpected; we haven’t seen that locally. If the package is in use by the runtime, the cooker is supposed to find it during its graph search of all runtime-referenced packages, and if it is not in the oplog (or is modified) then it gets recooked and added to the oplog, so that by the time the end of cook UpdateOplogPackages function is called it now exists in the oplog.

Further, during staging, only the packages that the cooker found (and therefore added to the oplog if not already there) are supposed to be cooked. The cooker creates a ReferencedSet.txt file (stored in the oplog), and CopyBuildToStagingDirectory.Automation.cs and UnrealPak read that ReferencedSet file from the oplog, and only cook the packages mentioned in that referenced set. So they should not be referencing the old package that does not exist in the oplog, and therefore should not be referencing its shaders that were pruned.

When the problem occurs, is the package present in <ProjectRoot>\Saved\Cooked\<Platform>\<ProjectName>\Metadata\ReferencedSet.txt

If not, can you find out how the package is still getting referenced during staging? CookPackageStore.cpp calls ZenStoreClient->GetOplog().Get(), with the default argument to GetOplog bTrimByReferencedSet=true, so it is supposed to not read any ops that do not exist in the referenced set.

Also if not, if you do a full recook afterwards, does it then appear?

If the op is present in the ReferencedSet.txt when the problem occurs, then there must be something wrong in the cooker and it is passing in an incorrect value for Context.GetOplogPackages.

[Attachment Removed]