Implement quality levels for actors & corresponding setting.

An actor file, as referenced by the VisualActor, can now define
different actors for different "quality level" setting.
In this initial version, the quality is handled directly by the object
manager.

Actor format impact:
- '<qualitylevels>' may be used as the root node, containing actor nodes
as children.
  - such actor nodes can refer to a file, or to an inline actor, or
simply be inlined.
  - such actor nodes may have a 'quality' attribute, specifying the
maximum quality level of this actor. By default, 255 (the maximum) is
implied.
- The actor format remains valid, but 'groups', 'variants', 'material',
'castshadow' and 'float' can be given a [minquality, maxquality[ range
via XML attributes. Outside of this range, the XML node is ignored
(making it possible to define, in a single actor file, several quality
levels).

Quality is a 0-255 value, with:
- Range 0-99 intended for lower level-of-detail actors (billboards,
etc.)
- Range 100-200 the 'normal' range for models. 100 is "low", 150
"medium", and 200 "high".
- Range 201-255 used for higher quality actors that might be used for
e.g. cinematics.
The range is wide to make it easier to add intermediate levels in the
future and it seemed easier given that an integer value of some kind was
required anyways.

Engine impacts:
- A new CActorDef class is introduced, wrapping an art/actors XML file
and its different quality levels. ObjectBase remains the definition of a
given 'actor', now at a given quality level.
- CActorDef imposes a maximal # of quality level for a particular actor
definition (5 currently).
- CUnit is made to refer to an Actor Definition explicitly, not a
particular ObjectBase.
- As a minor optimisation, variation keys are calculated on
pointer-to-sets-of-selections, instead of raw sets-of-selections, as
this reduces copying.
- some refactoring, including better const-correctness and hotloading
support via std::shared_ptr.

Differential Revision: https://code.wildfiregames.com/D3787
This was SVN commit r25210.
This commit is contained in:
wraitii
2021-04-08 07:22:24 +00:00
parent e3695abe59
commit 76acc4e146
23 changed files with 1063 additions and 657 deletions
+79 -96
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -22,6 +22,7 @@
#include "graphics/ObjectBase.h"
#include "graphics/ObjectEntry.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Game.h"
#include "ps/Profile.h"
#include "ps/Filesystem.h"
@@ -30,12 +31,11 @@
#include "simulation2/components/ICmpTerrain.h"
#include "simulation2/components/ICmpVisual.h"
bool CObjectManager::ObjectKey::operator< (const CObjectManager::ObjectKey& a) const
{
if (ActorName < a.ActorName)
if (ObjectBaseIdentifier < a.ObjectBaseIdentifier)
return true;
else if (ActorName > a.ActorName)
else if (ObjectBaseIdentifier > a.ObjectBaseIdentifier)
return false;
else
return ActorVariation < a.ActorVariation;
@@ -51,6 +51,8 @@ CObjectManager::CObjectManager(CMeshManager& meshManager, CSkeletonAnimManager&
{
RegisterFileReloadFunc(ReloadChangedFileCB, this);
m_QualityHook = std::make_unique<CConfigDBHook>(g_ConfigDB.RegisterHookAndCall("max_actor_quality", [this]() { this->ActorQualityChanged(); }));
if (!CXeromyces::AddValidator(g_VFS, "actor", "art/actors/actor.rng"))
LOGERROR("CObjectManager: failed to load actor grammar file 'art/actors/actor.rng'");
}
@@ -59,92 +61,74 @@ CObjectManager::~CObjectManager()
{
UnloadObjects();
g_ConfigDB.UnregisterHook(std::move(m_QualityHook));
UnregisterFileReloadFunc(ReloadChangedFileCB, this);
}
CObjectBase* CObjectManager::FindObjectBase(const CStrW& objectname)
CActorDef* CObjectManager::FindActorDef(const CStrW& actorName)
{
ENSURE(!objectname.empty());
ENSURE(!actorName.empty());
// See if the base type has been loaded yet:
decltype(m_ActorDefs)::iterator it = m_ActorDefs.find(actorName);
if (it != m_ActorDefs.end())
return it->second.get();
std::map<CStrW, CObjectBase*>::iterator it = m_ObjectBases.find(objectname);
if (it != m_ObjectBases.end())
return it->second;
std::unique_ptr<CActorDef> actor = std::make_unique<CActorDef>(*this);
// Not already loaded, so try to load it:
VfsPath pathname = VfsPath("art/actors/") / actorName;
CObjectBase* obj = new CObjectBase(*this);
if (actor->Load(pathname))
return m_ActorDefs.emplace(actorName, std::move(actor)).first->second.get();
VfsPath pathname = VfsPath("art/actors/") / objectname;
LOGERROR("CObjectManager::FindActorDef(): Cannot find actor '%s'", utf8_from_wstring(actorName));
if (obj->Load(pathname))
{
m_ObjectBases[objectname] = obj;
return obj;
}
else
delete obj;
LOGERROR("CObjectManager::FindObjectBase(): Cannot find object '%s'", utf8_from_wstring(objectname));
return 0;
return nullptr;
}
CObjectEntry* CObjectManager::FindObject(const CStrW& objname)
CObjectEntry* CObjectManager::FindObjectVariation(const CActorDef* actor, const std::vector<std::set<CStr>>& selections, uint32_t seed)
{
std::vector<std::set<CStr> > selections; // TODO - should this really be empty?
return FindObjectVariation(objname, selections);
if (!actor)
return nullptr;
const std::shared_ptr<CObjectBase>& base = actor->GetBase(m_QualityLevel);
std::vector<const std::set<CStr>*> completeSelections;
for (const std::set<CStr>& selectionSet : selections)
completeSelections.emplace_back(&selectionSet);
// To maintain a consistent look between quality levels, first complete with the highest-quality variants.
// then complete again at the required quality level (since not all variants may be available).
std::set<CStr> highQualitySelections = actor->GetBase(255)->CalculateRandomRemainingSelections(seed, selections);
completeSelections.emplace_back(&highQualitySelections);
// We don't have to pass the high-quality selections here because they have higher priority anyways.
std::set<CStr> remainingSelections = base->CalculateRandomRemainingSelections(seed, selections);
completeSelections.emplace_back(&remainingSelections);
return FindObjectVariation(base, completeSelections);
}
CObjectEntry* CObjectManager::FindObjectVariation(const CStrW& objname, const std::vector<std::set<CStr> >& selections)
CObjectEntry* CObjectManager::FindObjectVariation(const std::shared_ptr<CObjectBase>& base, const std::vector<const std::set<CStr>*>& completeSelections)
{
CObjectBase* base = FindObjectBase(objname);
if (! base)
return NULL;
return FindObjectVariation(base, selections);
}
CObjectEntry* CObjectManager::FindObjectVariation(CObjectBase* base, const std::vector<std::set<CStr> >& selections)
{
PROFILE("object variation loading");
PROFILE2("FindObjectVariation");
// Look to see whether this particular variation has already been loaded
std::vector<u8> choices = base->CalculateVariationKey(selections);
ObjectKey key (base->m_Pathname.string(), choices);
std::map<ObjectKey, CObjectEntry*>::iterator it = m_Objects.find(key);
std::vector<u8> choices = base->CalculateVariationKey(completeSelections);
ObjectKey key (base->GetIdentifier(), choices);
decltype(m_Objects)::iterator it = m_Objects.find(key);
if (it != m_Objects.end() && !it->second->m_Outdated)
return it->second;
return it->second.get();
// If it hasn't been loaded, load it now
// If it hasn't been loaded, load it now.
// TODO: If there was an existing ObjectEntry, but it's outdated (due to hotloading),
// we'll get a memory leak when replacing its entry in m_Objects. The problem is
// some CUnits might still have a pointer to the old ObjectEntry so we probably can't
// just delete it now. Probably we need to redesign the caching/hotloading system so it
// makes more sense (e.g. use shared_ptr); for now I'll just leak, to avoid making the logic
// more complex than it is already is, since this only matters for the rare case of hotloading.
CObjectEntry* obj = new CObjectEntry(base, m_Simulation); // TODO: type ?
std::unique_ptr<CObjectEntry> obj = std::make_unique<CObjectEntry>(base, m_Simulation);
// TODO (for some efficiency): use the pre-calculated choices for this object,
// which has already worked out what to do for props, instead of passing the
// selections into BuildVariation and having it recalculate the props' choices.
if (! obj->BuildVariation(selections, choices, *this))
{
DeleteObject(obj);
return NULL;
}
if (!obj->BuildVariation(completeSelections, choices, *this))
return nullptr;
m_Objects[key] = obj;
return obj;
return m_Objects.emplace(key, std::move(obj)).first->second.get();
}
CTerrain* CObjectManager::GetTerrain()
@@ -155,53 +139,52 @@ CTerrain* CObjectManager::GetTerrain()
return cmpTerrain->GetCTerrain();
}
void CObjectManager::DeleteObject(CObjectEntry* entry)
{
std::function<bool(const std::pair<ObjectKey, CObjectEntry*>&)> second_equals =
[&entry](const std::pair<ObjectKey, CObjectEntry*>& a) { return a.second == entry; };
std::map<ObjectKey, CObjectEntry*>::iterator it = m_Objects.begin();
while (m_Objects.end() != (it = find_if(it, m_Objects.end(), second_equals)))
it = m_Objects.erase(it);
delete entry;
}
void CObjectManager::UnloadObjects()
{
for (const std::pair<const ObjectKey, CObjectEntry*>& p : m_Objects)
delete p.second;
m_Objects.clear();
for (const std::pair<const CStrW, CObjectBase*>& p : m_ObjectBases)
delete p.second;
m_ObjectBases.clear();
m_ActorDefs.clear();
}
Status CObjectManager::ReloadChangedFile(const VfsPath& path)
{
// Mark old entries as outdated so we don't reload them from the cache
for (std::map<ObjectKey, CObjectEntry*>::iterator it = m_Objects.begin(); it != m_Objects.end(); ++it)
for (std::map<ObjectKey, std::unique_ptr<CObjectEntry>>::iterator it = m_Objects.begin(); it != m_Objects.end(); ++it)
if (it->second->m_Base->UsesFile(path))
it->second->m_Outdated = true;
const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual);
// Reload actors that use a changed object
for (std::map<CStrW, CObjectBase*>::iterator it = m_ObjectBases.begin(); it != m_ObjectBases.end(); ++it)
for (std::unordered_map<CStrW, std::unique_ptr<CActorDef>>::iterator it = m_ActorDefs.begin(); it != m_ActorDefs.end(); ++it)
{
if (it->second->UsesFile(path))
{
it->second->Reload();
if (!it->second->UsesFile(path))
continue;
it->second->Reload();
// Slightly ugly hack: The graphics system doesn't preserve enough information to regenerate the
// object with all correct variations, and we don't want to waste space storing it just for the
// rare occurrence of hotloading, so we'll tell the component (which does preserve the information)
// to do the reloading itself
const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual);
for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit)
static_cast<ICmpVisual*>(eit->second)->Hotload(it->first);
}
// Slightly ugly hack: The graphics system doesn't preserve enough information to regenerate the
// object with all correct variations, and we don't want to waste space storing it just for the
// rare occurrence of hotloading, so we'll tell the component (which does preserve the information)
// to do the reloading itself
for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit)
static_cast<ICmpVisual*>(eit->second)->Hotload(it->first);
}
return INFO::OK;
}
void CObjectManager::ActorQualityChanged()
{
int quality;
CFG_GET_VAL("max_actor_quality", quality);
if (quality == m_QualityLevel)
return;
m_QualityLevel = quality > 255 ? 255 : quality < 0 ? 0 : quality;
// No need to reload entries or actors, but we do need to reload all units.
const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual);
for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit)
static_cast<ICmpVisual*>(eit->second)->Hotload();
// Trigger an interpolate call - needed because the game is generally paused & models disappear otherwise.
m_Simulation.Interpolate(0.f, 0.f, 0.f);
}