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 §or)
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