How to mount pak files without breaking the loading of other assets?

Hi,

I am working on an 3D viewer based on Unreal Engine. We need to enable users to load models packaged separately, not with the main app. I found this tutorial provided by Epic which explains how to do this with pak files. I also found this primer about pak files, but I have to admit that it’s still blurry for me to understand how “mount points” work.

Following the tutorial mentioned above, I created a pak file, containing a 3D model with its textures. It is displayed in a browser (as 2D thumbnail) and can be dragged and dropped in the scene. Below is the code of the method starting the dragging process. It uses a class named APopulation containing a UFoliageInstancedStaticMeshComponent (to instantiate the object), which works well with 3D models packaged with the application. The UBFL_Pak namespace is from the Epic tutorial.

if (!pakPath.IsEmpty() && UBFL_Pak::MountPakFile(pakPath, FString()))
{
	const FString mountPoint = UBFL_Pak::GetPakMountContentPath(pakPath);
	UBFL_Pak::RegisterMountPoint(FString("/Game/"), mountPoint);

	TArray<FString> pakContent = UBFL_Pak::GetPakContent(pakPath);
	for (auto& pakElement : pakContent)
	{
		pakElement = FString("/Game/") + pakElement.LeftChop(7); // remove ".uasset"
	}

	FAssetRegistryModule& assetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
	assetRegistryModule.Get().ScanFilesSynchronous(pakContent, true);

	unmountPak = true;
}

APopulation* population = gm->GetOrCreatePopulation(GetAssetPathForLoading());

[code initializing instTransform (an FTransform)]

population->AddInstance(instTransform);

Later in the function ending the dragging process (object is dropped), I have this:

if (!pakPath.IsEmpty() && unmountPak)
{
	FString mountPoint = UBFL_Pak::GetPakMountContentPath(pakPath);
	FPackageName::UnRegisterMountPoint(FString("/Game/"), mountPoint);
}

The problem is that after calling UnRegisterMountPoint, Unreal Engine can’t load the necessary mip-maps for textures of the added model when moving the point of view. It shows this error “Texture stream in request failed due to IO error (Mip 2-4).”. The model is loaded but only with a single mip-map level.

If I don’t call UnRegisterMountPoint, it solves the loading of mip-maps, but it leads to problems elsewhere because Unreal Engine fails to correctly load assets that were packaged with the main application (other 3D models or a level asset).

What should I do? Any help would be really appreciated.

I found a workaround, which consists in packaging the additional pak files from a project that has the same name as the main application. This way, since the mount point is the same as the main application, there is no need to register and unregister the pak file’s mount point. I simply retrieve the list of files in the pak files and call ScanFilesSynchronous, then the meshes and textures are loaded correctly.

In the Epic tutorial, the projects names are different, but the spawned mesh hasn’t any texture so unregistering the mount point doesn’t cause any problem.

Before finding this workaround, I tried mounting my previous pak files by specifying a mount point directly in this method (using the InPath argument, described as “Path to mount the pak at”) but it didn’t change anything to the problem:
FPakPlatformFile::Mount(const TCHAR* InPakFilename, uint32 PakOrder, const TCHAR* InPath = NULL)

lexicon in accordance with PackageName.cpp :slight_smile:

  • “Long Package Name” : virtual path that the engine uses to reference packages and assets
    eg: /Game/MyAsset

  • “Filename” : real path of the asset on disk before being PAKed, but is also the path expected by unreal’s FileSystem(s) when loading a package
    eg: ../../../MyProject/Content/MyAsset.uasset

  • “Root Path” : a root of the virtual file system, there are multiple
    eg: /Game and /Engine

  • “Mount Point” : mapping a “Root Path” to a “Filename” (or rather, a directory name)
    eg: /Game../../../MyProject/Content

References to assets/objects are always in LongPackageName/LongObjectName form. Whenever the engine needs to load an asset, it finds the MountPoint that best matches the input LongPackageName, converts to real file path, and loads the file via whichever filesystem is available.
Step by step :

  1. you request load object /Game/MyBlueprint.MyBlueprint_C
  2. engine first needs to find and load the package, ie. /Game/MyBlueprint
  3. it finds the mount point with the closest root path, which is /Game
  4. that mount point points to ../../../MyProject/Content
  5. path is converted to ../../../MyProject/Content/MyBlueprint
  6. asset is loaded via filesystem

Important note here : the PAK filesystem is an abstraction of the filesystem only. It has no business in the loading part, or in the path-conversion part. And neither of those two parts know anything about PAK files. They are completely dissociate.

Understanding this, what happens in your use case is pretty straightforward :

  1. There is a default mount point /GameYourProject/Content, that allows your project to access its own content

  2. You add mount point /GameMountedPakProject/Content. This mount point takes priority over existing one.

  3. From this point on, any content from your main project becomes unreachable, because all references to /Game/Something will be converted to MountedPakProject/Content/Something

  4. Remove the mount point

  5. Content from main project is now reachable again, but content from mounted pak is no longer reachable


Unfortunately, there’s no simple solution unless you have a degree of control over the PAKing of assets.

Let’s say for example your inspected pak has a StaticMesh (SM_MyStatic) and a Texture (T_MyTex), and the StaticMesh references the Texture as its texture. The reference will look like /Game/T_MyTex. When you load the static mesh, it will automatically load its dependencies and look for /Game/T_MyTex.

  • If /Game points to your project content, it will not find T_MyTex there.
  • If you override /Game to point to the inspected pak contents, you already know what happens.
  • If you use a different mount point, eg /ThePak to point to the pak’s contents, your project contents will still work, and you may be able to load PAK assets individually, but the references/dependencies will not work - static mesh will be untextured because it still looks for /Game/T_MyTex and not for /ThePak/T_MyTex

Unfortunately, there is no way (AFAIK) that will let you remap those references at runtime, that would probably require editing uasset files in memory when mounting the pak file which would be ultra wonky.

The better way to handle this is something I described in the Primer you read - it involves placing content in a plugin rather than in project, which results in assets (as well as their references/dependencies!) having their own root path such as /Plugin1, then you can pak it that way and when mounting the pak you just need to register mount point /Plugin1 to pak content. Of course this requires having control over assets before they are cooked/paked, which may not be your case…

In your case, a couple other options may be worth considering. Since you are loading PAKs from other projects into yours, I assume those PAKs are not encrypted, or that you have already decrypted them. That means you have capability to unpak and repak assets. This will not help you remap references between assets, however by doing that you can change the real path of files within the PAK. So instead of having files at ../../../OtherProject/Content/something you can repak them at ../../../YourProject/Content/something. With that, the content from both your main PAK and the inspected PAK should be reachable simultaneously from the standard /Game/ root. Keep in mind however that this is equivalent to unzipping two zip archives into the same folder - if there are conflicts, one will overwrite the other. This would be especially bad if the conflicting assets are not of the same type. To avoid issues, you can wrap all your project assets into a folder with a low risk of conflict, ie. something like /Game/SomeRandomString/... But you might still encounter those issues if you try to inspect multiple PAKs simultaneously.

Alternatively, it would be worth looking into making a subclass of the PakFile subsystem, or just making a custom filesystem in general. It doesn’t have to be super complex, you’d make some kind of wrapper filesystem that just delegates to the PAKFile filesystem, but you’d have the chance to remap the input file path into a different one before delegating. Basically, create a IPlatformFile subclass, implement the IPlatformFile interface (functions like FileExists and OpenRead would be the most important), transform the Filename parameter, then forward to the PlatformFilePak implementation. This is just an idea, I haven’t really dug into it.


In retrospect, maybe I went a bit overboard. If you are just making a static mesh viewer, you probably don’t need to enter into that level of complexity. Some more ideas :

  • Load the static mesh and provide your own default material. Then you don’t have to care about dependencies and texture streaming failing after unmounting.

  • Disable texture streaming. This should at least correct the error you are getting. Maybe there’s streaming for Mesh LOD as well though, not sure. Maybe it can be disabled as well.

1 Like

Thank you for your time and your detailed answer. :slightly_smiling_face:

My initial misunderstanding was to think that registering a mount point would preserve the previous ones, and that loading a mesh/texture file with “/Game/” in its path would make Unreal Engine look in all registered mount points.

I didn’t try the plugin method for now because I feared it would be more complicated (as suggested in this topic where the developer created a subsystem) but I will have a look. I just want to avoid extra developments.

They are indeed not encrypted. For now we generate these pak files ourselves (our users may need to do it too later).
I was naive but I thought it was possible to change the location of files in the pak by simply using the last argument of FPakPlatformFile::Mount(const TCHAR* InPakFilename, uint32 PakOrder, const TCHAR* InPath)

I prefer not doing this because we want to take advantage of all optimizations of Unreal Engine.

You are right, this parameter will let you alter the pak’s mount point, which can save you the hassle of unpaking and repaking with a different folder name. However, there are some more complications to take into account. As described in the Primer, PAK file is structured like this :


It has a defined mount point, and all files within it are relative to that mount point.
If you alter the mount point, files will be relative to the mount point you specify.
However, in my experience the mount point is not necessarily consistent, ie. here is another example of PAK file :

Display: Mount point ../../../MyProject/
Display: "AssetRegistry.bin"
Display: "Content/BP_Base.uasset"
Display: "Content/BP_Base.uexp"
Display: "Content/M_Mat.uasset"
Display: "Content/M_Mat.uexp"

In the first sample, the PAK mount point is pointing to the parent folder of the Engine folder, which would be your game’s root folder. In the second sample, the PAK mount point is pointing to MyProject folder, so it is already one level deeper.

Then there’s the case of plugin paks, which looks like this

Display: Mount point ../../../MyProject/Plugins/Plugin1/
Display: "AssetRegistry.bin"
Display: "Content/BP_Base2.uasset"
Display: "Content/BP_Base2.uexp"
Display: "Content/M_Mat2.uasset"
Display: "Content/M_Mat2.uexp"

Here the references will look like /Plugin1/BP_Base2 and /Plugin1/M_Mat2 so if you override the mount point to place it under /Game it’s not gonna work either. In this case you do need to register a new mount point /Plugin1 that points to wherever you mount the pak.


Thinking about it, lots of game ship with a single monolithic PAK. So the PAK contains both Engine folder and project’s folder. That means the PAK mount point is most likely a parent of these two, ie. the game’s root folder, and that means the project’s name is not part of the mount point but part of each asset’s path. In that case the parameter to override mount point won’t be useful unfortunately.