1 /** @file clientresources.cpp  Client-side resource subsystem.
2  *
3  * @authors Copyright © 2005-2015 Daniel Swanson <danij@dengine.net>
4  * @authors Copyright © 2003-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
5  * @authors Copyright © 2006-2007 Jamie Jones <jamie_jones_au@yahoo.com.au>
6  *
7  * @par License
8  * GPL: http://www.gnu.org/licenses/gpl.html
9  *
10  * <small>This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by the
12  * Free Software Foundation; either version 2 of the License, or (at your
13  * option) any later version. This program is distributed in the hope that it
14  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
15  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
16  * Public License for more details. You should have received a copy of the GNU
17  * General Public License along with this program; if not, see:
18  * http://www.gnu.org/licenses</small>
19  */
20 
21 #include "de_platform.h"
22 #include "resource/clientresources.h"
23 
24 #include <QHash>
25 #include <QVector>
26 #include <QtAlgorithms>
27 
28 #include <de/memory.h>
29 #include <de/App>
30 #include <de/ArrayValue>
31 #include <de/ByteOrder>
32 #include <de/ByteRefArray>
33 #include <de/DirectoryFeed>
34 #include <de/Function>
35 #include <de/LogBuffer>
36 #include <de/Loop>
37 #include <de/Module>
38 #include <de/NativePath>
39 #include <de/NumberValue>
40 #include <de/PackageLoader>
41 #include <de/Reader>
42 #include <de/RecordValue>
43 #include <de/StringPool>
44 #include <de/Task>
45 #include <de/TaskPool>
46 #include <de/Time>
47 
48 #include <doomsday/console/cmd.h>
49 #include <doomsday/defs/music.h>
50 #include <doomsday/defs/sprite.h>
51 #include <doomsday/doomsdayapp.h>
52 #include <doomsday/filesys/fs_main.h>
53 #include <doomsday/filesys/fs_util.h>
54 #include <doomsday/filesys/lumpindex.h>
55 #include <doomsday/res/AnimGroups>
56 #include <doomsday/res/ColorPalettes>
57 #include <doomsday/res/Composite>
58 #include <doomsday/res/MapManifests>
59 #include <doomsday/res/Patch>
60 #include <doomsday/res/PatchName>
61 #include <doomsday/res/Sprites>
62 #include <doomsday/res/TextureManifest>
63 #include <doomsday/res/Textures>
64 #include <doomsday/world/Material>
65 #include <doomsday/world/Materials>
66 
67 #include "def_main.h"
68 #include "dd_main.h"
69 #include "dd_def.h"
70 
71 #include "clientapp.h"
72 #include "ui/progress.h"
73 #include "ui/clientwindowsystem.h"
74 #include "sys_system.h"  // novideo
75 #include "gl/gl_tex.h"
76 #include "gl/gl_texmanager.h"
77 #include "gl/svg.h"
78 #include "resource/clienttexture.h"
79 #include "render/rend_model.h"
80 #include "render/rend_particle.h"  // Rend_ParticleReleaseSystemTextures
81 #include "render/rendersystem.h"
82 
83 // For smart caching logics:
84 #include "network/net_demo.h"  // playback
85 #include "render/rend_main.h"  // Rend_MapSurfaceMaterialSpec
86 #include "render/billboard.h"  // Rend_SpriteMaterialSpec
87 #include "render/skydrawable.h"
88 
89 #include "world/clientserverworld.h"
90 #include "world/map.h"
91 #include "world/p_object.h"
92 #include "world/sky.h"
93 #include "world/thinkers.h"
94 #include "Sector"
95 #include "Surface"
96 
97 using namespace de;
98 
99 /// @c TST_DETAIL type specifications are stored separately into a set of
100 /// buckets. Bucket selection is determined by their quantized contrast value.
101 #define DETAILVARIANT_CONTRAST_HASHSIZE     (DETAILTEXTURE_CONTRAST_QUANTIZATION_FACTOR+1)
102 
103 // Console variables (globals).
104 byte precacheMapMaterials = true;
105 byte precacheSprites      = true;
106 
get()107 ClientResources &ClientResources::get() // static
108 {
109     return static_cast<ClientResources &>(Resources::get());
110 }
111 
DENG2_PIMPL(ClientResources)112 DENG2_PIMPL(ClientResources)
113 , DENG2_OBSERVES(FontScheme,         ManifestDefined)
114 , DENG2_OBSERVES(FontManifest,       Deletion)
115 , DENG2_OBSERVES(AbstractFont,       Deletion)
116 , DENG2_OBSERVES(res::ColorPalettes, Addition)
117 , DENG2_OBSERVES(res::ColorPalette,  ColorTableChange)
118 {
119     typedef QHash<lumpnum_t, rawtex_t *> RawTextureHash;
120     RawTextureHash rawTexHash;
121 
122     /// System subspace schemes containing the manifests/resources.
123     FontSchemes fontSchemes;
124     QList<FontScheme *> fontSchemeCreationOrder;
125 
126     AllFonts fonts;                    ///< From all schemes.
127     uint fontManifestCount;            ///< Total number of font manifests (in all schemes).
128 
129     uint fontManifestIdMapSize;
130     FontManifest **fontManifestIdMap;  ///< Index with fontid_t-1
131 
132     typedef QVector<FrameModelDef> ModelDefs;
133     ModelDefs modefs;
134     QVector<int> stateModefs;          ///< Index to the modefs array.
135 
136     typedef StringPool ModelRepository;
137     ModelRepository *modelRepository;  ///< Owns FrameModel instances.
138 
139     /// A list of specifications for material variants.
140     typedef QList<MaterialVariantSpec *> MaterialSpecs;
141     MaterialSpecs materialSpecs;
142 
143     typedef QList<TextureVariantSpec *> TextureSpecs;
144     TextureSpecs textureSpecs;
145     TextureSpecs detailTextureSpecs[DETAILVARIANT_CONTRAST_HASHSIZE];
146 
147     struct CacheTask
148     {
149         virtual ~CacheTask() {}
150         virtual void run() = 0;
151     };
152 
153     /**
154      * Stores the arguments for a resource cache work item.
155      */
156     struct MaterialCacheTask : public CacheTask
157     {
158         ClientMaterial *material;
159         MaterialVariantSpec const *spec; /// Interned context specification.
160 
161         MaterialCacheTask(ClientMaterial &resource, MaterialVariantSpec const &contextSpec)
162             : CacheTask()
163             , material(&resource)
164             , spec(&contextSpec)
165         {}
166 
167         void run()
168         {
169             // Cache all dependent assets and upload GL textures if necessary.
170             material->getAnimator(*spec).cacheAssets();
171         }
172     };
173 
174     /// A FIFO queue of material variant caching tasks.
175     /// Implemented as a list because we may need to remove tasks from the queue if
176     /// the material is destroyed in the mean time.
177     typedef QList<CacheTask *> CacheQueue;
178     CacheQueue cacheQueue;
179 
180     Impl(Public *i)
181         : Base(i)
182         , fontManifestCount        (0)
183         , fontManifestIdMapSize    (0)
184         , fontManifestIdMap        (0)
185         , modelRepository          (0)
186     {
187         LOG_AS("ClientResources");
188 
189         res::TextureManifest::setTextureConstructor([] (res::TextureManifest &m) -> res::Texture * {
190             return new ClientTexture(m);
191         });
192 
193         /// @note Order here defines the ambigious-URI search order.
194         createFontScheme("System");
195         createFontScheme("Game");
196 
197         self().colorPalettes().audienceForAddition() += this;
198     }
199 
200     ~Impl()
201     {
202         self().clearAllFontSchemes();
203         clearFontManifests();
204         self().clearAllRawTextures();
205         self().purgeCacheQueue();
206 
207         clearAllTextureSpecs();
208         clearMaterialSpecs();
209 
210         clearModels();
211     }
212 
213     inline de::FS1 &fileSys() { return App_FileSystem(); }
214 
215     void clearFontManifests()
216     {
217         qDeleteAll(fontSchemes);
218         fontSchemes.clear();
219         fontSchemeCreationOrder.clear();
220 
221         // Clear the manifest index/map.
222         if (fontManifestIdMap)
223         {
224             M_Free(fontManifestIdMap); fontManifestIdMap = 0;
225             fontManifestIdMapSize = 0;
226         }
227         fontManifestCount = 0;
228     }
229 
230     void createFontScheme(String name)
231     {
232         DENG2_ASSERT(name.length() >= FontScheme::min_name_length);
233 
234         // Create a new scheme.
235         FontScheme *newScheme = new FontScheme(name);
236         fontSchemes.insert(name.toLower(), newScheme);
237         fontSchemeCreationOrder.append(newScheme);
238 
239         // We want notification when a new manifest is defined in this scheme.
240         newScheme->audienceForManifestDefined += this;
241     }
242 
243     void clearRuntimeFonts()
244     {
245         self().fontScheme("Game").clear();
246 
247         self().pruneUnusedTextureSpecs();
248     }
249 
250     void clearSystemFonts()
251     {
252         self().fontScheme("System").clear();
253 
254         self().pruneUnusedTextureSpecs();
255     }
256 
257     void clearMaterialSpecs()
258     {
259         qDeleteAll(materialSpecs);
260         materialSpecs.clear();
261     }
262 
263     MaterialVariantSpec *findMaterialSpec(MaterialVariantSpec const &tpl,
264         bool canCreate)
265     {
266         foreach (MaterialVariantSpec *spec, materialSpecs)
267         {
268             if (spec->compare(tpl)) return spec;
269         }
270 
271         if (!canCreate) return 0;
272 
273         materialSpecs.append(new MaterialVariantSpec(tpl));
274         return materialSpecs.back();
275     }
276 
277     MaterialVariantSpec &getMaterialSpecForContext(MaterialContextId contextId,
278         int flags, byte border, int tClass, int tMap, int wrapS, int wrapT,
279         int minFilter, int magFilter, int anisoFilter,
280         bool mipmapped, bool gammaCorrection, bool noStretch, bool toAlpha)
281     {
282         static MaterialVariantSpec tpl;
283 
284         texturevariantusagecontext_t primaryContext = TC_UNKNOWN;
285         switch (contextId)
286         {
287         case UiContext:         primaryContext = TC_UI;                 break;
288         case MapSurfaceContext: primaryContext = TC_MAPSURFACE_DIFFUSE; break;
289         case SpriteContext:     primaryContext = TC_SPRITE_DIFFUSE;     break;
290         case ModelSkinContext:  primaryContext = TC_MODELSKIN_DIFFUSE;  break;
291         case PSpriteContext:    primaryContext = TC_PSPRITE_DIFFUSE;    break;
292         case SkySphereContext:  primaryContext = TC_SKYSPHERE_DIFFUSE;  break;
293 
294         default: DENG2_ASSERT(false);
295         }
296 
297         TextureVariantSpec const &primarySpec =
298             self().textureSpec(primaryContext, flags, border, tClass, tMap,
299                              wrapS, wrapT, minFilter, magFilter,
300                              anisoFilter, mipmapped, gammaCorrection,
301                              noStretch, toAlpha);
302 
303         // Apply the normalized spec to the template.
304         tpl.contextId     = contextId;
305         tpl.primarySpec = &primarySpec;
306 
307         return *findMaterialSpec(tpl, true);
308     }
309 
310     static int hashDetailTextureSpec(detailvariantspecification_t const &spec)
311     {
312         return (spec.contrast * (1/255.f) * DETAILTEXTURE_CONTRAST_QUANTIZATION_FACTOR + .5f);
313     }
314 
315     static variantspecification_t &configureTextureSpec(variantspecification_t &spec,
316         texturevariantusagecontext_t tc, int flags, byte border, int tClass, int tMap,
317         int wrapS, int wrapT, int minFilter, int magFilter, int anisoFilter,
318         dd_bool mipmapped, dd_bool gammaCorrection, dd_bool noStretch, dd_bool toAlpha)
319     {
320         DENG2_ASSERT(tc == TC_UNKNOWN || VALID_TEXTUREVARIANTUSAGECONTEXT(tc));
321 
322         flags &= ~TSF_INTERNAL_MASK;
323 
324         spec.context         = tc;
325         spec.flags           = flags;
326         spec.border          = (flags & TSF_UPSCALE_AND_SHARPEN)? 1 : border;
327         spec.mipmapped       = mipmapped;
328         spec.wrapS           = wrapS;
329         spec.wrapT           = wrapT;
330         spec.minFilter       = de::clamp(-1, minFilter, spec.mipmapped? 3:1);
331         spec.magFilter       = de::clamp(-3, magFilter, 1);
332         spec.anisoFilter     = de::clamp(-1, anisoFilter, 4);
333         spec.gammaCorrection = gammaCorrection;
334         spec.noStretch       = noStretch;
335         spec.toAlpha         = toAlpha;
336 
337         if (tClass || tMap)
338         {
339             spec.flags      |= TSF_HAS_COLORPALETTE_XLAT;
340             spec.tClass      = de::max(0, tClass);
341             spec.tMap        = de::max(0, tMap);
342         }
343 
344         return spec;
345     }
346 
347     static detailvariantspecification_t &configureDetailTextureSpec(
348         detailvariantspecification_t &spec, float contrast)
349     {
350         int const quantFactor = DETAILTEXTURE_CONTRAST_QUANTIZATION_FACTOR;
351 
352         spec.contrast = 255 * de::clamp<int>(0, contrast * quantFactor + .5f, quantFactor) * (1 / float(quantFactor));
353         return spec;
354     }
355 
356     TextureVariantSpec &linkTextureSpec(TextureVariantSpec *spec)
357     {
358         DENG2_ASSERT(spec != 0);
359 
360         switch (spec->type)
361         {
362         case TST_GENERAL:
363             textureSpecs.append(spec);
364             break;
365         case TST_DETAIL: {
366             int hash = hashDetailTextureSpec(spec->detailVariant);
367             detailTextureSpecs[hash].append(spec);
368             break; }
369         }
370 
371         return *spec;
372     }
373 
374     TextureVariantSpec *findTextureSpec(TextureVariantSpec const &tpl, bool canCreate)
375     {
376         // Do we already have a concrete version of the template specification?
377         switch (tpl.type)
378         {
379         case TST_GENERAL: {
380             foreach (TextureVariantSpec *varSpec, textureSpecs)
381             {
382                 if (*varSpec == tpl)
383                 {
384                     return varSpec;
385                 }
386             }
387             break; }
388 
389         case TST_DETAIL: {
390             int hash = hashDetailTextureSpec(tpl.detailVariant);
391             foreach (TextureVariantSpec *varSpec, detailTextureSpecs[hash])
392             {
393                 if (*varSpec == tpl)
394                 {
395                     return varSpec;
396                 }
397 
398             }
399             break; }
400         }
401 
402         // Not found, can we create?
403         if (canCreate)
404         {
405             return &linkTextureSpec(new TextureVariantSpec(tpl));
406         }
407 
408         return 0;
409     }
410 
411     TextureVariantSpec *textureSpec(texturevariantusagecontext_t tc, int flags,
412         byte border, int tClass, int tMap, int wrapS, int wrapT, int minFilter,
413         int magFilter, int anisoFilter, dd_bool mipmapped, dd_bool gammaCorrection,
414         dd_bool noStretch, dd_bool toAlpha)
415     {
416         static TextureVariantSpec tpl;
417         tpl.type = TST_GENERAL;
418 
419         configureTextureSpec(tpl.variant, tc, flags, border, tClass, tMap, wrapS,
420             wrapT, minFilter, magFilter, anisoFilter, mipmapped, gammaCorrection,
421             noStretch, toAlpha);
422 
423         // Retrieve a concrete version of the rationalized specification.
424         return findTextureSpec(tpl, true);
425     }
426 
427     TextureVariantSpec *detailTextureSpec(float contrast)
428     {
429         static TextureVariantSpec tpl;
430 
431         tpl.type = TST_DETAIL;
432         configureDetailTextureSpec(tpl.detailVariant, contrast);
433         return findTextureSpec(tpl, true);
434     }
435 
436     bool textureSpecInUse(TextureVariantSpec const &spec)
437     {
438         for (res::Texture *texture : self().textures().allTextures())
439         {
440             for (TextureVariant *variant : static_cast<ClientTexture *>(texture)->variants())
441             {
442                 if (&variant->spec() == &spec)
443                 {
444                     return true; // Found one; stop.
445                 }
446             }
447         }
448         return false;
449     }
450 
451     int pruneUnusedTextureSpecs(TextureSpecs &list)
452     {
453         int numPruned = 0;
454         QMutableListIterator<TextureVariantSpec *> it(list);
455         while (it.hasNext())
456         {
457             TextureVariantSpec *spec = it.next();
458             if (!textureSpecInUse(*spec))
459             {
460                 it.remove();
461                 delete spec;
462                 numPruned += 1;
463             }
464         }
465         return numPruned;
466     }
467 
468     int pruneUnusedTextureSpecs(texturevariantspecificationtype_t specType)
469     {
470         switch (specType)
471         {
472         case TST_GENERAL: return pruneUnusedTextureSpecs(textureSpecs);
473         case TST_DETAIL: {
474             int numPruned = 0;
475             for (int i = 0; i < DETAILVARIANT_CONTRAST_HASHSIZE; ++i)
476             {
477                 numPruned += pruneUnusedTextureSpecs(detailTextureSpecs[i]);
478             }
479             return numPruned; }
480         }
481         return 0;
482     }
483 
484     void clearAllTextureSpecs()
485     {
486         qDeleteAll(textureSpecs);
487         textureSpecs.clear();
488 
489         for (int i = 0; i < DETAILVARIANT_CONTRAST_HASHSIZE; ++i)
490         {
491             qDeleteAll(detailTextureSpecs[i]);
492             detailTextureSpecs[i].clear();
493         }
494     }
495 
496     void processCacheQueue()
497     {
498         while (!cacheQueue.isEmpty())
499         {
500             QScopedPointer<CacheTask> task(cacheQueue.takeFirst());
501             task->run();
502         }
503     }
504 
505     void queueCacheTasksForMaterial(ClientMaterial &material,
506                                     MaterialVariantSpec const &contextSpec,
507                                     bool cacheGroups = true)
508     {
509         // Already in the queue?
510         bool alreadyQueued = false;
511         foreach (CacheTask *baseTask, cacheQueue)
512         {
513             if (MaterialCacheTask *task = dynamic_cast<MaterialCacheTask *>(baseTask))
514             {
515                 if (&material == task->material && &contextSpec == task->spec)
516                 {
517                     alreadyQueued = true;
518                     break;
519                 }
520             }
521         }
522 
523         if (!alreadyQueued)
524         {
525             cacheQueue.append(new MaterialCacheTask(material, contextSpec));
526         }
527 
528         if (!cacheGroups) return;
529 
530         // If the material is part of one or more groups enqueue cache tasks
531         // for all other materials within the same group(s). Although we could
532         // use a flag in the task and have it find the groups come prepare time,
533         // this way we can be sure there are no overlapping tasks.
534         foreach (world::Materials::MaterialManifestGroup *group,
535                  world::Materials::get().allMaterialGroups())
536         {
537             if (!group->contains(&material.manifest()))
538             {
539                 continue;
540             }
541 
542             foreach (world::MaterialManifest *manifest, *group)
543             {
544                 if (!manifest->hasMaterial()) continue;
545 
546                 // Have we already enqueued this material?
547                 if (&manifest->material() == &material) continue;
548 
549                 queueCacheTasksForMaterial(manifest->material().as<ClientMaterial>(),
550                                            contextSpec, false /* do not cache groups */);
551             }
552         }
553     }
554 
555     void queueCacheTasksForSprite(spritenum_t id,
556                                   MaterialVariantSpec const &contextSpec,
557                                   bool cacheGroups = true)
558     {
559         if (auto const *sprites = self().sprites().tryFindSpriteSet(id))
560         {
561             for (Record const &sprite : *sprites)
562             {
563                 defn::Sprite const spriteDef(sprite);
564                 for (auto const &view : spriteDef.def().compiled().views)
565                 {
566                     //de::Uri const &viewMaterial = ; // spriteDef.viewMaterial(iter->first.value->asInt());
567                     if (world::Material *material = world::Materials::get().materialPtr(view.uri))
568                     {
569                         queueCacheTasksForMaterial(material->as<ClientMaterial>(),
570                                                    contextSpec, cacheGroups);
571                     }
572                 }
573             }
574         }
575     }
576 
577     void queueCacheTasksForModel(FrameModelDef &modelDef)
578     {
579         if (!useModels) return;
580 
581         for (duint sub = 0; sub < modelDef.subCount(); ++sub)
582         {
583             SubmodelDef &subdef = modelDef.subModelDef(sub);
584             FrameModel *mdl = modelForId(subdef.modelId);
585             if (!mdl) continue;
586 
587             // Load all skins.
588             for (FrameModelSkin const &skin : mdl->skins())
589             {
590                 if (ClientTexture *tex = static_cast<ClientTexture *>(skin.texture))
591                 {
592                     tex->prepareVariant(Rend_ModelDiffuseTextureSpec(mdl->flags().testFlag(FrameModel::NoTextureCompression)));
593                 }
594             }
595 
596             // Load the shiny skin too.
597             if (ClientTexture *shinyTex = static_cast<ClientTexture *>(subdef.shinySkin))
598             {
599                 shinyTex->prepareVariant(Rend_ModelShinyTextureSpec());
600             }
601         }
602     }
603 
604     void clearModels()
605     {
606         /// @todo Why only centralized memory deallocation? Bad (lazy) design...
607         modefs.clear();
608         stateModefs.clear();
609 
610         clearModelList();
611 
612         if (modelRepository)
613         {
614             delete modelRepository; modelRepository = nullptr;
615         }
616     }
617 
618     FrameModel *modelForId(modelid_t id)
619     {
620         DENG2_ASSERT(modelRepository);
621         return reinterpret_cast<FrameModel *>(modelRepository->userPointer(id));
622     }
623 
624     inline String const &findModelPath(modelid_t id)
625     {
626         return modelRepository->stringRef(id);
627     }
628 
629     /**
630      * Create a new modeldef or find an existing one. This is for ID'd models.
631      */
632     FrameModelDef *getModelDefWithId(String id)
633     {
634         if (id.isEmpty()) return nullptr;
635 
636         // First try to find an existing modef.
637         if (self().hasModelDef(id))
638         {
639             return &self().modelDef(id);
640         }
641 
642         // Get a new entry.
643         modefs.append(FrameModelDef(id.toUtf8().constData()));
644         return &modefs.last();
645     }
646 
647     /**
648      * Create a new modeldef or find an existing one. There can be only one model
649      * definition associated with a state/intermark pair.
650      */
651     FrameModelDef *getModelDef(dint state, dfloat interMark, dint select)
652     {
653         // Is this a valid state?
654         if (state < 0 || state >= runtimeDefs.states.size())
655         {
656             return nullptr;
657         }
658 
659         // First try to find an existing modef.
660         for (FrameModelDef const &modef : modefs)
661         {
662             if (modef.state == &runtimeDefs.states[state] &&
663                fequal(modef.interMark, interMark) && modef.select == select)
664             {
665                 // Models are loaded in reverse order; this one already has a model.
666                 return nullptr;
667             }
668         }
669 
670         modefs.append(FrameModelDef());
671         FrameModelDef *md = &modefs.last();
672 
673         // Set initial data.
674         md->state     = &runtimeDefs.states[state];
675         md->interMark = interMark;
676         md->select    = select;
677 
678         return md;
679     }
680 
681     String findSkinPath(Path const &skinPath, Path const &modelFilePath)
682     {
683         //DENG2_ASSERT(!skinPath.isEmpty());
684 
685         // Try the "first choice" directory first.
686         if (!modelFilePath.isEmpty())
687         {
688             // The "first choice" directory is that in which the model file resides.
689             try
690             {
691                 return fileSys().findPath(de::Uri("Models", modelFilePath.toString().fileNamePath() / skinPath.fileName()),
692                                           RLF_DEFAULT, self().resClass(RC_GRAPHIC));
693             }
694             catch (FS1::NotFoundError const &)
695             {}  // Ignore this error.
696         }
697 
698         /// @throws FS1::NotFoundError if no resource was found.
699         return fileSys().findPath(de::Uri("Models", skinPath), RLF_DEFAULT,
700                                   self().resClass(RC_GRAPHIC));
701     }
702 
703     /**
704      * Allocate room for a new skin file name.
705      */
706     short defineSkinAndAddToModelIndex(FrameModel &mdl, Path const &skinPath)
707     {
708         if (ClientTexture *tex = static_cast<ClientTexture *>(self().textures().defineTexture("ModelSkins", de::Uri(skinPath))))
709         {
710             // A duplicate? (return existing skin number)
711             for (dint i = 0; i < mdl.skinCount(); ++i)
712             {
713                 if (mdl.skin(i).texture == tex)
714                     return i;
715             }
716 
717             // Add this new skin.
718             mdl.newSkin(skinPath.toString()).texture = tex;
719             return mdl.skinCount() - 1;
720         }
721 
722         return -1;
723     }
724 
725     void defineAllSkins(FrameModel &mdl)
726     {
727         String const &modelFilePath = findModelPath(mdl.modelId());
728 
729         dint numFoundSkins = 0;
730         for (dint i = 0; i < mdl.skinCount(); ++i)
731         {
732             FrameModelSkin &skin = mdl.skin(i);
733             try
734             {
735                 de::Uri foundResourceUri(Path(findSkinPath(skin.name, modelFilePath)));
736 
737                 skin.texture = self().textures().defineTexture("ModelSkins", foundResourceUri);
738 
739                 // We have found one more skin for this model.
740                 numFoundSkins += 1;
741             }
742             catch (FS1::NotFoundError const &)
743             {
744                 LOG_RES_VERBOSE("Failed to locate \"%s\" (#%i) for model \"%s\"")
745                         << skin.name << i << NativePath(modelFilePath).pretty();
746             }
747         }
748 
749         if (!numFoundSkins)
750         {
751             // Lastly try a skin named similarly to the model in the same directory.
752             de::Uri searchPath(modelFilePath.fileNamePath() / modelFilePath.fileNameWithoutExtension(), RC_GRAPHIC);
753             try
754             {
755                 String foundPath = fileSys().findPath(searchPath, RLF_DEFAULT,
756                                                       self().resClass(RC_GRAPHIC));
757                 // Ensure the found path is absolute.
758                 foundPath = App_BasePath() / foundPath;
759 
760                 defineSkinAndAddToModelIndex(mdl, foundPath);
761                 // We have found one more skin for this model.
762                 numFoundSkins = 1;
763 
764                 LOG_RES_MSG("Assigned fallback skin \"%s\" to index #0 for model \"%s\"")
765                     << NativePath(foundPath).pretty()
766                     << NativePath(modelFilePath).pretty();
767             }
768             catch (FS1::NotFoundError const &)
769             {}  // Ignore this error.
770         }
771 
772         if (!numFoundSkins)
773         {
774             LOG_RES_MSG("No skins found for model \"%s\" (it may use a custom skin specified in a DED)")
775                 << NativePath(modelFilePath).pretty();
776         }
777 
778 #ifdef DENG_DEBUG
779         LOGDEV_RES_XVERBOSE("Model \"%s\" skins:", NativePath(modelFilePath).pretty());
780         dint skinIdx = 0;
781         for (FrameModelSkin const &skin : mdl.skins())
782         {
783             res::TextureManifest const *texManifest = skin.texture? &skin.texture->manifest() : 0;
784             LOGDEV_RES_XVERBOSE("  %i: %s %s",
785                        (skinIdx++) << skin.name
786                     << (texManifest? (String("\"") + texManifest->composeUri() + "\"") : "(missing texture)")
787                     << (texManifest? (String(" => \"") + NativePath(texManifest->resourceUri().compose()).pretty() + "\"") : ""));
788         }
789 #endif
790     }
791 
792     /**
793      * Scales the given model so that it'll be 'destHeight' units tall. Measurements
794      * are based on submodel zero. Scale is applied uniformly.
795      */
796     void scaleModel(FrameModelDef &mf, dfloat destHeight, dfloat offset)
797     {
798         if (!mf.subCount()) return;
799 
800         SubmodelDef &smf = mf.subModelDef(0);
801 
802         // No model to scale?
803         if (!smf.modelId) return;
804 
805         // Find the top and bottom heights.
806         dfloat top, bottom;
807         dfloat height = self().model(smf.modelId).frame(smf.frame).horizontalRange(&top, &bottom);
808         if (fequal(height, 0.f)) height = 1;
809 
810         dfloat scale = destHeight / height;
811 
812         mf.scale    = Vector3f(scale, scale, scale);
813         mf.offset.y = -bottom * scale + offset;
814     }
815 
816     void scaleModelToSprite(FrameModelDef &mf, Record const *spriteRec)
817     {
818         if (!spriteRec) return;
819 
820         defn::Sprite sprite(*spriteRec);
821         if (!sprite.hasView(0)) return;
822 
823         world::Material *mat = world::Materials::get().materialPtr(sprite.viewMaterial(0));
824         if (!mat) return;
825 
826         MaterialAnimator &matAnimator = mat->as<ClientMaterial>().getAnimator(Rend_SpriteMaterialSpec());
827         matAnimator.prepare();  // Ensure we have up-to-date info.
828 
829         ClientTexture const &texture = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture->base();
830         dint off = de::max(0, -texture.origin().y - int(matAnimator.dimensions().y));
831 
832         scaleModel(mf, matAnimator.dimensions().y, off);
833     }
834 
835     dfloat calcModelVisualRadius(FrameModelDef *def)
836     {
837         if (!def || !def->subModelId(0)) return 0;
838 
839         // Use the first frame bounds.
840         Vector3f min, max;
841         dfloat maxRadius = 0;
842         for (duint i = 0; i < def->subCount(); ++i)
843         {
844             if (!def->subModelId(i)) break;
845 
846             SubmodelDef &sub = def->subModelDef(i);
847 
848             self().model(sub.modelId).frame(sub.frame).bounds(min, max);
849 
850             // Half the distance from bottom left to top right.
851             dfloat radius = (  def->scale.x * (max.x - min.x)
852                              + def->scale.z * (max.z - min.z)) / 3.5f;
853             if (radius > maxRadius)
854             {
855                 maxRadius = radius;
856             }
857         }
858 
859         return maxRadius;
860     }
861 
862     /**
863      * Creates a modeldef based on the given DED info. A pretty straightforward
864      * operation. No interlinks are set yet. Autoscaling is done and the scale
865      * factors set appropriately. After this has been called for all available
866      * Model DEDs, each State that has a model will have a pointer to the one
867      * with the smallest intermark (start of a chain).
868      */
869     void setupModel(defn::Model const &def)
870     {
871         LOG_AS("setupModel");
872 
873         auto &defs = *DED_Definitions();
874 
875         dint const modelScopeFlags = def.geti("flags") | defs.modelFlags;
876         dint const statenum = defs.getStateNum(def.gets("state"));
877 
878         // Is this an ID'd model?
879         FrameModelDef *modef = getModelDefWithId(def.gets("id"));
880         if (!modef)
881         {
882             // No, normal State-model.
883             if (statenum < 0) return;
884 
885             modef = getModelDef(statenum + def.geti("off"), def.getf("interMark"), def.geti("selector"));
886             if (!modef) return; // Overridden or invalid definition.
887         }
888 
889         // Init modef info (state & intermark already set).
890         modef->def       = def;
891         modef->group     = def.getui("group");
892         modef->flags     = modelScopeFlags;
893         modef->offset    = Vector3f(def.get("offset"));
894         modef->offset.y += defs.modelOffset; // Common Y axis offset.
895         modef->scale     = Vector3f(def.get("scale"));
896         modef->scale.y  *= defs.modelScale;  // Common Y axis scaling.
897         modef->resize    = def.getf("resize");
898         modef->skinTics  = de::max(def.geti("skinTics"), 1);
899         for (dint i = 0; i < 2; ++i)
900         {
901             modef->interRange[i] = float(def.geta("interRange")[i].asNumber());
902         }
903 
904         // Submodels.
905         modef->clearSubs();
906         for (dint i = 0; i < def.subCount(); ++i)
907         {
908             Record const &subdef = def.sub(i);
909             SubmodelDef *sub = modef->addSub();
910 
911             sub->modelId = 0;
912 
913             if (subdef.gets("filename").isEmpty()) continue;
914 
915             de::Uri const searchPath(subdef.gets("filename"));
916             if (searchPath.isEmpty()) continue;
917 
918             try
919             {
920                 String foundPath = fileSys().findPath(searchPath, RLF_DEFAULT,
921                                                       self().resClass(RC_MODEL));
922                 // Ensure the found path is absolute.
923                 foundPath = App_BasePath() / foundPath;
924 
925                 // Have we already loaded this?
926                 modelid_t modelId = modelRepository->intern(foundPath);
927                 FrameModel *mdl = modelForId(modelId);
928                 if (!mdl)
929                 {
930                     // Attempt to load it in now.
931                     QScopedPointer<FileHandle> hndl(&fileSys().openFile(foundPath, "rb"));
932 
933                     mdl = FrameModel::loadFromFile(*hndl, modelAspectMod);
934 
935                     // We're done with the file.
936                     fileSys().releaseFile(hndl->file());
937 
938                     // Loaded?
939                     if (mdl)
940                     {
941                         // Add it to the repository,
942                         mdl->setModelId(modelId);
943                         modelRepository->setUserPointer(modelId, mdl);
944 
945                         defineAllSkins(*mdl);
946 
947                         // Enlarge the vertex buffers in preparation for drawing of this model.
948                         if (!Rend_ModelExpandVertexBuffers(mdl->vertexCount()))
949                         {
950                             LOG_RES_WARNING("Model \"%s\" contains more than %u max vertices (%i), it will not be rendered")
951                                 << NativePath(foundPath).pretty()
952                                 << uint(RENDER_MAX_MODEL_VERTS) << mdl->vertexCount();
953                         }
954                     }
955                 }
956 
957                 // Loaded?
958                 if (!mdl) continue;
959 
960                 sub->modelId    = mdl->modelId();
961                 sub->frame      = mdl->frameNumber(subdef.gets("frame"));
962                 if (sub->frame < 0) sub->frame = 0;
963                 sub->frameRange = de::max(1, subdef.geti("frameRange")); // Frame range must always be greater than zero.
964 
965                 sub->alpha      = byte(de::clamp(0, int(255 - subdef.getf("alpha") * 255), 255));
966                 sub->blendMode  = blendmode_t(subdef.geti("blendMode"));
967 
968                 // Submodel-specific flags cancel out model-scope flags!
969                 sub->setFlags(modelScopeFlags ^ subdef.geti("flags"));
970 
971                 // Flags may override alpha and/or blendmode.
972                 if (sub->testFlag(MFF_BRIGHTSHADOW))
973                 {
974                     sub->alpha = byte(256 * .80f);
975                     sub->blendMode = BM_ADD;
976                 }
977                 else if (sub->testFlag(MFF_BRIGHTSHADOW2))
978                 {
979                     sub->blendMode = BM_ADD;
980                 }
981                 else if (sub->testFlag(MFF_DARKSHADOW))
982                 {
983                     sub->blendMode = BM_DARK;
984                 }
985                 else if (sub->testFlag(MFF_SHADOW2))
986                 {
987                     sub->alpha = byte(256 * .2f);
988                 }
989                 else if (sub->testFlag(MFF_SHADOW1))
990                 {
991                     sub->alpha = byte(256 * .62f);
992                 }
993 
994                 // Extra blendmodes:
995                 if (sub->testFlag(MFF_REVERSE_SUBTRACT))
996                 {
997                     sub->blendMode = BM_REVERSE_SUBTRACT;
998                 }
999                 else if (sub->testFlag(MFF_SUBTRACT))
1000                 {
1001                     sub->blendMode = BM_SUBTRACT;
1002                 }
1003 
1004                 if (!subdef.gets("skinFilename").isEmpty())
1005                 {
1006                     // A specific file name has been given for the skin.
1007                     String const &skinFilePath  = de::Uri(subdef.gets("skinFilename")).path();
1008                     String const &modelFilePath = findModelPath(sub->modelId);
1009                     try
1010                     {
1011                         Path foundResourcePath(findSkinPath(skinFilePath, modelFilePath));
1012 
1013                         sub->skin = defineSkinAndAddToModelIndex(*mdl, foundResourcePath);
1014                     }
1015                     catch (FS1::NotFoundError const &)
1016                     {
1017                         LOG_RES_WARNING("Failed to locate skin \"%s\" for model \"%s\"")
1018                             << subdef.gets("skinFilename") << NativePath(modelFilePath).pretty();
1019                     }
1020                 }
1021                 else
1022                 {
1023                     sub->skin = subdef.geti("skin");
1024                 }
1025 
1026                 // Skin range must always be greater than zero.
1027                 sub->skinRange = de::max(subdef.geti("skinRange"), 1);
1028 
1029                 // Offset within the model.
1030                 sub->offset = subdef.get("offset");
1031 
1032                 if (!subdef.gets("shinySkin").isEmpty())
1033                 {
1034                     String const &skinFilePath  = de::Uri(subdef.gets("shinySkin")).path();
1035                     String const &modelFilePath = findModelPath(sub->modelId);
1036                     try
1037                     {
1038                         de::Uri foundResourceUri(Path(findSkinPath(skinFilePath, modelFilePath)));
1039 
1040                         sub->shinySkin = self().textures().defineTexture("ModelReflectionSkins", foundResourceUri);
1041                     }
1042                     catch (FS1::NotFoundError const &)
1043                     {
1044                         LOG_RES_WARNING("Failed to locate skin \"%s\" for model \"%s\"")
1045                             << skinFilePath << NativePath(modelFilePath).pretty();
1046                     }
1047                 }
1048                 else
1049                 {
1050                     sub->shinySkin = 0;
1051                 }
1052 
1053                 // Should we allow texture compression with this model?
1054                 if (sub->testFlag(MFF_NO_TEXCOMP))
1055                 {
1056                     // All skins of this model will no longer use compression.
1057                     mdl->setFlags(FrameModel::NoTextureCompression);
1058                 }
1059             }
1060             catch (FS1::NotFoundError const &)
1061             {
1062                 LOG_RES_WARNING("Failed to locate \"%s\"") << searchPath;
1063             }
1064         }
1065 
1066         // Do scaling, if necessary.
1067         if (modef->resize)
1068         {
1069             scaleModel(*modef, modef->resize, modef->offset.y);
1070         }
1071         else if (modef->state && modef->testSubFlag(0, MFF_AUTOSCALE))
1072         {
1073             spritenum_t sprNum = DED_Definitions()->getSpriteNum(def.gets("sprite"));
1074             int sprFrame       = def.geti("spriteFrame");
1075 
1076             if (sprNum < 0)
1077             {
1078                 // No sprite ID given.
1079                 sprNum   = modef->state->sprite;
1080                 sprFrame = modef->state->frame;
1081             }
1082 
1083             if (Record const *sprite = self().sprites().spritePtr(sprNum, sprFrame))
1084             {
1085                 scaleModelToSprite(*modef, sprite);
1086             }
1087         }
1088 
1089         if (modef->state)
1090         {
1091             int stateNum = runtimeDefs.states.indexOf(modef->state);
1092 
1093             // Associate this modeldef with its state.
1094             if (stateModefs[stateNum] < 0)
1095             {
1096                 // No modef; use this.
1097                 stateModefs[stateNum] = self().indexOf(modef);
1098             }
1099             else
1100             {
1101                 // Must check intermark; smallest wins!
1102                 FrameModelDef *other = self().modelDefForState(stateNum);
1103 
1104                 if ((modef->interMark <= other->interMark && // Should never be ==
1105                     modef->select == other->select) || modef->select < other->select) // Smallest selector?
1106                 {
1107                     stateModefs[stateNum] = self().indexOf(modef);
1108                 }
1109             }
1110         }
1111 
1112         // Calculate the particle offset for each submodel.
1113         Vector3f min, max;
1114         for (uint i = 0; i < modef->subCount(); ++i)
1115         {
1116             SubmodelDef *sub = &modef->subModelDef(i);
1117             if (sub->modelId && sub->frame >= 0)
1118             {
1119                 self().model(sub->modelId).frame(sub->frame).bounds(min, max);
1120                 modef->setParticleOffset(i, ((max + min) / 2 + sub->offset) * modef->scale + modef->offset);
1121             }
1122         }
1123 
1124         modef->visualRadius = calcModelVisualRadius(modef); // based on geometry bounds
1125 
1126         // Shadow radius can be specified manually.
1127         modef->shadowRadius = def.getf("shadowRadius");
1128     }
1129 
1130     void clearModelList()
1131     {
1132         if (!modelRepository) return;
1133 
1134         modelRepository->forAll([this] (StringPool::Id id)
1135         {
1136             if (auto *model = reinterpret_cast<FrameModel *>(modelRepository->userPointer(id)))
1137             {
1138                 modelRepository->setUserPointer(id, nullptr);
1139                 delete model;
1140             }
1141             return LoopContinue;
1142         });
1143     }
1144 
1145     /// Observes FontScheme ManifestDefined.
1146     void fontSchemeManifestDefined(FontScheme & /*scheme*/, FontManifest &manifest)
1147     {
1148         // We want notification when the manifest is derived to produce a resource.
1149         //manifest.audienceForFontDerived += this;
1150 
1151         // We want notification when the manifest is about to be deleted.
1152         manifest.audienceForDeletion += this;
1153 
1154         // Acquire a new unique identifier for the manifest.
1155         fontid_t const id = ++fontManifestCount; // 1-based.
1156         manifest.setUniqueId(id);
1157 
1158         // Add the new manifest to the id index/map.
1159         if (fontManifestCount > fontManifestIdMapSize)
1160         {
1161             // Allocate more memory.
1162             fontManifestIdMapSize += 32;
1163             fontManifestIdMap = (FontManifest **) M_Realloc(fontManifestIdMap, sizeof(*fontManifestIdMap) * fontManifestIdMapSize);
1164         }
1165         fontManifestIdMap[fontManifestCount - 1] = &manifest;
1166     }
1167 
1168 #if 0
1169     /// Observes FontManifest FontDerived.
1170     void fontManifestFontDerived(FontManifest & /*manifest*/, AbstractFont &font)
1171     {
1172         // Include this new font in the scheme-agnostic list of instances.
1173         fonts.append(&font);
1174 
1175         // We want notification when the font is about to be deleted.
1176         font.audienceForDeletion += this;
1177     }
1178 #endif
1179 
1180     /// Observes FontManifest Deletion.
1181     void fontManifestBeingDeleted(FontManifest const &manifest)
1182     {
1183         fontManifestIdMap[manifest.uniqueId() - 1 /*1-based*/] = 0;
1184 
1185         // There will soon be one fewer manifest in the system.
1186         fontManifestCount -= 1;
1187     }
1188 
1189     /// Observes AbstractFont Deletion.
1190     void fontBeingDeleted(AbstractFont const &font)
1191     {
1192         fonts.removeOne(const_cast<AbstractFont *>(&font));
1193     }
1194 
1195     void colorPaletteAdded(res::ColorPalette &newPalette)
1196     {
1197         // Observe changes to the color table so we can schedule texture updates.
1198         newPalette.audienceForColorTableChange += this;
1199     }
1200 
1201     /// Observes ColorPalette ColorTableChange
1202     void colorPaletteColorTableChanged(res::ColorPalette &colorPalette)
1203     {
1204         // Release all GL-textures prepared using @a colorPalette.
1205         foreach (res::Texture *texture, self().textures().allTextures())
1206         {
1207             colorpalette_analysis_t *cp = reinterpret_cast<colorpalette_analysis_t *>(texture->analysisDataPointer(res::Texture::ColorPaletteAnalysis));
1208             if (cp && cp->paletteId == colorpaletteid_t(colorPalette.id()))
1209             {
1210                 texture->release();
1211             }
1212         }
1213     }
1214 };
1215 
ClientResources()1216 ClientResources::ClientResources() : d(new Impl(this))
1217 {}
1218 
clear()1219 void ClientResources::clear()
1220 {
1221     Resources::clear();
1222 
1223     R_ShutdownSvgs();
1224 }
1225 
clearAllRuntimeResources()1226 void ClientResources::clearAllRuntimeResources()
1227 {
1228     Resources::clearAllRuntimeResources();
1229 
1230     d->clearRuntimeFonts();
1231     pruneUnusedTextureSpecs();
1232 }
1233 
clearAllSystemResources()1234 void ClientResources::clearAllSystemResources()
1235 {
1236     Resources::clearAllSystemResources();
1237 
1238     d->clearSystemFonts();
1239     pruneUnusedTextureSpecs();
1240 }
1241 
initSystemTextures()1242 void ClientResources::initSystemTextures()
1243 {
1244     Resources::initSystemTextures();
1245 
1246     if (novideo) return;
1247 
1248     LOG_AS("ClientResources");
1249 
1250     static struct {
1251         String const graphicName;
1252         Path const path;
1253     } const texDefs[] = {
1254         { "bbox",       "bbox" },
1255         { "gray",       "gray" },
1256         //{ "boxcorner",  "ui/boxcorner" },
1257         //{ "boxfill",    "ui/boxfill" },
1258         //{ "boxshade",   "ui/boxshade" }
1259     };
1260 
1261     LOG_RES_VERBOSE("Initializing System textures...");
1262 
1263     for (auto const &def : texDefs)
1264     {
1265         textures().declareSystemTexture(def.path, de::Uri("Graphics", def.graphicName));
1266     }
1267 
1268     // Define any as yet undefined system textures.
1269     /// @todo Defer until necessary (manifest texture is first referenced).
1270     textures().deriveAllTexturesInScheme("System");
1271 }
1272 
reloadAllResources()1273 void ClientResources::reloadAllResources()
1274 {
1275     DENG2_ASSERT_IN_MAIN_THREAD();
1276     DENG2_ASSERT(QOpenGLContext::currentContext() != nullptr);
1277 
1278     Resources::reloadAllResources();
1279     DD_UpdateEngineState();
1280 }
1281 
rawTexture(lumpnum_t lumpNum)1282 rawtex_t *ClientResources::rawTexture(lumpnum_t lumpNum)
1283 {
1284     LOG_AS("ClientResources::rawTexture");
1285     if (-1 == lumpNum || lumpNum >= App_FileSystem().lumpCount())
1286     {
1287         LOGDEV_RES_WARNING("LumpNum #%i out of bounds (%i), returning 0")
1288                 << lumpNum << App_FileSystem().lumpCount();
1289         return nullptr;
1290     }
1291 
1292     auto found = d->rawTexHash.find(lumpNum);
1293     return (found != d->rawTexHash.end() ? found.value() : nullptr);
1294 }
1295 
declareRawTexture(lumpnum_t lumpNum)1296 rawtex_t *ClientResources::declareRawTexture(lumpnum_t lumpNum)
1297 {
1298     LOG_AS("ClientResources::rawTexture");
1299     if (-1 == lumpNum || lumpNum >= App_FileSystem().lumpCount())
1300     {
1301         LOGDEV_RES_WARNING("LumpNum #%i out of range %s, returning 0")
1302             << lumpNum << Rangeui(0, App_FileSystem().lumpCount()).asText();
1303         return nullptr;
1304     }
1305 
1306     // Has this raw texture already been declared?
1307     rawtex_t *raw = rawTexture(lumpNum);
1308     if (!raw)
1309     {
1310         // An entirely new raw texture.
1311         raw = new rawtex_t(App_FileSystem().lump(lumpNum).name(), lumpNum);
1312         d->rawTexHash.insert(lumpNum, raw);
1313     }
1314 
1315     return raw;
1316 }
1317 
collectRawTextures() const1318 QList<rawtex_t *> ClientResources::collectRawTextures() const
1319 {
1320     return d->rawTexHash.values();
1321 }
1322 
clearAllRawTextures()1323 void ClientResources::clearAllRawTextures()
1324 {
1325     qDeleteAll(d->rawTexHash);
1326     d->rawTexHash.clear();
1327 }
1328 
releaseAllSystemGLTextures()1329 void ClientResources::releaseAllSystemGLTextures()
1330 {
1331     if (::novideo) return;
1332 
1333     LOG_AS("ResourceSystem");
1334     LOG_RES_VERBOSE("Releasing system textures...");
1335 
1336     // The rendering lists contain persistent references to texture names.
1337     // Which, obviously, can't persist any longer...
1338     ClientApp::renderSystem().clearDrawLists();
1339 
1340     GL_ReleaseAllLightingSystemTextures();
1341     GL_ReleaseAllFlareTextures();
1342 
1343     releaseGLTexturesByScheme("System");
1344     Rend_ParticleReleaseSystemTextures();
1345     releaseFontGLTexturesByScheme("System");
1346 
1347     pruneUnusedTextureSpecs();
1348 }
1349 
releaseAllRuntimeGLTextures()1350 void ClientResources::releaseAllRuntimeGLTextures()
1351 {
1352     if (::novideo) return;
1353 
1354     LOG_AS("ResourceSystem");
1355     LOG_RES_VERBOSE("Releasing runtime textures...");
1356 
1357     // The rendering lists contain persistent references to texture names.
1358     // Which, obviously, can't persist any longer...
1359     ClientApp::renderSystem().clearDrawLists();
1360 
1361     // texture-wrapped GL textures; textures, flats, sprites...
1362     releaseGLTexturesByScheme("Flats");
1363     releaseGLTexturesByScheme("Textures");
1364     releaseGLTexturesByScheme("Patches");
1365     releaseGLTexturesByScheme("Sprites");
1366     releaseGLTexturesByScheme("Details");
1367     releaseGLTexturesByScheme("Reflections");
1368     releaseGLTexturesByScheme("Masks");
1369     releaseGLTexturesByScheme("ModelSkins");
1370     releaseGLTexturesByScheme("ModelReflectionSkins");
1371     releaseGLTexturesByScheme("Lightmaps");
1372     releaseGLTexturesByScheme("Flaremaps");
1373     GL_ReleaseTexturesForRawImages();
1374 
1375     Rend_ParticleReleaseExtraTextures();
1376     releaseFontGLTexturesByScheme("Game");
1377 
1378     pruneUnusedTextureSpecs();
1379 }
1380 
releaseAllGLTextures()1381 void ClientResources::releaseAllGLTextures()
1382 {
1383     releaseAllRuntimeGLTextures();
1384     releaseAllSystemGLTextures();
1385 }
1386 
releaseGLTexturesByScheme(String schemeName)1387 void ClientResources::releaseGLTexturesByScheme(String schemeName)
1388 {
1389     if (schemeName.isEmpty()) return;
1390 
1391     PathTreeIterator<res::TextureScheme::Index> iter(textures().textureScheme(schemeName).index().leafNodes());
1392     while (iter.hasNext())
1393     {
1394         res::TextureManifest &manifest = iter.next();
1395         if (manifest.hasTexture())
1396         {
1397             manifest.texture().release();
1398         }
1399     }
1400 }
1401 
clearAllTextureSpecs()1402 void ClientResources::clearAllTextureSpecs()
1403 {
1404     d->clearAllTextureSpecs();
1405 }
1406 
pruneUnusedTextureSpecs()1407 void ClientResources::pruneUnusedTextureSpecs()
1408 {
1409     if (Sys_IsShuttingDown()) return;
1410 
1411     dint numPruned = 0;
1412     numPruned += d->pruneUnusedTextureSpecs(TST_GENERAL);
1413     numPruned += d->pruneUnusedTextureSpecs(TST_DETAIL);
1414 
1415     LOGDEV_RES_VERBOSE("Pruned %i unused texture variant %s")
1416         << numPruned << (numPruned == 1? "specification" : "specifications");
1417 }
1418 
textureSpec(texturevariantusagecontext_t tc,dint flags,byte border,dint tClass,dint tMap,dint wrapS,dint wrapT,dint minFilter,dint magFilter,dint anisoFilter,dd_bool mipmapped,dd_bool gammaCorrection,dd_bool noStretch,dd_bool toAlpha)1419 TextureVariantSpec const &ClientResources::textureSpec(texturevariantusagecontext_t tc,
1420     dint flags, byte border, dint tClass, dint tMap, dint wrapS, dint wrapT, dint minFilter,
1421     dint magFilter, dint anisoFilter, dd_bool mipmapped, dd_bool gammaCorrection,
1422     dd_bool noStretch, dd_bool toAlpha)
1423 {
1424     TextureVariantSpec *tvs =
1425         d->textureSpec(tc, flags, border, tClass, tMap, wrapS, wrapT, minFilter,
1426                        magFilter, anisoFilter, mipmapped, gammaCorrection,
1427                        noStretch, toAlpha);
1428 
1429 #ifdef DENG_DEBUG
1430     if (tClass || tMap)
1431     {
1432         DENG2_ASSERT(tvs->variant.flags & TSF_HAS_COLORPALETTE_XLAT);
1433         DENG2_ASSERT(tvs->variant.tClass == tClass);
1434         DENG2_ASSERT(tvs->variant.tMap == tMap);
1435     }
1436 #endif
1437 
1438     return *tvs;
1439 }
1440 
detailTextureSpec(dfloat contrast)1441 TextureVariantSpec &ClientResources::detailTextureSpec(dfloat contrast)
1442 {
1443     return *d->detailTextureSpec(contrast);
1444 }
1445 
fontScheme(String name) const1446 FontScheme &ClientResources::fontScheme(String name) const
1447 {
1448     LOG_AS("ClientResources::fontScheme");
1449     if (!name.isEmpty())
1450     {
1451         FontSchemes::iterator found = d->fontSchemes.find(name.toLower());
1452         if (found != d->fontSchemes.end()) return **found;
1453     }
1454     /// @throw UnknownSchemeError An unknown scheme was referenced.
1455     throw UnknownSchemeError("ClientResources::fontScheme", "No scheme found matching '" + name + "'");
1456 }
1457 
knownFontScheme(String name) const1458 bool ClientResources::knownFontScheme(String name) const
1459 {
1460     if (!name.isEmpty())
1461     {
1462         return d->fontSchemes.contains(name.toLower());
1463     }
1464     return false;
1465 }
1466 
allFontSchemes() const1467 ClientResources::FontSchemes const &ClientResources::allFontSchemes() const
1468 {
1469     return d->fontSchemes;
1470 }
1471 
hasFont(de::Uri const & path) const1472 bool ClientResources::hasFont(de::Uri const &path) const
1473 {
1474     try
1475     {
1476         fontManifest(path);
1477         return true;
1478     }
1479     catch (MissingResourceManifestError const &)
1480     {}  // Ignore this error.
1481     return false;
1482 }
1483 
fontManifest(de::Uri const & uri) const1484 FontManifest &ClientResources::fontManifest(de::Uri const &uri) const
1485 {
1486     LOG_AS("ClientResources::findFont");
1487 
1488     // Perform the search.
1489     // Is this a URN? (of the form "urn:schemename:uniqueid")
1490     if (!uri.scheme().compareWithoutCase("urn"))
1491     {
1492         String const &pathStr = uri.path().toStringRef();
1493         dint uIdPos = pathStr.indexOf(':');
1494         if (uIdPos > 0)
1495         {
1496             String schemeName = pathStr.left(uIdPos);
1497             dint uniqueId     = pathStr.mid(uIdPos + 1 /*skip delimiter*/).toInt();
1498 
1499             try
1500             {
1501                 return fontScheme(schemeName).findByUniqueId(uniqueId);
1502             }
1503             catch (FontScheme::NotFoundError const &)
1504             {}  // Ignore, we'll throw our own...
1505         }
1506     }
1507     else
1508     {
1509         // No, this is a URI.
1510         String const &path = uri.path();
1511 
1512         // Does the user want a manifest in a specific scheme?
1513         if (!uri.scheme().isEmpty())
1514         {
1515             try
1516             {
1517                 return fontScheme(uri.scheme()).find(path);
1518             }
1519             catch (FontScheme::NotFoundError const &)
1520             {}  // Ignore, we'll throw our own...
1521         }
1522         else
1523         {
1524             // No, check each scheme in priority order.
1525             for (FontScheme *scheme : d->fontSchemeCreationOrder)
1526             {
1527                 try
1528                 {
1529                     return scheme->find(path);
1530                 }
1531                 catch (FontScheme::NotFoundError const &)
1532                 {}  // Ignore, we'll throw our own...
1533             }
1534         }
1535     }
1536 
1537     /// @throw MissingResourceManifestError  Failed to locate a matching manifest.
1538     throw MissingResourceManifestError("ClientResources::findFont", "Failed to locate a manifest matching \"" + uri.asText() + "\"");
1539 }
1540 
toFontManifest(fontid_t id) const1541 FontManifest &ClientResources::toFontManifest(fontid_t id) const
1542 {
1543     if (id > 0 && id <= d->fontManifestCount)
1544     {
1545         duint32 idx = id - 1;  // 1-based index.
1546         if (d->fontManifestIdMap[idx])
1547         {
1548             return *d->fontManifestIdMap[idx];
1549         }
1550         DENG2_ASSERT(!"Bookkeeping error");
1551     }
1552 
1553     /// @throw UnknownIdError The specified manifest id is invalid.
1554     throw UnknownFontIdError("ClientResources::toFontManifest", QString("Invalid font ID %1, valid range [1..%2)").arg(id).arg(d->fontManifestCount + 1));
1555 }
1556 
allFonts() const1557 ClientResources::AllFonts const &ClientResources::allFonts() const
1558 {
1559     return d->fonts;
1560 }
1561 
newFontFromDef(ded_compositefont_t const & def)1562 AbstractFont *ClientResources::newFontFromDef(ded_compositefont_t const &def)
1563 {
1564     LOG_AS("ClientResources::newFontFromDef");
1565 
1566     if (!def.uri) return nullptr;
1567     de::Uri const &uri = *def.uri;
1568 
1569     try
1570     {
1571         // Create/retrieve a manifest for the would-be font.
1572         FontManifest &manifest = declareFont(uri);
1573         if (manifest.hasResource())
1574         {
1575             if (auto *compFont = maybeAs<CompositeBitmapFont>(manifest.resource()))
1576             {
1577                 /// @todo Do not update fonts here (not enough knowledge). We should
1578                 /// instead return an invalid reference/signal and force the caller
1579                 /// to implement the necessary update logic.
1580                 LOGDEV_RES_XVERBOSE("Font with uri \"%s\" already exists, returning existing",
1581                                     manifest.composeUri());
1582 
1583                 compFont->rebuildFromDef(def);
1584             }
1585             return &manifest.resource();
1586         }
1587 
1588         // A new font.
1589         manifest.setResource(CompositeBitmapFont::fromDef(manifest, def));
1590         if (manifest.hasResource())
1591         {
1592             if (verbose >= 1)
1593             {
1594                 LOG_RES_VERBOSE("New font \"%s\"")
1595                     << manifest.composeUri();
1596             }
1597             return &manifest.resource();
1598         }
1599 
1600         LOG_RES_WARNING("Failed defining new Font for \"%s\"")
1601             << NativePath(uri.asText()).pretty();
1602     }
1603     catch (UnknownSchemeError const &er)
1604     {
1605         LOG_RES_WARNING("Failed declaring font \"%s\": %s")
1606             << NativePath(uri.asText()).pretty() << er.asText();
1607     }
1608     catch (FontScheme::InvalidPathError const &er)
1609     {
1610         LOG_RES_WARNING("Failed declaring font \"%s\": %s")
1611             << NativePath(uri.asText()).pretty() << er.asText();
1612     }
1613 
1614     return nullptr;
1615 }
1616 
newFontFromFile(de::Uri const & uri,String filePath)1617 AbstractFont *ClientResources::newFontFromFile(de::Uri const &uri, String filePath)
1618 {
1619     LOG_AS("ClientResources::newFontFromFile");
1620 
1621     if (!d->fileSys().accessFile(de::Uri::fromNativePath(filePath)))
1622     {
1623         LOGDEV_RES_WARNING("Ignoring invalid filePath: ") << filePath;
1624         return nullptr;
1625     }
1626 
1627     try
1628     {
1629         // Create/retrieve a manifest for the would-be font.
1630         FontManifest &manifest = declareFont(uri);
1631 
1632         if (manifest.hasResource())
1633         {
1634             if (auto *bmapFont = maybeAs<BitmapFont>(manifest.resource()))
1635             {
1636                 /// @todo Do not update fonts here (not enough knowledge). We should
1637                 /// instead return an invalid reference/signal and force the caller
1638                 /// to implement the necessary update logic.
1639                 LOGDEV_RES_XVERBOSE("Font with uri \"%s\" already exists, returning existing",
1640                                     manifest.composeUri());
1641 
1642                 bmapFont->setFilePath(filePath);
1643             }
1644             return &manifest.resource();
1645         }
1646 
1647         // A new font.
1648         manifest.setResource(BitmapFont::fromFile(manifest, filePath));
1649         if (manifest.hasResource())
1650         {
1651             if (verbose >= 1)
1652             {
1653                 LOG_RES_VERBOSE("New font \"%s\"")
1654                     << manifest.composeUri();
1655             }
1656             return &manifest.resource();
1657         }
1658 
1659         LOG_RES_WARNING("Failed defining new Font for \"%s\"")
1660             << NativePath(uri.asText()).pretty();
1661     }
1662     catch (UnknownSchemeError const &er)
1663     {
1664         LOG_RES_WARNING("Failed declaring font \"%s\": %s")
1665             << NativePath(uri.asText()).pretty() << er.asText();
1666     }
1667     catch (FontScheme::InvalidPathError const &er)
1668     {
1669         LOG_RES_WARNING("Failed declaring font \"%s\": %s")
1670             << NativePath(uri.asText()).pretty() << er.asText();
1671     }
1672 
1673     return nullptr;
1674 }
1675 
releaseFontGLTexturesByScheme(String schemeName)1676 void ClientResources::releaseFontGLTexturesByScheme(String schemeName)
1677 {
1678     if (schemeName.isEmpty()) return;
1679 
1680     PathTreeIterator<FontScheme::Index> iter(fontScheme(schemeName).index().leafNodes());
1681     while (iter.hasNext())
1682     {
1683         FontManifest &manifest = iter.next();
1684         if (manifest.hasResource())
1685         {
1686             manifest.resource().glDeinit();
1687         }
1688     }
1689 }
1690 
model(modelid_t id)1691 FrameModel &ClientResources::model(modelid_t id)
1692 {
1693     if (FrameModel *model = d->modelForId(id)) return *model;
1694     /// @throw MissingResourceError An unknown/invalid id was specified.
1695     throw MissingResourceError("ClientResources::model", "Invalid id " + String::number(id));
1696 }
1697 
hasModelDef(String id) const1698 bool ClientResources::hasModelDef(String id) const
1699 {
1700     if (!id.isEmpty())
1701     {
1702         for (FrameModelDef const &modef : d->modefs)
1703         {
1704             if (!id.compareWithoutCase(modef.id))
1705             {
1706                 return true;
1707             }
1708         }
1709     }
1710     return false;
1711 }
1712 
modelDef(dint index)1713 FrameModelDef &ClientResources::modelDef(dint index)
1714 {
1715     if (index >= 0 && index < modelDefCount()) return d->modefs[index];
1716     /// @throw MissingModelDefError An unknown model definition was referenced.
1717     throw MissingModelDefError("ClientResources::modelDef", "Invalid index #" + String::number(index) + ", valid range " + Rangeui(0, modelDefCount()).asText());
1718 }
1719 
modelDef(String id)1720 FrameModelDef &ClientResources::modelDef(String id)
1721 {
1722     if (!id.isEmpty())
1723     {
1724         for (FrameModelDef const &modef : d->modefs)
1725         {
1726             if (!id.compareWithoutCase(modef.id))
1727             {
1728                 return const_cast<FrameModelDef &>(modef);
1729             }
1730         }
1731     }
1732     /// @throw MissingModelDefError An unknown model definition was referenced.
1733     throw MissingModelDefError("ClientResources::modelDef", "Invalid id '" + id + "'");
1734 }
1735 
modelDefForState(dint stateIndex,dint select)1736 FrameModelDef *ClientResources::modelDefForState(dint stateIndex, dint select)
1737 {
1738     if (stateIndex < 0 || stateIndex >= DED_Definitions()->states.size())
1739         return nullptr;
1740     if (stateIndex < 0 || stateIndex >= d->stateModefs.count())
1741         return nullptr;
1742     if (d->stateModefs[stateIndex] < 0)
1743         return nullptr;
1744 
1745     DENG2_ASSERT(d->stateModefs[stateIndex] >= 0);
1746     DENG2_ASSERT(d->stateModefs[stateIndex] < d->modefs.count());
1747 
1748     FrameModelDef *def = &d->modefs[d->stateModefs[stateIndex]];
1749     if (select)
1750     {
1751         // Choose the correct selector, or selector zero if the given one not available.
1752         dint const mosel = (select & DDMOBJ_SELECTOR_MASK);
1753         for (FrameModelDef *it = def; it; it = it->selectNext)
1754         {
1755             if (it->select == mosel)
1756             {
1757                 return it;
1758             }
1759         }
1760     }
1761 
1762     return def;
1763 }
1764 
modelDefCount() const1765 dint ClientResources::modelDefCount() const
1766 {
1767     return d->modefs.count();
1768 }
1769 
initModels()1770 void ClientResources::initModels()
1771 {
1772     LOG_AS("ResourceSystem");
1773 
1774     if (CommandLine_Check("-nomd2"))
1775     {
1776         LOG_RES_NOTE("3D models are disabled");
1777         return;
1778     }
1779 
1780     LOG_RES_VERBOSE("Initializing Models...");
1781     Time begunAt;
1782 
1783     d->clearModelList();
1784     d->modefs.clear();
1785 
1786     delete d->modelRepository;
1787     d->modelRepository = new StringPool();
1788 
1789     auto &defs = *DED_Definitions();
1790 
1791     // There can't be more modeldefs than there are DED Models.
1792     d->modefs.resize(defs.models.size());
1793 
1794     // Clear the stateid => modeldef LUT.
1795     d->stateModefs.resize(runtimeDefs.states.size());
1796     for (dint i = 0; i < runtimeDefs.states.size(); ++i)
1797     {
1798         d->stateModefs[i] = -1;
1799     }
1800 
1801     // Read in the model files and their data.
1802     // Use the latest definition available for each sprite ID.
1803     for (dint i = dint(defs.models.size()) - 1; i >= 0; --i)
1804     {
1805         if (!(i % 100))
1806         {
1807             // This may take a while, so keep updating the progress.
1808             Con_SetProgress(130 + 70*(defs.models.size() - i)/defs.models.size());
1809         }
1810 
1811         d->setupModel(defs.models[i]);
1812     }
1813 
1814     // Create interlinks. Note that the order in which the defs were loaded
1815     // is important. We want to allow "patch" definitions, right?
1816 
1817     // For each modeldef we will find the "next" def.
1818     for (dint i = d->modefs.count() - 1; i >= 0; --i)
1819     {
1820         FrameModelDef *me = &d->modefs[i];
1821 
1822         dfloat minmark = 2; // max = 1, so this is "out of bounds".
1823 
1824         FrameModelDef *closest = 0;
1825         for (dint k = d->modefs.count() - 1; k >= 0; --k)
1826         {
1827             FrameModelDef *other = &d->modefs[k];
1828 
1829             /// @todo Need an index by state. -jk
1830             if (other->state != me->state) continue;
1831 
1832             // Same state and a bigger order are the requirements.
1833             if (other->def.order() > me->def.order() && // Defined after me.
1834                 other->interMark > me->interMark &&
1835                 other->interMark < minmark &&
1836                 other->select == me->select)
1837             {
1838                 minmark = other->interMark;
1839                 closest = other;
1840             }
1841         }
1842 
1843         me->interNext = closest;
1844     }
1845 
1846     // Create selectlinks.
1847     for (dint i = d->modefs.count() - 1; i >= 0; --i)
1848     {
1849         FrameModelDef *me = &d->modefs[i];
1850 
1851         dint minsel = DDMAXINT;
1852 
1853         FrameModelDef *closest = 0;
1854 
1855         // Start scanning from the next definition.
1856         for (dint k = d->modefs.count() - 1; k >= 0; --k)
1857         {
1858             FrameModelDef *other = &d->modefs[k];
1859 
1860             // Same state and a bigger order are the requirements.
1861             if (other->state == me->state &&
1862                 other->def.order() > me->def.order() && // Defined after me.
1863                 other->select > me->select && other->select < minsel &&
1864                 other->interMark >= me->interMark)
1865             {
1866                 minsel = other->select;
1867                 closest = other;
1868             }
1869         }
1870 
1871         me->selectNext = closest;
1872     }
1873 
1874     LOG_RES_MSG("Model init completed in %.2f seconds") << begunAt.since();
1875 }
1876 
indexOf(FrameModelDef const * modelDef)1877 dint ClientResources::indexOf(FrameModelDef const *modelDef)
1878 {
1879     dint index = dint(modelDef - &d->modefs[0]);
1880     return (index >= 0 && index < d->modefs.count() ? index : -1);
1881 }
1882 
setModelDefFrame(FrameModelDef & modef,dint frame)1883 void ClientResources::setModelDefFrame(FrameModelDef &modef, dint frame)
1884 {
1885     for (duint i = 0; i < modef.subCount(); ++i)
1886     {
1887         SubmodelDef &subdef = modef.subModelDef(i);
1888         if (subdef.modelId == NOMODELID) continue;
1889 
1890         // Modify the modeldef itself: set the current frame.
1891         subdef.frame = frame % model(subdef.modelId).frameCount();
1892     }
1893 }
1894 
purgeCacheQueue()1895 void ClientResources::purgeCacheQueue()
1896 {
1897     qDeleteAll(d->cacheQueue);
1898     d->cacheQueue.clear();
1899 }
1900 
processCacheQueue()1901 void ClientResources::processCacheQueue()
1902 {
1903     d->processCacheQueue();
1904 }
1905 
cache(ClientMaterial & material,MaterialVariantSpec const & spec,bool cacheGroups)1906 void ClientResources::cache(ClientMaterial &material, MaterialVariantSpec const &spec,
1907                             bool cacheGroups)
1908 {
1909     d->queueCacheTasksForMaterial(material, spec, cacheGroups);
1910 }
1911 
cache(spritenum_t spriteId,MaterialVariantSpec const & spec)1912 void ClientResources::cache(spritenum_t spriteId, MaterialVariantSpec const &spec)
1913 {
1914     d->queueCacheTasksForSprite(spriteId, spec);
1915 }
1916 
cache(FrameModelDef * modelDef)1917 void ClientResources::cache(FrameModelDef *modelDef)
1918 {
1919     if (!modelDef) return;
1920     d->queueCacheTasksForModel(*modelDef);
1921 }
1922 
materialSpec(MaterialContextId contextId,dint flags,byte border,dint tClass,dint tMap,dint wrapS,dint wrapT,dint minFilter,dint magFilter,dint anisoFilter,bool mipmapped,bool gammaCorrection,bool noStretch,bool toAlpha)1923 MaterialVariantSpec const &ClientResources::materialSpec(MaterialContextId contextId,
1924     dint flags, byte border, dint tClass, dint tMap, dint wrapS, dint wrapT,
1925     dint minFilter, dint magFilter, dint anisoFilter,
1926     bool mipmapped, bool gammaCorrection, bool noStretch, bool toAlpha)
1927 {
1928     return d->getMaterialSpecForContext(contextId, flags, border, tClass, tMap,
1929                                         wrapS, wrapT, minFilter, magFilter, anisoFilter,
1930                                         mipmapped, gammaCorrection, noStretch, toAlpha);
1931 }
1932 
cacheForCurrentMap()1933 void ClientResources::cacheForCurrentMap()
1934 {
1935     // Don't precache when playing a demo (why not? -ds).
1936     if (playback) return;
1937 
1938     world::Map &map = App_World().map();
1939 
1940     if (precacheMapMaterials)
1941     {
1942         MaterialVariantSpec const &spec = Rend_MapSurfaceMaterialSpec();
1943 
1944         map.forAllLines([this, &spec] (Line &line)
1945         {
1946             for (dint i = 0; i < 2; ++i)
1947             {
1948                 LineSide &side = line.side(i);
1949                 if (!side.hasSections()) continue;
1950 
1951                 if (side.middle().hasMaterial())
1952                     cache(side.middle().material().as<ClientMaterial>(), spec);
1953 
1954                 if (side.top().hasMaterial())
1955                     cache(side.top().material().as<ClientMaterial>(), spec);
1956 
1957                 if (side.bottom().hasMaterial())
1958                     cache(side.bottom().material().as<ClientMaterial>(), spec);
1959             }
1960             return LoopContinue;
1961         });
1962 
1963         map.forAllSectors([this, &spec] (Sector &sector)
1964         {
1965             // Skip sectors with no line sides as their planes will never be drawn.
1966             if (sector.sideCount())
1967             {
1968                 sector.forAllPlanes([this, &spec] (Plane &plane)
1969                 {
1970                     if (plane.surface().hasMaterial())
1971                     {
1972                         cache(plane.surface().material().as<ClientMaterial>(), spec);
1973                     }
1974                     return LoopContinue;
1975                 });
1976             }
1977             return LoopContinue;
1978         });
1979     }
1980 
1981     if (precacheSprites)
1982     {
1983         MaterialVariantSpec const &matSpec = Rend_SpriteMaterialSpec();
1984 
1985         for (dint i = 0; i < sprites().spriteCount(); ++i)
1986         {
1987             auto const sprite = spritenum_t(i);
1988 
1989             // Is this sprite used by a state of at least one mobj?
1990             LoopResult found = map.thinkers().forAll(reinterpret_cast<thinkfunc_t>(gx.MobjThinker),
1991                                                      0x1/*public*/, [&sprite] (thinker_t *th)
1992             {
1993                 auto const &mob = *reinterpret_cast<mobj_t *>(th);
1994                 if (mob.type >= 0 && mob.type < runtimeDefs.mobjInfo.size())
1995                 {
1996                     /// @todo optimize: traverses the entire state list!
1997                     for (dint k = 0; k < DED_Definitions()->states.size(); ++k)
1998                     {
1999                         if (runtimeDefs.stateInfo[k].owner != &runtimeDefs.mobjInfo[mob.type])
2000                             continue;
2001 
2002                         if (Def_GetState(k)->sprite == sprite)
2003                         {
2004                             return LoopAbort;  // Found one.
2005                         }
2006                     }
2007                 }
2008                 return LoopContinue;
2009             });
2010 
2011             if (found)
2012             {
2013                 cache(sprite, matSpec);
2014             }
2015         }
2016     }
2017 
2018     // Precache model skins?
2019     /// @note The skins are also bound here once so they should be ready
2020     /// for use the next time they are needed.
2021     if (useModels && precacheSkins)
2022     {
2023         map.thinkers().forAll(reinterpret_cast<thinkfunc_t>(gx.MobjThinker),
2024                               0x1/*public*/, [this] (thinker_t *th)
2025         {
2026             auto const &mob = *reinterpret_cast<mobj_t *>(th);
2027             // Check through all the model definitions.
2028             for (dint i = 0; i < modelDefCount(); ++i)
2029             {
2030                 FrameModelDef &modef = modelDef(i);
2031 
2032                 if (!modef.state) continue;
2033                 if (mob.type < 0 || mob.type >= runtimeDefs.mobjInfo.size()) continue; // Hmm?
2034                 if (runtimeDefs.stateInfo[runtimeDefs.states.indexOf(modef.state)].owner != &runtimeDefs.mobjInfo[mob.type]) continue;
2035 
2036                 cache(&modef);
2037             }
2038             return LoopContinue;
2039         });
2040     }
2041 }
2042 
2043 /**
2044  * @param scheme    Resource subspace scheme being printed. Can be @c NULL in
2045  *                  which case resources are printed from all schemes.
2046  * @param like      Resource path search term.
2047  * @param composeUriFlags  Flags governing how URIs should be composed.
2048  */
printFontIndex2(FontScheme * scheme,Path const & like,de::Uri::ComposeAsTextFlags composeUriFlags)2049 static int printFontIndex2(FontScheme *scheme, Path const &like,
2050     de::Uri::ComposeAsTextFlags composeUriFlags)
2051 {
2052     FontScheme::Index::FoundNodes found;
2053     if (scheme) // Only resources in this scheme.
2054     {
2055         scheme->index().findAll(found, res::pathBeginsWithComparator, const_cast<Path *>(&like));
2056     }
2057     else // Consider resources in any scheme.
2058     {
2059         foreach (FontScheme *scheme, App_Resources().allFontSchemes())
2060         {
2061             scheme->index().findAll(found, res::pathBeginsWithComparator, const_cast<Path *>(&like));
2062         }
2063     }
2064     if (found.isEmpty()) return 0;
2065 
2066     bool const printSchemeName = !(composeUriFlags & de::Uri::OmitScheme);
2067 
2068     // Print a heading.
2069     String heading = "Known fonts";
2070     if (!printSchemeName && scheme)
2071         heading += " in scheme '" + scheme->name() + "'";
2072     if (!like.isEmpty())
2073         heading += " like \"" _E(b) + like.toStringRef() + _E(.) "\"";
2074     LOG_RES_MSG(_E(D) "%s:" _E(.)) << heading;
2075 
2076     // Print the result index.
2077     qSort(found.begin(), found.end(), comparePathTreeNodePathsAscending<FontManifest>);
2078     int numFoundDigits = de::max(3/*idx*/, M_NumDigits(found.count()));
2079     int idx = 0;
2080     foreach (FontManifest *manifest, found)
2081     {
2082         String info = String("%1: %2%3" _E(.))
2083                         .arg(idx, numFoundDigits)
2084                         .arg(manifest->hasResource()? _E(1) : _E(2))
2085                         .arg(manifest->description(composeUriFlags));
2086 
2087         LOG_RES_MSG("  " _E(>)) << info;
2088         idx++;
2089     }
2090 
2091     return found.count();
2092 }
2093 
printFontIndex(de::Uri const & search,de::Uri::ComposeAsTextFlags flags=de::Uri::DefaultComposeAsTextFlags)2094 static void printFontIndex(de::Uri const &search,
2095     de::Uri::ComposeAsTextFlags flags = de::Uri::DefaultComposeAsTextFlags)
2096 {
2097     int printTotal = 0;
2098 
2099     // Collate and print results from all schemes?
2100     if (search.scheme().isEmpty() && !search.path().isEmpty())
2101     {
2102         printTotal = printFontIndex2(0/*any scheme*/, search.path(), flags & ~de::Uri::OmitScheme);
2103         LOG_RES_MSG(_E(R));
2104     }
2105     // Print results within only the one scheme?
2106     else if (App_Resources().knownFontScheme(search.scheme()))
2107     {
2108         printTotal = printFontIndex2(&App_Resources().fontScheme(search.scheme()),
2109                                      search.path(), flags | de::Uri::OmitScheme);
2110         LOG_RES_MSG(_E(R));
2111     }
2112     else
2113     {
2114         // Collect and sort results in each scheme separately.
2115         foreach (FontScheme *scheme, App_Resources().allFontSchemes())
2116         {
2117             int numPrinted = printFontIndex2(scheme, search.path(), flags | de::Uri::OmitScheme);
2118             if (numPrinted)
2119             {
2120                 LOG_MSG(_E(R));
2121                 printTotal += numPrinted;
2122             }
2123         }
2124     }
2125     LOG_RES_MSG("Found " _E(b) "%i" _E(.) " %s.") << printTotal << (printTotal == 1? "font" : "fonts in total");
2126 }
2127 
isKnownFontSchemeCallback(String name)2128 static bool isKnownFontSchemeCallback(String name)
2129 {
2130     return App_Resources().knownFontScheme(name);
2131 }
2132 
D_CMD(ListFonts)2133 D_CMD(ListFonts)
2134 {
2135     DENG2_UNUSED(src);
2136 
2137     de::Uri search = de::Uri::fromUserInput(&argv[1], argc - 1, &isKnownFontSchemeCallback);
2138     if (!search.scheme().isEmpty() &&
2139        !App_Resources().knownFontScheme(search.scheme()))
2140     {
2141         LOG_RES_WARNING("Unknown scheme %s") << search.scheme();
2142         return false;
2143     }
2144 
2145     printFontIndex(search);
2146     return true;
2147 }
2148 
2149 #ifdef DENG_DEBUG
D_CMD(PrintFontStats)2150 D_CMD(PrintFontStats)
2151 {
2152     DENG2_UNUSED3(src, argc, argv);
2153 
2154     LOG_MSG(_E(b) "Font Statistics:");
2155     foreach (FontScheme *scheme, App_Resources().allFontSchemes())
2156     {
2157         FontScheme::Index const &index = scheme->index();
2158 
2159         uint const count = index.count();
2160         LOG_MSG("Scheme: %s (%u %s)")
2161             << scheme->name() << count << (count == 1? "font" : "fonts");
2162         index.debugPrintHashDistribution();
2163         index.debugPrint();
2164     }
2165     return true;
2166 }
2167 #endif // DENG_DEBUG
2168 
consoleRegister()2169 void ClientResources::consoleRegister() // static
2170 {
2171     Resources::consoleRegister();
2172 
2173     C_CMD("listfonts",      "ss",   ListFonts)
2174     C_CMD("listfonts",      "s",    ListFonts)
2175     C_CMD("listfonts",      "",     ListFonts)
2176 #ifdef DENG_DEBUG
2177     C_CMD("fontstats",      NULL,   PrintFontStats)
2178 #endif
2179 }
2180