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 }