1 /*
2 Copyright (c) 2017-2022 Timur Gafarov
3 
4 Boost Software License - Version 1.0 - August 17th, 2003
5 Permission is hereby granted, free of charge, to any person or organization
6 obtaining a copy of the software and accompanying documentation covered by
7 this license (the "Software") to use, reproduce, display, distribute,
8 execute, and transmit the Software, and to prepare derivative works of the
9 Software, and to permit third-parties to whom the Software is furnished to
10 do so, all subject to the following:
11 
12 The copyright notices in the Software and this entire statement, including
13 the above license grant, this restriction and the following disclaimer,
14 must be included in all copies of the Software, in whole or in part, and
15 all derivative works of the Software, unless such copies or derivative
16 works are solely in the form of machine-executable object code generated by
17 a source language processor.
18 
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
22 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
23 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
24 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 DEALINGS IN THE SOFTWARE.
26 */
27 
28 module dagon.resource.asset;
29 
30 import std.stdio;
31 import std.algorithm;
32 import std.path;
33 
34 import dlib.core.memory;
35 import dlib.core.ownership;
36 import dlib.core.compound;
37 import dlib.core.stream;
38 import dlib.core.thread;
39 import dlib.container.dict;
40 import dlib.filesystem.filesystem;
41 import dlib.filesystem.stdfs;
42 import dlib.image.image;
43 import dlib.image.unmanaged;
44 import dlib.image.hdri;
45 import dlib.image.io;
46 
47 import dagon.core.event;
48 import dagon.core.vfs;
49 import dagon.resource.dds;
50 import dagon.resource.boxfs;
51 
52 struct MonitorInfo
53 {
54     FileStat lastStat;
55     bool fileExists = false;
56 }
57 
58 abstract class Asset: Owner
59 {
60     this(Owner o)
61     {
62         super(o);
63     }
64 
65     MonitorInfo monitorInfo;
66     bool threadSafePartLoaded = false;
67     bool threadUnsafePartLoaded = false;
68     bool loadThreadSafePart(string filename, InputStream istrm, ReadOnlyFileSystem fs, AssetManager mngr);
69     bool loadThreadUnsafePart();
70     void release();
71 }
72 
73 Compound!(SuperImage, string) dlibImageLoader(alias loadFunc)(InputStream istrm, SuperImageFactory imgFac)
74 {
75     ubyte[] data = New!(ubyte[])(istrm.size);
76     istrm.fillArray(data);
77     ArrayStream arrStrm = New!ArrayStream(data);
78     auto res = loadFunc(arrStrm, imgFac);
79     Delete(arrStrm);
80     Delete(data);
81     return res;
82 }
83 
84 alias ImageLoaderCallback = Compound!(SuperImage, string) function(InputStream, SuperImageFactory);
85 
86 struct ImageFormatInfo
87 {
88     string format;
89     string dummyFilename;
90     string prefix;
91 }
92 
93 class AssetManager: Owner
94 {
95     Dict!(ImageLoaderCallback, string) imageLoaderCallbacks;
96     Dict!(Asset, string) assetsByFilename;
97     VirtualFileSystem fs;
98     StdFileSystem stdfs;
99     UnmanagedImageFactory imageFactory;
100     UnmanagedHDRImageFactory hdrImageFactory;
101     Thread loadingThread;
102 
103     bool liveUpdate = false;
104     double liveUpdatePeriod = 5.0;
105 
106     protected double monitorTimer = 0.0;
107 
108     float nextLoadingPercentage = 0.0f;
109 
110     EventManager eventManager;
111     
112     Dict!(string, string) base64ImagePrefixes;
113 
114     this(EventManager emngr, Owner o = null)
115     {
116         super(o);
117 
118         imageLoaderCallbacks = New!(Dict!(ImageLoaderCallback, string));
119         registerImageLoader([".bmp", ".BMP"], &dlibImageLoader!loadBMP);
120         registerImageLoader([".jpg", ".JPG", ".jpeg", ".JPEG"], &dlibImageLoader!loadJPEG);
121         registerImageLoader([".png", ".PNG"], &dlibImageLoader!loadPNG);
122         registerImageLoader([".tga", ".TGA"], &dlibImageLoader!loadTGA);
123         
124         assetsByFilename = New!(Dict!(Asset, string));
125         fs = New!VirtualFileSystem();
126         stdfs = New!StdFileSystem();
127         fs.mount(stdfs);
128         fs.mount(".");
129         imageFactory = New!UnmanagedImageFactory();
130         hdrImageFactory = New!UnmanagedHDRImageFactory();
131 
132         loadingThread = New!Thread(&threadFunc);
133 
134         eventManager = emngr;
135         
136         base64ImagePrefixes = New!(Dict!(string, string))();
137         
138         // Reference: https://www.digipres.org/formats/mime-types/
139         base64ImagePrefixes["data:image/png;base64,"] = "image.png";
140         base64ImagePrefixes["data:image/apng;base64,"] = "image.png";
141         base64ImagePrefixes["data:image/jpeg;base64,"] = "image.jpg";
142         base64ImagePrefixes["data:image/gif;base64,"] = "image.gif";
143         base64ImagePrefixes["data:image/webp;base64,"] = "image.webp";
144         base64ImagePrefixes["data:image/ktx;base64,"] = "image.ktx";
145         base64ImagePrefixes["data:image/svg+xml;base64,"] = "image.svg";
146         base64ImagePrefixes["data:image/vnd-ms.dds;base64,"] = "image.dds";
147         base64ImagePrefixes["data:image/image/vnd.radiance;base64,"] = "image.hdr";
148         base64ImagePrefixes["data:image/x-targa;base64,"] = "image.tga";
149         base64ImagePrefixes["data:image/x-tga;base64,"] = "image.tga";
150         base64ImagePrefixes["data:image/x-ms-bmp;base64,"] = "image.bmp";
151         base64ImagePrefixes["data:image/x-psd;base64,"] = "image.psd";
152     }
153     
154     ImageFormatInfo detectBase64Image(string uri)
155     {
156         ImageFormatInfo result;
157         result.format = "";
158         result.dummyFilename = "";
159         result.prefix = "";
160         
161         foreach(string prefix, string dummyFilename; base64ImagePrefixes)
162         {
163             if (uri.startsWith(prefix))
164             {
165                 result.format = extension(dummyFilename);
166                 result.dummyFilename = dummyFilename;
167                 result.prefix = prefix;
168                 break;
169             }
170         }
171         
172         return result;
173     }
174 
175     ~this()
176     {
177         Delete(imageLoaderCallbacks);
178         Delete(assetsByFilename);
179         Delete(fs);
180         Delete(imageFactory);
181         Delete(hdrImageFactory);
182         Delete(loadingThread);
183         Delete(base64ImagePrefixes);
184     }
185 
186     void mountDirectory(string dir)
187     {
188         fs.mount(dir);
189     }
190 
191     void mountBoxFile(string filename)
192     {
193         BoxFileSystem boxfs = New!BoxFileSystem(fs.openForInput(filename), true);
194         fs.mount(boxfs);
195     }
196 
197     void mountBoxFileDirectory(string filename, string dir)
198     {
199         BoxFileSystem boxfs = New!BoxFileSystem(fs.openForInput(filename), true, dir);
200         fs.mount(boxfs);
201     }
202     
203     void registerImageLoader(string extension, ImageLoaderCallback callback)
204     {
205         imageLoaderCallbacks[extension] = callback;
206     }
207     
208     void registerImageLoader(string[] extensions, ImageLoaderCallback callback)
209     {
210         foreach(extension; extensions)
211         {
212             registerImageLoader(extension, callback);
213         }
214     }
215     
216     Compound!(SuperImage, string) loadImage(string extension, InputStream istrm)
217     {
218         if (extension == ".hdr" ||
219             extension == ".HDR")
220         {
221             Compound!(SuperHDRImage, string) res;
222             ubyte[] data = New!(ubyte[])(istrm.size);
223             istrm.fillArray(data);
224             ArrayStream arrStrm = New!ArrayStream(data);
225             res = loadHDR(arrStrm, hdrImageFactory);
226             SuperImage img = res[0];
227             string errMsg = res[1];
228             Delete(arrStrm);
229             Delete(data);
230             return compound(img, errMsg);
231         }
232         else if (extension in imageLoaderCallbacks)
233         {
234             return imageLoaderCallbacks[extension](istrm, imageFactory);
235         }
236         else
237         {
238             SuperImage img = null;
239             return compound(img, "No loader registered for " ~ extension);
240         }
241     }
242 
243     bool assetExists(string name)
244     {
245         if (name in assetsByFilename)
246             return true;
247         else
248             return false;
249     }
250 
251     Asset addAsset(Asset asset, string name)
252     {
253         if (!(name in assetsByFilename))
254         {
255             assetsByFilename[name] = asset;
256             if (fs.stat(name, asset.monitorInfo.lastStat))
257                 asset.monitorInfo.fileExists = true;
258         }
259         return asset;
260     }
261 
262     Asset preloadAsset(Asset asset, string name)
263     {
264         if (!(name in assetsByFilename))
265         {
266             assetsByFilename[name] = asset;
267             if (fs.stat(name, asset.monitorInfo.lastStat))
268                 asset.monitorInfo.fileExists = true;
269         }
270 
271         asset.release();
272         asset.threadSafePartLoaded = false;
273         asset.threadUnsafePartLoaded = false;
274 
275         asset.threadSafePartLoaded = loadAssetThreadSafePart(asset, name);
276         if (asset.threadSafePartLoaded)
277             asset.threadUnsafePartLoaded = asset.loadThreadUnsafePart();
278 
279         return asset;
280     }
281 
282     void reloadAsset(Asset asset, string filename)
283     {
284         asset.release();
285         asset.threadSafePartLoaded = false;
286         asset.threadUnsafePartLoaded = false;
287 
288         asset.threadSafePartLoaded = loadAssetThreadSafePart(asset, filename);
289         if (asset.threadSafePartLoaded)
290             asset.threadUnsafePartLoaded = asset.loadThreadUnsafePart();
291     }
292 
293     void reloadAsset(string name)
294     {
295         auto asset = assetsByFilename[name];
296 
297         asset.release();
298         asset.threadSafePartLoaded = false;
299         asset.threadUnsafePartLoaded = false;
300 
301         asset.threadSafePartLoaded = loadAssetThreadSafePart(asset, name);
302         if (asset.threadSafePartLoaded)
303             asset.threadUnsafePartLoaded = asset.loadThreadUnsafePart();
304     }
305 
306     Asset getAsset(string name)
307     {
308         if (name in assetsByFilename)
309             return assetsByFilename[name];
310         else
311             return null;
312     }
313 
314     void removeAsset(string name)
315     {
316         Delete(assetsByFilename[name]);
317         assetsByFilename.remove(name);
318     }
319 
320     void releaseAssets()
321     {
322         clearOwnedObjects();
323         Delete(assetsByFilename);
324         assetsByFilename = New!(Dict!(Asset, string));
325 
326         Delete(loadingThread);
327         loadingThread = New!Thread(&threadFunc);
328     }
329     
330     bool loadAssetThreadSafePart(Asset asset, ubyte[] buffer, string filename)
331     {
332         ArrayStream arrStrm = New!ArrayStream(buffer);
333         bool res = loadAssetThreadSafePart(asset, arrStrm, filename);
334         Delete(arrStrm);
335         return res;
336     }
337     
338     bool loadAssetThreadSafePart(Asset asset, InputStream istrm, string filename)
339     {
340         bool res = asset.loadThreadSafePart(filename, istrm, fs, this);
341         if (!res)
342         {
343             writefln("Error: failed to load asset \"%s\"", filename);
344         }
345         return res;
346     }
347 
348     bool loadAssetThreadSafePart(Asset asset, string filename)
349     {
350         if (!fileExists(filename))
351         {
352             writefln("Error: cannot find file \"%s\"", filename);
353             return false;
354         }
355 
356         auto fstrm = fs.openForInput(filename);
357         bool res = loadAssetThreadSafePart(asset, fstrm, filename);
358         Delete(fstrm);
359         return res;
360     }
361 
362     void threadFunc()
363     {
364         foreach(filename, asset; assetsByFilename)
365         {
366             nextLoadingPercentage += 1.0f / cast(float)(assetsByFilename.length);
367 
368             if (!asset.threadSafePartLoaded)
369             {
370                 asset.threadSafePartLoaded = loadAssetThreadSafePart(asset, filename);
371                 asset.threadUnsafePartLoaded = false;
372             }
373         }
374     }
375 
376     void loadThreadSafePart()
377     {
378         nextLoadingPercentage = 0.0f;
379         monitorTimer = 0.0;
380         loadingThread.start();
381     }
382 
383     bool isLoading()
384     {
385         return loadingThread.isRunning;
386     }
387 
388     bool loadThreadUnsafePart()
389     {
390         bool res = true;
391         foreach(filename, asset; assetsByFilename)
392         //if (!asset.threadUnsafePartLoaded)
393         if (asset.threadSafePartLoaded)
394         {
395             res = asset.loadThreadUnsafePart();
396             asset.threadUnsafePartLoaded = res;
397             if (!res)
398             {
399                 writefln("Error: failed to load asset \"%s\"", filename);
400                 break;
401             }
402         }
403         else
404         {
405             res = false;
406             break;
407         }
408         return res;
409     }
410 
411     bool fileExists(string filename)
412     {
413         FileStat stat;
414         return fs.stat(filename, stat);
415     }
416 
417     void updateMonitor(double dt)
418     {
419         if (liveUpdate)
420         {
421             monitorTimer += dt;
422             if (monitorTimer >= liveUpdatePeriod)
423             {
424                 monitorTimer = 0.0;
425                 foreach(filename, asset; assetsByFilename)
426                     monitorCheck(filename, asset);
427             }
428         }
429     }
430 
431     protected void monitorCheck(string filename, Asset asset)
432     {
433         FileStat currentStat;
434         if (fs.stat(filename, currentStat))
435         {
436             if (!asset.monitorInfo.fileExists)
437             {
438                 asset.monitorInfo.fileExists = true;
439             }
440             else if (currentStat.modificationTimestamp >
441                      asset.monitorInfo.lastStat.modificationTimestamp ||
442                      currentStat.sizeInBytes !=
443                      asset.monitorInfo.lastStat.sizeInBytes)
444             {
445                 reloadAsset(filename);
446                 asset.monitorInfo.lastStat = currentStat;
447                 eventManager.generateFileChangeEvent(filename);
448             }
449         }
450         else
451         {
452             if (asset.monitorInfo.fileExists)
453             {
454                 asset.monitorInfo.fileExists = false;
455             }
456         }
457     }
458 }