CacheHelper.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. using ICSharpCode.SharpZipLib.Zip;
  2. using Ryujinx.Common;
  3. using Ryujinx.Common.Configuration;
  4. using Ryujinx.Common.Logging;
  5. using Ryujinx.Graphics.GAL;
  6. using Ryujinx.Graphics.Gpu.Memory;
  7. using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
  8. using Ryujinx.Graphics.Shader;
  9. using Ryujinx.Graphics.Shader.Translation;
  10. using System;
  11. using System.Collections.Generic;
  12. using System.IO;
  13. using System.Runtime.CompilerServices;
  14. using System.Runtime.InteropServices;
  15. namespace Ryujinx.Graphics.Gpu.Shader.Cache
  16. {
  17. /// <summary>
  18. /// Helper to manipulate the disk shader cache.
  19. /// </summary>
  20. static class CacheHelper
  21. {
  22. /// <summary>
  23. /// Try to read the manifest header from a given file path.
  24. /// </summary>
  25. /// <param name="manifestPath">The path to the manifest file</param>
  26. /// <param name="header">The manifest header read</param>
  27. /// <returns>Return true if the manifest header was read</returns>
  28. public static bool TryReadManifestHeader(string manifestPath, out CacheManifestHeader header)
  29. {
  30. header = default;
  31. if (File.Exists(manifestPath))
  32. {
  33. Memory<byte> rawManifest = File.ReadAllBytes(manifestPath);
  34. if (MemoryMarshal.TryRead(rawManifest.Span, out header))
  35. {
  36. return true;
  37. }
  38. }
  39. return false;
  40. }
  41. /// <summary>
  42. /// Try to read the manifest from a given file path.
  43. /// </summary>
  44. /// <param name="manifestPath">The path to the manifest file</param>
  45. /// <param name="graphicsApi">The graphics api used by the cache</param>
  46. /// <param name="hashType">The hash type of the cache</param>
  47. /// <param name="header">The manifest header read</param>
  48. /// <param name="entries">The entries read from the cache manifest</param>
  49. /// <returns>Return true if the manifest was read</returns>
  50. public static bool TryReadManifestFile(string manifestPath, CacheGraphicsApi graphicsApi, CacheHashType hashType, out CacheManifestHeader header, out HashSet<Hash128> entries)
  51. {
  52. header = default;
  53. entries = new HashSet<Hash128>();
  54. if (File.Exists(manifestPath))
  55. {
  56. Memory<byte> rawManifest = File.ReadAllBytes(manifestPath);
  57. if (MemoryMarshal.TryRead(rawManifest.Span, out header))
  58. {
  59. Memory<byte> hashTableRaw = rawManifest.Slice(Unsafe.SizeOf<CacheManifestHeader>());
  60. bool isValid = header.IsValid(graphicsApi, hashType, hashTableRaw.Span);
  61. if (isValid)
  62. {
  63. ReadOnlySpan<Hash128> hashTable = MemoryMarshal.Cast<byte, Hash128>(hashTableRaw.Span);
  64. foreach (Hash128 hash in hashTable)
  65. {
  66. entries.Add(hash);
  67. }
  68. }
  69. return isValid;
  70. }
  71. }
  72. return false;
  73. }
  74. /// <summary>
  75. /// Compute a cache manifest from runtime data.
  76. /// </summary>
  77. /// <param name="version">The version of the cache</param>
  78. /// <param name="graphicsApi">The graphics api used by the cache</param>
  79. /// <param name="hashType">The hash type of the cache</param>
  80. /// <param name="entries">The entries in the cache</param>
  81. /// <returns>The cache manifest from runtime data</returns>
  82. public static byte[] ComputeManifest(ulong version, CacheGraphicsApi graphicsApi, CacheHashType hashType, HashSet<Hash128> entries)
  83. {
  84. if (hashType != CacheHashType.XxHash128)
  85. {
  86. throw new NotImplementedException($"{hashType}");
  87. }
  88. CacheManifestHeader manifestHeader = new CacheManifestHeader(version, graphicsApi, hashType);
  89. byte[] data = new byte[Unsafe.SizeOf<CacheManifestHeader>() + entries.Count * Unsafe.SizeOf<Hash128>()];
  90. // CacheManifestHeader has the same size as a Hash128.
  91. Span<Hash128> dataSpan = MemoryMarshal.Cast<byte, Hash128>(data.AsSpan()).Slice(1);
  92. int i = 0;
  93. foreach (Hash128 hash in entries)
  94. {
  95. dataSpan[i++] = hash;
  96. }
  97. manifestHeader.UpdateChecksum(data.AsSpan(Unsafe.SizeOf<CacheManifestHeader>()));
  98. MemoryMarshal.Write(data, ref manifestHeader);
  99. return data;
  100. }
  101. /// <summary>
  102. /// Get the base directory of the shader cache for a given title id.
  103. /// </summary>
  104. /// <param name="titleId">The title id of the target application</param>
  105. /// <returns>The base directory of the shader cache for a given title id</returns>
  106. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  107. public static string GetBaseCacheDirectory(string titleId) => Path.Combine(AppDataManager.GamesDirPath, titleId, "cache", "shader");
  108. /// <summary>
  109. /// Get the temp path to the cache data directory.
  110. /// </summary>
  111. /// <param name="cacheDirectory">The cache directory</param>
  112. /// <returns>The temp path to the cache data directory</returns>
  113. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  114. public static string GetCacheTempDataPath(string cacheDirectory) => Path.Combine(cacheDirectory, "temp");
  115. /// <summary>
  116. /// The path to the cache archive file.
  117. /// </summary>
  118. /// <param name="cacheDirectory">The cache directory</param>
  119. /// <returns>The path to the cache archive file</returns>
  120. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  121. public static string GetArchivePath(string cacheDirectory) => Path.Combine(cacheDirectory, "cache.zip");
  122. /// <summary>
  123. /// The path to the cache manifest file.
  124. /// </summary>
  125. /// <param name="cacheDirectory">The cache directory</param>
  126. /// <returns>The path to the cache manifest file</returns>
  127. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  128. public static string GetManifestPath(string cacheDirectory) => Path.Combine(cacheDirectory, "cache.info");
  129. /// <summary>
  130. /// Create a new temp path to the given cached file via its hash.
  131. /// </summary>
  132. /// <param name="cacheDirectory">The cache directory</param>
  133. /// <param name="key">The hash of the cached data</param>
  134. /// <returns>New path to the given cached file</returns>
  135. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  136. public static string GenCacheTempFilePath(string cacheDirectory, Hash128 key) => Path.Combine(GetCacheTempDataPath(cacheDirectory), key.ToString());
  137. /// <summary>
  138. /// Generate the path to the cache directory.
  139. /// </summary>
  140. /// <param name="baseCacheDirectory">The base of the cache directory</param>
  141. /// <param name="graphicsApi">The graphics api in use</param>
  142. /// <param name="shaderProvider">The name of the shader provider in use</param>
  143. /// <param name="cacheName">The name of the cache</param>
  144. /// <returns>The path to the cache directory</returns>
  145. public static string GenerateCachePath(string baseCacheDirectory, CacheGraphicsApi graphicsApi, string shaderProvider, string cacheName)
  146. {
  147. string graphicsApiName = graphicsApi switch
  148. {
  149. CacheGraphicsApi.OpenGL => "opengl",
  150. CacheGraphicsApi.OpenGLES => "opengles",
  151. CacheGraphicsApi.Vulkan => "vulkan",
  152. CacheGraphicsApi.DirectX => "directx",
  153. CacheGraphicsApi.Metal => "metal",
  154. CacheGraphicsApi.Guest => "guest",
  155. _ => throw new NotImplementedException(graphicsApi.ToString()),
  156. };
  157. return Path.Combine(baseCacheDirectory, graphicsApiName, shaderProvider, cacheName);
  158. }
  159. /// <summary>
  160. /// Read a cached file with the given hash that is present in the archive.
  161. /// </summary>
  162. /// <param name="archive">The archive in use</param>
  163. /// <param name="entry">The given hash</param>
  164. /// <returns>The cached file if present or null</returns>
  165. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  166. public static byte[] ReadFromArchive(ZipFile archive, Hash128 entry)
  167. {
  168. if (archive != null)
  169. {
  170. ZipEntry archiveEntry = archive.GetEntry($"{entry}");
  171. if (archiveEntry != null)
  172. {
  173. try
  174. {
  175. byte[] result = new byte[archiveEntry.Size];
  176. using (Stream archiveStream = archive.GetInputStream(archiveEntry))
  177. {
  178. archiveStream.Read(result);
  179. return result;
  180. }
  181. }
  182. catch (Exception e)
  183. {
  184. Logger.Error?.Print(LogClass.Gpu, $"Cannot load cache file {entry} from archive");
  185. Logger.Error?.Print(LogClass.Gpu, e.ToString());
  186. }
  187. }
  188. }
  189. return null;
  190. }
  191. /// <summary>
  192. /// Read a cached file with the given hash that is not present in the archive.
  193. /// </summary>
  194. /// <param name="cacheDirectory">The cache directory</param>
  195. /// <param name="entry">The given hash</param>
  196. /// <returns>The cached file if present or null</returns>
  197. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  198. public static byte[] ReadFromFile(string cacheDirectory, Hash128 entry)
  199. {
  200. string cacheTempFilePath = GenCacheTempFilePath(cacheDirectory, entry);
  201. try
  202. {
  203. return File.ReadAllBytes(cacheTempFilePath);
  204. }
  205. catch (Exception e)
  206. {
  207. Logger.Error?.Print(LogClass.Gpu, $"Cannot load cache file at {cacheTempFilePath}");
  208. Logger.Error?.Print(LogClass.Gpu, e.ToString());
  209. }
  210. return null;
  211. }
  212. /// <summary>
  213. /// Compute the guest program code for usage while dumping to disk or hash.
  214. /// </summary>
  215. /// <param name="cachedShaderEntries">The guest shader entries to use</param>
  216. /// <param name="tfd">The transform feedback descriptors</param>
  217. /// <param name="forHashCompute">Used to determine if the guest program code is generated for hashing</param>
  218. /// <returns>The guest program code for usage while dumping to disk or hash</returns>
  219. private static byte[] ComputeGuestProgramCode(ReadOnlySpan<GuestShaderCacheEntry> cachedShaderEntries, TransformFeedbackDescriptor[] tfd, bool forHashCompute = false)
  220. {
  221. using (MemoryStream stream = new MemoryStream())
  222. {
  223. BinaryWriter writer = new BinaryWriter(stream);
  224. foreach (GuestShaderCacheEntry cachedShaderEntry in cachedShaderEntries)
  225. {
  226. if (cachedShaderEntry != null)
  227. {
  228. // Code (and Code A if present)
  229. stream.Write(cachedShaderEntry.Code);
  230. if (forHashCompute)
  231. {
  232. // Guest GPU accessor header (only write this for hashes, already present in the header for dumps)
  233. writer.WriteStruct(cachedShaderEntry.Header.GpuAccessorHeader);
  234. }
  235. // Texture descriptors
  236. foreach (GuestTextureDescriptor textureDescriptor in cachedShaderEntry.TextureDescriptors.Values)
  237. {
  238. writer.WriteStruct(textureDescriptor);
  239. }
  240. }
  241. }
  242. // Transform feedback
  243. if (tfd != null)
  244. {
  245. foreach (TransformFeedbackDescriptor transform in tfd)
  246. {
  247. writer.WriteStruct(new GuestShaderCacheTransformFeedbackHeader(transform.BufferIndex, transform.Stride, transform.VaryingLocations.Length));
  248. writer.Write(transform.VaryingLocations);
  249. }
  250. }
  251. return stream.ToArray();
  252. }
  253. }
  254. /// <summary>
  255. /// Compute a guest hash from shader entries.
  256. /// </summary>
  257. /// <param name="cachedShaderEntries">The guest shader entries to use</param>
  258. /// <param name="tfd">The optional transform feedback descriptors</param>
  259. /// <returns>A guest hash from shader entries</returns>
  260. public static Hash128 ComputeGuestHashFromCache(ReadOnlySpan<GuestShaderCacheEntry> cachedShaderEntries, TransformFeedbackDescriptor[] tfd = null)
  261. {
  262. return XXHash128.ComputeHash(ComputeGuestProgramCode(cachedShaderEntries, tfd, true));
  263. }
  264. /// <summary>
  265. /// Read transform feedback descriptors from guest.
  266. /// </summary>
  267. /// <param name="data">The raw guest transform feedback descriptors</param>
  268. /// <param name="header">The guest shader program header</param>
  269. /// <returns>The transform feedback descriptors read from guest</returns>
  270. public static TransformFeedbackDescriptor[] ReadTransformFeedbackInformation(ref ReadOnlySpan<byte> data, GuestShaderCacheHeader header)
  271. {
  272. if (header.TransformFeedbackCount != 0)
  273. {
  274. TransformFeedbackDescriptor[] result = new TransformFeedbackDescriptor[header.TransformFeedbackCount];
  275. for (int i = 0; i < result.Length; i++)
  276. {
  277. GuestShaderCacheTransformFeedbackHeader feedbackHeader = MemoryMarshal.Read<GuestShaderCacheTransformFeedbackHeader>(data);
  278. result[i] = new TransformFeedbackDescriptor(feedbackHeader.BufferIndex, feedbackHeader.Stride, data.Slice(Unsafe.SizeOf<GuestShaderCacheTransformFeedbackHeader>(), feedbackHeader.VaryingLocationsLength).ToArray());
  279. data = data.Slice(Unsafe.SizeOf<GuestShaderCacheTransformFeedbackHeader>() + feedbackHeader.VaryingLocationsLength);
  280. }
  281. return result;
  282. }
  283. return null;
  284. }
  285. /// <summary>
  286. /// Builds gpu state flags using information from the given gpu accessor.
  287. /// </summary>
  288. /// <param name="gpuAccessor">The gpu accessor</param>
  289. /// <returns>The gpu state flags</returns>
  290. private static GuestGpuStateFlags GetGpuStateFlags(IGpuAccessor gpuAccessor)
  291. {
  292. GuestGpuStateFlags flags = 0;
  293. if (gpuAccessor.QueryEarlyZForce())
  294. {
  295. flags |= GuestGpuStateFlags.EarlyZForce;
  296. }
  297. return flags;
  298. }
  299. /// <summary>
  300. /// Packs the tessellation parameters from the gpu accessor.
  301. /// </summary>
  302. /// <param name="gpuAccessor">The gpu accessor</param>
  303. /// <returns>The packed tessellation parameters</returns>
  304. private static byte GetTessellationModePacked(IGpuAccessor gpuAccessor)
  305. {
  306. byte value;
  307. value = (byte)((int)gpuAccessor.QueryTessPatchType() & 3);
  308. value |= (byte)(((int)gpuAccessor.QueryTessSpacing() & 3) << 2);
  309. if (gpuAccessor.QueryTessCw())
  310. {
  311. value |= 0x10;
  312. }
  313. return value;
  314. }
  315. /// <summary>
  316. /// Create a new instance of <see cref="GuestGpuAccessorHeader"/> from an gpu accessor.
  317. /// </summary>
  318. /// <param name="gpuAccessor">The gpu accessor</param>
  319. /// <returns>A new instance of <see cref="GuestGpuAccessorHeader"/></returns>
  320. public static GuestGpuAccessorHeader CreateGuestGpuAccessorCache(IGpuAccessor gpuAccessor)
  321. {
  322. return new GuestGpuAccessorHeader
  323. {
  324. ComputeLocalSizeX = gpuAccessor.QueryComputeLocalSizeX(),
  325. ComputeLocalSizeY = gpuAccessor.QueryComputeLocalSizeY(),
  326. ComputeLocalSizeZ = gpuAccessor.QueryComputeLocalSizeZ(),
  327. ComputeLocalMemorySize = gpuAccessor.QueryComputeLocalMemorySize(),
  328. ComputeSharedMemorySize = gpuAccessor.QueryComputeSharedMemorySize(),
  329. PrimitiveTopology = gpuAccessor.QueryPrimitiveTopology(),
  330. TessellationModePacked = GetTessellationModePacked(gpuAccessor),
  331. StateFlags = GetGpuStateFlags(gpuAccessor)
  332. };
  333. }
  334. /// <summary>
  335. /// Create guest shader cache entries from the runtime contexts.
  336. /// </summary>
  337. /// <param name="channel">The GPU channel in use</param>
  338. /// <param name="shaderContexts">The runtime contexts</param>
  339. /// <returns>Guest shader cahe entries from the runtime contexts</returns>
  340. public static GuestShaderCacheEntry[] CreateShaderCacheEntries(GpuChannel channel, ReadOnlySpan<TranslatorContext> shaderContexts)
  341. {
  342. MemoryManager memoryManager = channel.MemoryManager;
  343. int startIndex = shaderContexts.Length > 1 ? 1 : 0;
  344. GuestShaderCacheEntry[] entries = new GuestShaderCacheEntry[shaderContexts.Length - startIndex];
  345. for (int i = startIndex; i < shaderContexts.Length; i++)
  346. {
  347. TranslatorContext context = shaderContexts[i];
  348. if (context == null)
  349. {
  350. continue;
  351. }
  352. GpuAccessor gpuAccessor = context.GpuAccessor as GpuAccessor;
  353. ulong cb1DataAddress;
  354. int cb1DataSize = gpuAccessor?.Cb1DataSize ?? 0;
  355. if (context.Stage == ShaderStage.Compute)
  356. {
  357. cb1DataAddress = channel.BufferManager.GetComputeUniformBufferAddress(1);
  358. }
  359. else
  360. {
  361. int stageIndex = context.Stage switch
  362. {
  363. ShaderStage.TessellationControl => 1,
  364. ShaderStage.TessellationEvaluation => 2,
  365. ShaderStage.Geometry => 3,
  366. ShaderStage.Fragment => 4,
  367. _ => 0
  368. };
  369. cb1DataAddress = channel.BufferManager.GetGraphicsUniformBufferAddress(stageIndex, 1);
  370. }
  371. int size = context.Size;
  372. TranslatorContext translatorContext2 = i == 1 ? shaderContexts[0] : null;
  373. int sizeA = translatorContext2 != null ? translatorContext2.Size : 0;
  374. byte[] code = new byte[size + cb1DataSize + sizeA];
  375. memoryManager.GetSpan(context.Address, size).CopyTo(code);
  376. if (cb1DataAddress != 0 && cb1DataSize != 0)
  377. {
  378. memoryManager.Physical.GetSpan(cb1DataAddress, cb1DataSize).CopyTo(code.AsSpan(size, cb1DataSize));
  379. }
  380. if (translatorContext2 != null)
  381. {
  382. memoryManager.GetSpan(translatorContext2.Address, sizeA).CopyTo(code.AsSpan(size + cb1DataSize, sizeA));
  383. }
  384. GuestGpuAccessorHeader gpuAccessorHeader = CreateGuestGpuAccessorCache(context.GpuAccessor);
  385. if (gpuAccessor != null)
  386. {
  387. gpuAccessorHeader.TextureDescriptorCount = context.TextureHandlesForCache.Count;
  388. }
  389. GuestShaderCacheEntryHeader header = new GuestShaderCacheEntryHeader(
  390. context.Stage,
  391. size + cb1DataSize,
  392. sizeA,
  393. cb1DataSize,
  394. gpuAccessorHeader);
  395. GuestShaderCacheEntry entry = new GuestShaderCacheEntry(header, code);
  396. if (gpuAccessor != null)
  397. {
  398. foreach (int textureHandle in context.TextureHandlesForCache)
  399. {
  400. GuestTextureDescriptor textureDescriptor = ((Image.TextureDescriptor)gpuAccessor.GetTextureDescriptor(textureHandle, -1)).ToCache();
  401. textureDescriptor.Handle = (uint)textureHandle;
  402. entry.TextureDescriptors.Add(textureHandle, textureDescriptor);
  403. }
  404. }
  405. entries[i - startIndex] = entry;
  406. }
  407. return entries;
  408. }
  409. /// <summary>
  410. /// Create a guest shader program.
  411. /// </summary>
  412. /// <param name="shaderCacheEntries">The entries composing the guest program dump</param>
  413. /// <param name="tfd">The transform feedback descriptors in use</param>
  414. /// <returns>The resulting guest shader program</returns>
  415. public static byte[] CreateGuestProgramDump(GuestShaderCacheEntry[] shaderCacheEntries, TransformFeedbackDescriptor[] tfd = null)
  416. {
  417. using (MemoryStream resultStream = new MemoryStream())
  418. {
  419. BinaryWriter resultStreamWriter = new BinaryWriter(resultStream);
  420. byte transformFeedbackCount = 0;
  421. if (tfd != null)
  422. {
  423. transformFeedbackCount = (byte)tfd.Length;
  424. }
  425. // Header
  426. resultStreamWriter.WriteStruct(new GuestShaderCacheHeader((byte)shaderCacheEntries.Length, transformFeedbackCount));
  427. // Write all entries header
  428. foreach (GuestShaderCacheEntry entry in shaderCacheEntries)
  429. {
  430. if (entry == null)
  431. {
  432. resultStreamWriter.WriteStruct(new GuestShaderCacheEntryHeader());
  433. }
  434. else
  435. {
  436. resultStreamWriter.WriteStruct(entry.Header);
  437. }
  438. }
  439. // Finally, write all program code and all transform feedback information.
  440. resultStreamWriter.Write(ComputeGuestProgramCode(shaderCacheEntries, tfd));
  441. return resultStream.ToArray();
  442. }
  443. }
  444. /// <summary>
  445. /// Save temporary files not in archive.
  446. /// </summary>
  447. /// <param name="baseCacheDirectory">The base of the cache directory</param>
  448. /// <param name="archive">The archive to use</param>
  449. /// <param name="entries">The entries in the cache</param>
  450. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  451. public static void EnsureArchiveUpToDate(string baseCacheDirectory, ZipFile archive, HashSet<Hash128> entries)
  452. {
  453. List<string> filesToDelete = new List<string>();
  454. archive.BeginUpdate();
  455. foreach (Hash128 hash in entries)
  456. {
  457. string cacheTempFilePath = GenCacheTempFilePath(baseCacheDirectory, hash);
  458. if (File.Exists(cacheTempFilePath))
  459. {
  460. string cacheHash = $"{hash}";
  461. ZipEntry entry = archive.GetEntry(cacheHash);
  462. if (entry != null)
  463. {
  464. archive.Delete(entry);
  465. }
  466. // We enforce deflate compression here to avoid possible incompatibilities on older version of Ryujinx that use System.IO.Compression.
  467. archive.Add(new StaticDiskDataSource(cacheTempFilePath), cacheHash, CompressionMethod.Deflated);
  468. filesToDelete.Add(cacheTempFilePath);
  469. }
  470. }
  471. archive.CommitUpdate();
  472. foreach (string filePath in filesToDelete)
  473. {
  474. File.Delete(filePath);
  475. }
  476. }
  477. public static bool IsArchiveReadOnly(string archivePath)
  478. {
  479. FileInfo info = new FileInfo(archivePath);
  480. if (!info.Exists)
  481. {
  482. return false;
  483. }
  484. try
  485. {
  486. using (FileStream stream = info.Open(FileMode.Open, FileAccess.Read, FileShare.None))
  487. {
  488. return false;
  489. }
  490. }
  491. catch (IOException)
  492. {
  493. return true;
  494. }
  495. }
  496. }
  497. }