diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 817621d..c68590e 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -6883,6 +6883,19 @@ public static partial class KernelMemoryCompatExports return TryWriteHostPathStat(ctx, statAddress, hostPath, isDirectory); } + // /app0 is the read-only game image, so a path that is not in it now will not + // appear later. Remembering the absent ones turns a repeated pair of host stats + // on the (slow, external) game volume into a dictionary probe; Demon's Souls + // resolves tens of thousands of paths per boot and misses are the common case. + private static readonly ConcurrentDictionary _aprMissingImagePaths = new(HostFsPath.Comparer); + + private static bool IsUnderApp0(string cachePath) + { + var app0Root = ResolveApp0Root(); + return !string.IsNullOrWhiteSpace(app0Root) && + cachePath.StartsWith(Path.TrimEndingDirectorySeparator(app0Root) + Path.DirectorySeparatorChar, HostFsPath.Comparison); + } + private static bool TryGetAprFileSize(string hostPath, out ulong size) { size = 0; @@ -6902,6 +6915,11 @@ public static partial class KernelMemoryCompatExports return true; } + if (_aprMissingImagePaths.ContainsKey(cachePath)) + { + return false; + } + // One directory listing returns every file's size; titles resolve whole asset // directories at startup, so this replaces one host query per file. if (Path.GetDirectoryName(cachePath) is { } directory && _aprScannedDirectories.TryAdd(directory, 0)) @@ -6936,6 +6954,11 @@ public static partial class KernelMemoryCompatExports if (!new DirectoryInfo(cachePath).Exists) { + if (IsUnderApp0(cachePath)) + { + _aprMissingImagePaths.TryAdd(cachePath, 0); + } + return false; }