Paradox Game Engine  v1.0.0 beta06
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros Pages
BundlePacker.cs
Go to the documentation of this file.
1 // Copyright (c) 2014 Silicon Studio Corp. (http://siliconstudio.co.jp)
2 // This file is distributed under GPL v3. See LICENSE.md for details.
3 using System;
4 using System.Collections.Generic;
5 using System.IO;
6 using System.Linq;
7 using SiliconStudio.Core;
8 using SiliconStudio.Core.Diagnostics;
9 using SiliconStudio.Core.IO;
10 using SiliconStudio.Core.Serialization;
11 using SiliconStudio.Core.Serialization.Assets;
12 using SiliconStudio.Core.Serialization.Contents;
13 using SiliconStudio.Core.Storage;
14 
15 namespace SiliconStudio.Assets.CompilerApp
16 {
17  /// <summary>
18  /// Class that will help generate package bundles.
19  /// </summary>
21  {
22  /// <summary>
23  /// Builds bundles. It will automatically analyze assets and chunks to determine dependencies and what should be embedded in which bundle.
24  /// Bundle descriptions will be loaded from <see cref="Package.Bundles" /> provided by the <see cref="packageSession" />, and copied to <see cref="outputDirectory" />.
25  /// </summary>
26  /// <param name="logger">The builder logger.</param>
27  /// <param name="packageSession">The project session.</param>
28  /// <param name="profile">The build profile.</param>
29  /// <param name="indexName">Name of the index file.</param>
30  /// <param name="outputDirectory">The output directory.</param>
31  /// <param name="disableCompressionIds">The object id that should be kept uncompressed in the bundle (everything else will be compressed using LZ4).</param>
32  /// <exception cref="System.InvalidOperationException">
33  /// </exception>
34  public void Build(Logger logger, PackageSession packageSession, PackageProfile profile, string indexName, string outputDirectory, ISet<ObjectId> disableCompressionIds)
35  {
36  if (logger == null) throw new ArgumentNullException("logger");
37  if (packageSession == null) throw new ArgumentNullException("packageSession");
38  if (indexName == null) throw new ArgumentNullException("indexName");
39  if (outputDirectory == null) throw new ArgumentNullException("outputDirectory");
40  if (disableCompressionIds == null) throw new ArgumentNullException("disableCompressionIds");
41 
42  // Load index maps and mount databases
43  var objDatabase = new ObjectDatabase("/data/db", indexName, loadDefaultBundle: false);
44 
45  logger.Info("Generate bundles: Scan assets and their dependencies...");
46 
47  // Prepare list of bundles gathered from all projects
48  var bundles = new List<Bundle>();
49 
50  foreach (var project in packageSession.Packages)
51  {
52  bundles.AddRange(project.Bundles);
53  }
54 
55  var databaseFileProvider = new DatabaseFileProvider(objDatabase.AssetIndexMap, objDatabase);
56  AssetManager.GetFileProvider = () => databaseFileProvider;
57 
58  // Pass1: Create ResolvedBundle from user Bundle
59  var resolvedBundles = new Dictionary<string, ResolvedBundle>();
60  foreach (var bundle in bundles)
61  {
62  if (resolvedBundles.ContainsKey(bundle.Name))
63  {
64  throw new InvalidOperationException(string.Format("Two bundles with name {0} found", bundle.Name));
65  }
66 
67  resolvedBundles.Add(bundle.Name, new ResolvedBundle(bundle));
68  }
69 
70  // Pass2: Enumerate all assets which directly or indirectly belong to an bundle
71  var bundleAssets = new HashSet<string>();
72 
73  foreach (var bundle in resolvedBundles)
74  {
75  // For each project, we apply asset selectors of current bundle
76  // This will give us a list of "root assets".
77  foreach (var assetSelector in bundle.Value.Source.Selectors)
78  {
79  foreach (var assetLocation in assetSelector.Select(packageSession, objDatabase.AssetIndexMap))
80  {
81  bundle.Value.AssetUrls.Add(assetLocation);
82  }
83  }
84 
85  // Compute asset dependencies, and fill bundleAssets with list of all assets contained in bundles (directly or indirectly).
86  foreach (var assetUrl in bundle.Value.AssetUrls)
87  {
88  CollectReferences(bundle.Value.Source, bundleAssets, assetUrl, objDatabase.AssetIndexMap);
89  }
90  }
91 
92  // Pass3: Create a default bundle that contains all assets not contained in any bundle (directly or indirectly)
93  var defaultBundle = new Bundle { Name = "default" };
94  var resolvedDefaultBundle = new ResolvedBundle(defaultBundle);
95  bundles.Add(defaultBundle);
96  resolvedBundles.Add(defaultBundle.Name, resolvedDefaultBundle);
97  foreach (var asset in objDatabase.AssetIndexMap.GetMergedIdMap())
98  {
99  if (!bundleAssets.Contains(asset.Key))
100  {
101  resolvedDefaultBundle.AssetUrls.Add(asset.Key);
102  }
103  }
104 
105  // Pass4: Resolve dependencies
106  foreach (var bundle in resolvedBundles)
107  {
108  // Every bundle depends implicitely on default bundle
109  if (bundle.Key != "default")
110  {
111  bundle.Value.Dependencies.Add(resolvedBundles["default"]);
112  }
113 
114  // Add other explicit dependencies
115  foreach (var dependencyName in bundle.Value.Source.Dependencies)
116  {
117  ResolvedBundle dependency;
118  if (!resolvedBundles.TryGetValue(dependencyName, out dependency))
119  throw new InvalidOperationException(string.Format("Could not find dependency {0} when processing bundle {1}", dependencyName, bundle.Value.Name));
120 
121  bundle.Value.Dependencies.Add(dependency);
122  }
123  }
124 
125  logger.Info("Generate bundles: Assign assets to bundles...");
126 
127  // Pass5: Topological sort (a.k.a. build order)
128  // If there is a cyclic dependency, an exception will be thrown.
129  var sortedBundles = TopologicalSort(resolvedBundles.Values, assetBundle => assetBundle.Dependencies);
130 
131  // Pass6: Find which ObjectId belongs to which bundle
132  foreach (var bundle in sortedBundles)
133  {
134  // Add objects created by dependencies
135  foreach (var dep in bundle.Dependencies)
136  {
137  // ObjectIds
138  bundle.DependencyObjectIds.UnionWith(dep.DependencyObjectIds);
139  bundle.DependencyObjectIds.UnionWith(dep.ObjectIds);
140 
141  // IndexMap
142  foreach (var asset in dep.DependencyIndexMap.Concat(dep.IndexMap))
143  {
144  if (!bundle.DependencyIndexMap.ContainsKey(asset.Key))
145  bundle.DependencyIndexMap.Add(asset.Key, asset.Value);
146  }
147  }
148 
149  // Collect assets (object ids and partial index map) from given asset urls
150  // Those not present in dependencies will be added to this bundle
151  foreach (var assetUrl in bundle.AssetUrls)
152  {
153  CollectBundle(bundle, assetUrl, objDatabase.AssetIndexMap);
154  }
155  }
156 
157  logger.Info("Generate bundles: Compress and save bundles to HDD...");
158 
159  // Mount VFS for output database (currently disabled because already done in ProjectBuilder.CopyBuildToOutput)
160  VirtualFileSystem.MountFileSystem("/data_output", outputDirectory);
161  VirtualFileSystem.CreateDirectory("/data_output/db");
162 
163  // Mount output database and delete previous bundles that shouldn't exist anymore (others should be overwritten)
164  var outputDatabase = new ObjectDatabase("/data_output/db", loadDefaultBundle: false);
165  try
166  {
167  outputDatabase.LoadBundle("default").GetAwaiter().GetResult();
168  }
169  catch (Exception)
170  {
171  logger.Info("Generate bundles: Tried to load previous 'default' bundle but it was invalid. Deleting it...");
172  outputDatabase.BundleBackend.DeleteBundles(x => Path.GetFileNameWithoutExtension(x) == "default");
173  }
174  var outputBundleBackend = outputDatabase.BundleBackend;
175 
176  var outputGroupBundleBackends = new Dictionary<string, BundleOdbBackend>();
177 
178  if (profile != null && profile.OutputGroupDirectories != null)
179  {
180  var rootPackage = packageSession.LocalPackages.First();
181 
182  foreach (var item in profile.OutputGroupDirectories)
183  {
184  var path = Path.Combine(rootPackage.RootDirectory, item.Value);
185  var vfsPath = "/data_group_" + item.Key;
186  var vfsDatabasePath = vfsPath + "/db";
187 
188  // Mount VFS for output database (currently disabled because already done in ProjectBuilder.CopyBuildToOutput)
189  VirtualFileSystem.MountFileSystem(vfsPath, path);
190  VirtualFileSystem.CreateDirectory(vfsDatabasePath);
191 
192  outputGroupBundleBackends.Add(item.Key, new BundleOdbBackend(vfsDatabasePath));
193  }
194  }
195 
196  // Pass7: Assign bundle backends
197  foreach (var bundle in sortedBundles)
198  {
199  BundleOdbBackend bundleBackend;
200  if (bundle.Source.OutputGroup == null)
201  {
202  // No output group, use OutputDirectory
203  bundleBackend = outputBundleBackend;
204  }
205  else if (!outputGroupBundleBackends.TryGetValue(bundle.Source.OutputGroup, out bundleBackend))
206  {
207  // Output group not found in OutputGroupDirectories, let's issue a warning and fallback to OutputDirectory
208  logger.Warning("Generate bundles: Could not find OutputGroup {0} for bundle {1} in ProjectBuildProfile.OutputGroupDirectories", bundle.Source.OutputGroup, bundle.Name);
209  bundleBackend = outputBundleBackend;
210  }
211 
212  bundle.BundleBackend = bundleBackend;
213  }
214 
215  CleanUnknownBundles(outputBundleBackend, resolvedBundles);
216 
217  foreach (var bundleBackend in outputGroupBundleBackends)
218  {
219  CleanUnknownBundles(bundleBackend.Value, resolvedBundles);
220  }
221 
222  // Pass8: Pack actual data
223  foreach (var bundle in sortedBundles)
224  {
225  // Compute dependencies (by bundle names)
226  var dependencies = bundle.Dependencies.Select(x => x.Name).Distinct().ToList();
227 
228  BundleOdbBackend bundleBackend;
229  if (bundle.Source.OutputGroup == null)
230  {
231  // No output group, use OutputDirectory
232  bundleBackend = outputBundleBackend;
233  }
234  else if (!outputGroupBundleBackends.TryGetValue(bundle.Source.OutputGroup, out bundleBackend))
235  {
236  // Output group not found in OutputGroupDirectories, let's issue a warning and fallback to OutputDirectory
237  logger.Warning("Generate bundles: Could not find OutputGroup {0} for bundle {1} in ProjectBuildProfile.OutputGroupDirectories", bundle.Source.OutputGroup, bundle.Name);
238  bundleBackend = outputBundleBackend;
239  }
240 
241  objDatabase.CreateBundle(bundle.ObjectIds.ToArray(), bundle.Name, bundleBackend, disableCompressionIds, bundle.IndexMap, dependencies);
242  }
243 
244  logger.Info("Generate bundles: Done");
245  }
246 
247  private static void CleanUnknownBundles(BundleOdbBackend outputBundleBackend, Dictionary<string, ResolvedBundle> resolvedBundles)
248  {
249  // Delete previous bundles
250  outputBundleBackend.DeleteBundles(bundleFile =>
251  {
252  // Ensure we have proper extension, otherwise delete
253  if (Path.GetExtension(bundleFile) != BundleOdbBackend.BundleExtension)
254  return true;
255 
256  // Only keep bundles that are supposed to be output with same BundleBackend
257  ResolvedBundle bundle;
258  var bundleName = Path.GetFileNameWithoutExtension(bundleFile);
259  return !resolvedBundles.TryGetValue(Path.GetFileNameWithoutExtension(bundleFile), out bundle)
260  || bundle.BundleBackend != outputBundleBackend;
261  });
262  }
263 
264  private Dictionary<ObjectId, List<string>> referencesByObjectId = new Dictionary<ObjectId, List<string>>();
265 
266  /// <summary>
267  /// Gets and cache the asset url referenced by the chunk with the given identifier.
268  /// </summary>
269  /// <param name="objectId">The object identifier.</param>
270  /// <returns>The list of asset url referenced.</returns>
271  private List<string> GetChunkReferences(ref ObjectId objectId)
272  {
273  List<string> references;
274 
275  // Check the cache
276  if (!referencesByObjectId.TryGetValue(objectId, out references))
277  {
278  // First time, need to scan it
279  referencesByObjectId[objectId] = references = new List<string>();
280 
281  // Open stream to read list of chunk references
282  using (var stream = AssetManager.FileProvider.OpenStream("obj/" + objectId, VirtualFileMode.Open, VirtualFileAccess.Read))
283  {
284  // Read chunk header
285  var streamReader = new BinarySerializationReader(stream);
286  var header = ChunkHeader.Read(streamReader);
287 
288  // Only process chunks
289  if (header != null)
290  {
291  if (header.OffsetToReferences != -1)
292  {
293  // Seek to where references are stored and deserialize them
294  streamReader.NativeStream.Seek(header.OffsetToReferences, SeekOrigin.Begin);
295 
296  List<ChunkReference> chunkReferences = null;
297  streamReader.Serialize(ref chunkReferences, ArchiveMode.Deserialize);
298 
299  foreach (var chunkReference in chunkReferences)
300  {
301  references.Add(chunkReference.Location);
302  }
303  }
304  }
305  }
306  }
307 
308  return references;
309  }
310 
311  private void CollectReferences(Bundle bundle, HashSet<string> assets, string assetUrl, IAssetIndexMap assetIndexMap)
312  {
313  // Already included?
314  if (!assets.Add(assetUrl))
315  return;
316 
317  ObjectId objectId;
318  if (!assetIndexMap.TryGetValue(assetUrl, out objectId))
319  throw new InvalidOperationException(string.Format("Could not find asset {0} for bundle {1}", assetUrl, bundle.Name));
320 
321  // Include references
322  foreach (var reference in GetChunkReferences(ref objectId))
323  {
324  CollectReferences(bundle, assets, reference, assetIndexMap);
325  }
326  }
327 
328  private void CollectBundle(ResolvedBundle resolvedBundle, string assetUrl, IAssetIndexMap assetIndexMap)
329  {
330  // Check if index map contains it already (that also means object id has been stored as well)
331  if (resolvedBundle.DependencyIndexMap.ContainsKey(assetUrl) || resolvedBundle.IndexMap.ContainsKey(assetUrl))
332  return;
333 
334  ObjectId objectId;
335  if (!assetIndexMap.TryGetValue(assetUrl, out objectId))
336  throw new InvalidOperationException(string.Format("Could not find asset {0} for bundle {1}", assetUrl, resolvedBundle.Name));
337 
338  // Add asset to index map
339  resolvedBundle.IndexMap.Add(assetUrl, objectId);
340 
341  // Check if object id has already been added (either as dependency or inside this actual pack)
342  // As a consequence, no need to check references since they will somehow be part of this package or one of its dependencies.
343  if (resolvedBundle.DependencyObjectIds.Contains(objectId) || !resolvedBundle.ObjectIds.Add(objectId))
344  return;
345 
346  foreach (var reference in GetChunkReferences(ref objectId))
347  {
348  CollectBundle(resolvedBundle, reference, assetIndexMap);
349  }
350  }
351 
352  /// <summary>
353  /// Performs a topological sort.
354  /// </summary>
355  /// <typeparam name="T">The type of item.</typeparam>
356  /// <param name="source">The source items.</param>
357  /// <param name="dependencies">The function that will give dependencies for a given item.</param>
358  /// <returns></returns>
359  private static List<T> TopologicalSort<T>(IEnumerable<T> source, Func<T, IEnumerable<T>> dependencies)
360  {
361  var result = new List<T>();
362  var temporaryMark = new HashSet<T>();
363  var mark = new HashSet<T>();
364 
365  foreach (var item in source)
366  TopologicalSortVisit(item, temporaryMark, mark, result, dependencies);
367 
368  return result;
369  }
370 
371  private static void TopologicalSortVisit<T>(T item, HashSet<T> temporaryMark, HashSet<T> mark, List<T> result, Func<T, IEnumerable<T>> dependencies)
372  {
373  // Already processed?
374  if (mark.Contains(item))
375  return;
376 
377  if (temporaryMark.Contains(item))
378  throw new InvalidOperationException(string.Format("Cyclic dependency found, involving {0}", item));
379 
380  temporaryMark.Add(item);
381 
382  foreach (var dep in dependencies(item))
383  TopologicalSortVisit(dep, temporaryMark, mark, result, dependencies);
384 
385  mark.Add(item);
386  result.Add(item);
387  }
388  }
389 }
void Build(Logger logger, PackageSession packageSession, PackageProfile profile, string indexName, string outputDirectory, ISet< ObjectId > disableCompressionIds)
Builds bundles. It will automatically analyze assets and chunks to determine dependencies and what sh...
Definition: BundlePacker.cs:34
Description of an asset bundle.
Definition: Bundle.cs:15
Helper class that represents additional data added to a Bundle when resolving asset.
A session for editing a package.
Gives access to the object database.
Base implementation for ILogger.
Definition: Logger.cs:10
Implements SerializationStream as a binary reader.
Object Database Backend (ODB) implementation that bundles multiple chunks into a .bundle files, optionally compressed with LZ4.
This is a minimal implementation of the missing HashSet from Silverlight BCL It's nowhere near the re...
Definition: HashSet.cs:8
A hash to uniquely identify data.
Definition: ObjectId.cs:13
Describes buld parameters used when building assets.
bool TryGetValue(string url, out ObjectId objectId)
Class that will help generate package bundles.
Definition: BundlePacker.cs:20