Several resource management bug fixes

This commit is contained in:
Jack Andersen 2016-09-20 19:41:51 -10:00
parent d440c0ff43
commit c8d44d181b
7 changed files with 338 additions and 240 deletions

View File

@ -60,12 +60,18 @@ void ProjectResourceFactoryBase::ReadCatalog(const hecl::ProjectPath& catalogPat
const athena::io::YAMLNode* root = reader.getRootNode();
for (const auto& p : root->m_mapChildren)
{
/* Hash as lowercase since lookup is case-insensitive */
std::string pLower = p.first;
std::transform(pLower.cbegin(), pLower.cend(), pLower.begin(), tolower);
/* Avoid redundant filesystem access for re-caches */
if (m_catalogNameToTag.find(p.first) != m_catalogNameToTag.cend())
if (m_catalogNameToTag.find(pLower) != m_catalogNameToTag.cend())
continue;
athena::io::YAMLNode& node = *p.second;
hecl::ProjectPath path(m_proj->getProjectWorkingPath(), node.m_scalarString);
if (node.m_scalarString == "MP1/SamGunFx/PowerBeam.wpsm.yaml")
printf("");
if (node.m_type == YAML_SCALAR_NODE)
{
path = hecl::ProjectPath(m_proj->getProjectWorkingPath(), node.m_scalarString);
@ -84,7 +90,7 @@ void ProjectResourceFactoryBase::ReadCatalog(const hecl::ProjectPath& catalogPat
if (pathTag)
{
std::unique_lock<std::mutex> lk(m_backgroundIndexMutex);
m_catalogNameToTag[p.first] = pathTag;
m_catalogNameToTag[pLower] = pathTag;
WriteNameTag(nameWriter, pathTag, p.first);
#if 0
fprintf(stderr, "%s %s %08X\n",
@ -144,25 +150,29 @@ bool ProjectResourceFactoryBase::AddFileToIndex(const hecl::ProjectPath& path,
if (m_pathToTag.find(path.hash()) != m_pathToTag.cend())
return true;
/* Try as glob */
hecl::ProjectPath asGlob = path.getWithExtension(_S(".*"), true);
if (m_pathToTag.find(asGlob.hash()) != m_pathToTag.cend())
return true;
/* Classify intermediate into tag */
SObjectTag pathTag = BuildTagFromPath(path, m_backgroundBlender);
if (pathTag)
{
std::unique_lock<std::mutex> lk(m_backgroundIndexMutex);
m_tagToPath[pathTag] = path;
m_pathToTag[path.hash()] = pathTag;
WriteTag(cacheWriter, pathTag, path);
#if DUMP_CACHE_FILL
DumpCacheAdd(pathTag, path);
#endif
bool useGlob = false;
/* Special multi-resource intermediates */
if (pathTag.type == SBIG('ANCS'))
{
{
hecl::BlenderConnection& conn = m_backgroundBlender.getBlenderConnection();
if (!conn.openBlend(path) || conn.getBlendType() != hecl::BlenderConnection::BlendType::Actor)
return false;
/* Transform tag to glob */
pathTag = {SBIG('ANCS'), asGlob.hash().val32()};
useGlob = true;
hecl::BlenderConnection::DataStream ds = conn.beginData();
std::vector<std::string> armatureNames = ds.getArmatureNames();
std::vector<std::string> subtypeNames = ds.getSubtypeNames();
@ -171,7 +181,7 @@ bool ProjectResourceFactoryBase::AddFileToIndex(const hecl::ProjectPath& path,
for (const std::string& arm : armatureNames)
{
hecl::SystemStringView sysStr(arm);
hecl::ProjectPath subPath = path.ensureAuxInfo(sysStr.sys_str() + _S(".CINF"));
hecl::ProjectPath subPath = asGlob.ensureAuxInfo(sysStr.sys_str() + _S(".CINF"));
SObjectTag pathTag = BuildTagFromPath(subPath, m_backgroundBlender);
m_tagToPath[pathTag] = subPath;
m_pathToTag[subPath.hash()] = pathTag;
@ -184,7 +194,7 @@ bool ProjectResourceFactoryBase::AddFileToIndex(const hecl::ProjectPath& path,
for (const std::string& sub : subtypeNames)
{
hecl::SystemStringView sysStr(sub);
hecl::ProjectPath subPath = path.ensureAuxInfo(sysStr.sys_str() + _S(".CSKR"));
hecl::ProjectPath subPath = asGlob.ensureAuxInfo(sysStr.sys_str() + _S(".CSKR"));
SObjectTag pathTag = BuildTagFromPath(subPath, m_backgroundBlender);
m_tagToPath[pathTag] = subPath;
m_pathToTag[subPath.hash()] = pathTag;
@ -197,7 +207,7 @@ bool ProjectResourceFactoryBase::AddFileToIndex(const hecl::ProjectPath& path,
for (const std::string& act : actionNames)
{
hecl::SystemStringView sysStr(act);
hecl::ProjectPath subPath = path.ensureAuxInfo(sysStr.sys_str() + _S(".ANIM"));
hecl::ProjectPath subPath = asGlob.ensureAuxInfo(sysStr.sys_str() + _S(".ANIM"));
SObjectTag pathTag = BuildTagFromPath(subPath, m_backgroundBlender);
m_tagToPath[pathTag] = subPath;
m_pathToTag[subPath.hash()] = pathTag;
@ -207,6 +217,15 @@ bool ProjectResourceFactoryBase::AddFileToIndex(const hecl::ProjectPath& path,
#endif
}
}
/* Cache in-memory */
const hecl::ProjectPath& usePath = useGlob ? asGlob : path;
m_tagToPath[pathTag] = usePath;
m_pathToTag[usePath.hash()] = pathTag;
WriteTag(cacheWriter, pathTag, usePath);
#if DUMP_CACHE_FILL
DumpCacheAdd(pathTag, usePath);
#endif
}
return true;
@ -239,6 +258,7 @@ void ProjectResourceFactoryBase::BackgroundIndexRecursiveProc(const hecl::Projec
continue;
}
/* Index the regular file */
AddFileToIndex(path, cacheWriter);
}
@ -286,7 +306,7 @@ void ProjectResourceFactoryBase::BackgroundIndexProc()
path = path.ensureAuxInfo(sys.sys_str());
}
if (path.isFile())
if (path.isFileOrGlob())
{
SObjectTag pathTag(type, id);
m_tagToPath[pathTag] = path;
@ -317,8 +337,10 @@ void ProjectResourceFactoryBase::BackgroundIndexProc()
auto search = m_tagToPath.find(SObjectTag(FourCC(), uint32_t(id)));
if (search != m_tagToPath.cend())
{
m_catalogNameToTag[child.first] = search->first;
WriteNameTag(nameWriter, search->first, child.first);
std::string chLower = child.first;
std::transform(chLower.cbegin(), chLower.cend(), chLower.begin(), tolower);
m_catalogNameToTag[chLower] = search->first;
WriteNameTag(nameWriter, search->first, chLower);
}
}
}
@ -412,7 +434,7 @@ void ProjectResourceFactoryBase::AsyncTask::EnsurePath(const urde::SObjectTag& t
m_workingPath = path;
/* Ensure requested resource is on the filesystem */
if (!path.isFile())
if (!path.isFileOrGlob())
{
Log.report(logvisor::Error, _S("unable to find resource path '%s'"),
path.getRelativePath().c_str());
@ -523,7 +545,7 @@ ProjectResourceFactoryBase::PrepForReadSync(const SObjectTag& tag,
std::experimental::optional<athena::io::FileReader>& fr)
{
/* Ensure requested resource is on the filesystem */
if (!path.isFile())
if (!path.isFileOrGlob())
{
Log.report(logvisor::Error, _S("unable to find resource path '%s'"),
path.getAbsolutePath().c_str());
@ -724,8 +746,11 @@ bool ProjectResourceFactoryBase::CanBuild(const urde::SObjectTag& tag)
const urde::SObjectTag* ProjectResourceFactoryBase::GetResourceIdByName(const char* name) const
{
std::string lower = name;
std::transform(lower.cbegin(), lower.cend(), lower.begin(), tolower);
std::unique_lock<std::mutex> lk(const_cast<ProjectResourceFactoryBase*>(this)->m_backgroundIndexMutex);
auto search = m_catalogNameToTag.find(name);
auto search = m_catalogNameToTag.find(lower);
if (search == m_catalogNameToTag.end())
{
if (m_backgroundRunning)
@ -734,7 +759,7 @@ const urde::SObjectTag* ProjectResourceFactoryBase::GetResourceIdByName(const ch
{
lk.unlock();
lk.lock();
search = m_catalogNameToTag.find(name);
search = m_catalogNameToTag.find(lower);
if (search != m_catalogNameToTag.end())
break;
}

View File

@ -64,10 +64,16 @@ SObjectTag ProjectResourceFactoryMP1::BuildTagFromPath(const hecl::ProjectPath&
else if (hecl::StringUtils::EndsWith(path.getAuxInfo(), _S(".ANIM")))
return SObjectTag(SBIG('ANIM'), path.hash().val32());
if (hecl::IsPathBlend(path))
hecl::ProjectPath asBlend;
if (path.getPathType() == hecl::ProjectPath::Type::Glob)
asBlend = path.getWithExtension(_S(".blend"), true);
else
asBlend = path;
if (hecl::IsPathBlend(asBlend))
{
hecl::BlenderConnection& conn = btok.getBlenderConnection();
if (!conn.openBlend(path))
if (!conn.openBlend(asBlend))
return {};
switch (conn.getBlendType())
@ -113,49 +119,75 @@ SObjectTag ProjectResourceFactoryMP1::BuildTagFromPath(const hecl::ProjectPath&
yaml_parser_set_input_file(reader.getParser(), fp);
SObjectTag resTag;
if (reader.ClassTypeOperation([&](const char* className) -> bool {
if (!strcmp(className, "GPSM"))
{
resTag.type = SBIG('PART');
return true;
}
else if (!strcmp(className, "FONT"))
{
resTag.type = SBIG('FONT');
return true;
}
else if (!strcmp(className, "urde::DNAMP1::EVNT"))
{
resTag.type = SBIG('EVNT');
return true;
}
else if (!strcmp(className, "urde::DGRP"))
{
resTag.type = SBIG('DGRP');
return true;
}
else if (!strcmp(className, "urde::DNAMP1::STRG"))
{
resTag.type = SBIG('STRG');
return true;
}
else if (!strcmp(className, "DataSpec::DNAMP1::CTweakPlayerRes") ||
!strcmp(className, "DataSpec::DNAMP1::CTweakGunRes") ||
!strcmp(className, "DataSpec::DNAMP1::CTweakSlideShow") ||
!strcmp(className, "DataSpec::DNAMP1::CTweakPlayer") ||
!strcmp(className, "DataSpec::DNAMP1::CTweakCameraBob"))
{
resTag.type = SBIG('CTWK');
return true;
}
else if (!strcmp(className, "DataSpec::DNAMP1::HINT"))
{
resTag.type = SBIG('HINT');
return true;
}
if (reader.ClassTypeOperation([&](const char* className) -> bool
{
if (!strcmp(className, "GPSM"))
{
resTag.type = SBIG('PART');
return true;
}
if (!strcmp(className, "SWSH"))
{
resTag.type = SBIG('SWHC');
return true;
}
if (!strcmp(className, "ELSM"))
{
resTag.type = SBIG('ELSC');
return true;
}
if (!strcmp(className, "WPSM"))
{
resTag.type = SBIG('WPSC');
return true;
}
if (!strcmp(className, "CRSM"))
{
resTag.type = SBIG('CRSC');
return true;
}
if (!strcmp(className, "DPSM"))
{
resTag.type = SBIG('DPSC');
return true;
}
else if (!strcmp(className, "FONT"))
{
resTag.type = SBIG('FONT');
return true;
}
else if (!strcmp(className, "urde::DNAMP1::EVNT"))
{
resTag.type = SBIG('EVNT');
return true;
}
else if (!strcmp(className, "urde::DGRP"))
{
resTag.type = SBIG('DGRP');
return true;
}
else if (!strcmp(className, "urde::DNAMP1::STRG"))
{
resTag.type = SBIG('STRG');
return true;
}
else if (!strcmp(className, "DataSpec::DNAMP1::CTweakPlayerRes") ||
!strcmp(className, "DataSpec::DNAMP1::CTweakGunRes") ||
!strcmp(className, "DataSpec::DNAMP1::CTweakSlideShow") ||
!strcmp(className, "DataSpec::DNAMP1::CTweakPlayer") ||
!strcmp(className, "DataSpec::DNAMP1::CTweakCameraBob"))
{
resTag.type = SBIG('CTWK');
return true;
}
else if (!strcmp(className, "DataSpec::DNAMP1::HINT"))
{
resTag.type = SBIG('HINT');
return true;
}
return false;
}))
return false;
}))
{
resTag.id = path.hash().val32();
fclose(fp);

View File

@ -29,7 +29,7 @@ void ViewManager::BuildTestPART(urde::IObjectStore& objStore)
m_modelTest = objStore.GetObj("MP1/Shared/CMDL_B2B41738.blend");
CModel* ridley = m_modelTest.GetObj();
#if 1
SObjectTag samusCharSet = m_projManager.TagFromPath(_S("MP1/Shared/ANCS_77289A4A.blend"));
SObjectTag samusCharSet = m_projManager.TagFromPath(_S("MP1/Shared/ANCS_77289A4A.*"));
SObjectTag platModel = m_projManager.TagFromPath(_S("MP1/Shared/CMDL_6FA561D0.blend"));
SObjectTag bgModel = m_projManager.TagFromPath(_S("MP1/Shared/CMDL_BC34D54C.blend"));
CAnimRes samusAnimRes(samusCharSet.id, 2, zeus::CVector3f{2.f, 2.f, 2.f}, 1, true);

View File

@ -85,7 +85,7 @@ add_library(RuntimeCommon
IObj.hpp
IVParamObj.hpp
CTimeProvider.hpp CTimeProvider.cpp
CToken.hpp
CToken.hpp CToken.cpp
CFactoryMgr.hpp CFactoryMgr.cpp
CPakFile.hpp CPakFile.cpp
CStringExtras.hpp

191
Runtime/CToken.cpp Normal file
View File

@ -0,0 +1,191 @@
#include "CToken.hpp"
namespace urde
{
u16 CObjectReference::RemoveReference()
{
--x0_refCount;
if (x0_refCount == 0)
{
if (x10_object)
Unload();
if (IsLoading())
CancelLoad();
if (xC_objectStore)
xC_objectStore->ObjectUnreferenced(x4_objTag);
}
return x0_refCount;
}
CObjectReference::CObjectReference(IObjectStore& objStore, std::unique_ptr<IObj>&& obj,
const SObjectTag& objTag, CVParamTransfer buildParams)
: x4_objTag(objTag), xC_objectStore(&objStore),
x10_object(obj.release()), x14_params(buildParams) {}
CObjectReference::CObjectReference(std::unique_ptr<IObj>&& obj)
: x10_object(obj.release()) {}
void CObjectReference::Unlock()
{
--x2_lockCount;
if (x2_lockCount)
return;
if (x10_object && xC_objectStore)
Unload();
else if (IsLoading())
CancelLoad();
}
void CObjectReference::Lock()
{
++x2_lockCount;
if (!x10_object && !x3_loading)
{
IFactory& fac = xC_objectStore->GetFactory();
fac.BuildAsync(x4_objTag, x14_params, &x10_object, this);
x3_loading = true;
}
}
void CObjectReference::CancelLoad()
{
if (xC_objectStore && IsLoading())
{
xC_objectStore->GetFactory().CancelBuild(x4_objTag);
x3_loading = false;
}
}
void CObjectReference::Unload()
{
std::default_delete<IObj>()(x10_object);
x10_object = nullptr;
x3_loading = false;
}
IObj* CObjectReference::GetObject()
{
if (!x10_object)
{
IFactory& factory = xC_objectStore->GetFactory();
x10_object = factory.Build(x4_objTag, x14_params, this).release();
}
x3_loading = false;
return x10_object;
}
CObjectReference::~CObjectReference()
{
if (x10_object)
std::default_delete<IObj>()(x10_object);
else if (x3_loading)
xC_objectStore->GetFactory().CancelBuild(x4_objTag);
}
void CToken::RemoveRef()
{
if (x0_objRef && x0_objRef->RemoveReference() == 0)
{
std::default_delete<CObjectReference>()(x0_objRef);
x0_objRef = nullptr;
}
}
CToken::CToken(CObjectReference* obj)
{
x0_objRef = obj;
++x0_objRef->x0_refCount;
}
void CToken::Unlock()
{
if (x0_objRef && x4_lockHeld)
{
x0_objRef->Unlock();
x4_lockHeld = false;
}
}
void CToken::Lock()
{
if (x0_objRef && !x4_lockHeld)
{
x0_objRef->Lock();
x4_lockHeld = true;
}
}
bool CToken::IsLoaded() const
{
if (!x0_objRef)
return false;
return x0_objRef->IsLoaded();
}
IObj* CToken::GetObj()
{
if (!x0_objRef)
return nullptr;
Lock();
return x0_objRef->GetObject();
}
CToken& CToken::operator=(const CToken& other)
{
Unlock();
RemoveRef();
x0_objRef = other.x0_objRef;
if (x0_objRef)
{
++x0_objRef->x0_refCount;
if (other.x4_lockHeld)
Lock();
}
return *this;
}
CToken& CToken::operator=(CToken&& other)
{
Unlock();
RemoveRef();
x0_objRef = other.x0_objRef;
other.x0_objRef = nullptr;
x4_lockHeld = other.x4_lockHeld;
other.x4_lockHeld = false;
return *this;
}
CToken::CToken(const CToken& other)
: x0_objRef(other.x0_objRef)
{
if (x0_objRef)
++x0_objRef->x0_refCount;
}
CToken::CToken(CToken&& other)
: x0_objRef(other.x0_objRef), x4_lockHeld(other.x4_lockHeld)
{
other.x0_objRef = nullptr;
other.x4_lockHeld = false;
}
CToken::CToken(IObj* obj)
{
x0_objRef = new CObjectReference(std::unique_ptr<IObj>(obj));
++x0_objRef->x0_refCount;
Lock();
}
CToken::CToken(std::unique_ptr<IObj>&& obj)
{
x0_objRef = new CObjectReference(std::move(obj));
++x0_objRef->x0_refCount;
Lock();
}
const SObjectTag* CToken::GetObjectTag() const
{
if (!x0_objRef)
return nullptr;
return &x0_objRef->GetObjectTag();
}
CToken::~CToken()
{
if (x0_objRef)
{
if (x4_lockHeld)
x0_objRef->Unlock();
RemoveRef();
}
}
}

View File

@ -27,27 +27,11 @@ class CObjectReference
/** Mechanism by which CToken decrements 1st ref-count, indicating CToken invalidation or reset.
* Reaching 0 indicates the CToken should delete the CObjectReference */
u16 RemoveReference()
{
--x0_refCount;
if (x0_refCount == 0)
{
if (x10_object)
Unload();
if (IsLoading())
CancelLoad();
if (xC_objectStore)
xC_objectStore->ObjectUnreferenced(x4_objTag);
}
return x0_refCount;
}
u16 RemoveReference();
CObjectReference(IObjectStore& objStore, std::unique_ptr<IObj>&& obj,
const SObjectTag& objTag, CVParamTransfer buildParams)
: x4_objTag(objTag), xC_objectStore(&objStore),
x10_object(obj.release()), x14_params(buildParams) {}
CObjectReference(std::unique_ptr<IObj>&& obj)
: x10_object(obj.release()) {}
const SObjectTag& objTag, CVParamTransfer buildParams);
CObjectReference(std::unique_ptr<IObj>&& obj);
/** Indicates an asynchronous load transaction has been submitted and is not yet finished */
bool IsLoading() const {return x3_loading;}
@ -56,70 +40,26 @@ class CObjectReference
bool IsLoaded() const {return x10_object != nullptr;}
/** Decrements 2nd ref-count, performing unload or async-load-cancel if 0 reached */
void Unlock()
{
--x2_lockCount;
if (x2_lockCount)
return;
if (x10_object && xC_objectStore)
Unload();
else if (IsLoading())
CancelLoad();
}
void Unlock();
/** Increments 2nd ref-count, performing async-factory-load if needed */
void Lock()
{
++x2_lockCount;
if (!x10_object && !x3_loading)
{
IFactory& fac = xC_objectStore->GetFactory();
fac.BuildAsync(x4_objTag, x14_params, &x10_object, this);
x3_loading = true;
}
}
void Lock();
void CancelLoad()
{
if (xC_objectStore && IsLoading())
{
xC_objectStore->GetFactory().CancelBuild(x4_objTag);
x3_loading = false;
}
}
void CancelLoad();
/** Pointer-synchronized object-destructor, another building Lock cycle may be performed after */
void Unload()
{
std::default_delete<IObj>()(x10_object);
x10_object = nullptr;
x3_loading = false;
}
void Unload();
/** Synchronous object-fetch, guaranteed to return complete object on-demand, blocking build if not ready */
IObj* GetObject()
{
if (!x10_object)
{
IFactory& factory = xC_objectStore->GetFactory();
x10_object = factory.Build(x4_objTag, x14_params, this).release();
}
x3_loading = false;
return x10_object;
}
IObj* GetObject();
const SObjectTag& GetObjectTag() const
{
return x4_objTag;
}
public:
~CObjectReference()
{
if (x10_object)
std::default_delete<IObj>()(x10_object);
else if (x3_loading)
xC_objectStore->GetFactory().CancelBuild(x4_objTag);
}
~CObjectReference();
};
/** Counted meta-object, reference-counting against a shared CObjectReference
@ -132,121 +72,31 @@ class CToken
CObjectReference* x0_objRef = nullptr;
bool x4_lockHeld = false;
void RemoveRef()
{
if (x0_objRef && x0_objRef->RemoveReference() == 0)
{
std::default_delete<CObjectReference>()(x0_objRef);
x0_objRef = nullptr;
}
}
void RemoveRef();
CToken(CObjectReference* obj)
{
x0_objRef = obj;
++x0_objRef->x0_refCount;
}
CToken(CObjectReference* obj);
public:
/* Added to test for non-null state */
operator bool() const {return x0_objRef != nullptr;}
void Unlock()
{
if (x0_objRef && x4_lockHeld)
{
x0_objRef->Unlock();
x4_lockHeld = false;
}
}
void Lock()
{
if (x0_objRef && !x4_lockHeld)
{
x0_objRef->Lock();
x4_lockHeld = true;
}
}
void Unlock();
void Lock();
bool IsLocked() const {return x4_lockHeld;}
bool IsLoaded() const
{
if (!x0_objRef)
return false;
return x0_objRef->IsLoaded();
}
IObj* GetObj()
{
if (!x0_objRef)
return nullptr;
Lock();
return x0_objRef->GetObject();
}
bool IsLoaded() const;
IObj* GetObj();
const IObj* GetObj() const
{
return const_cast<CToken*>(this)->GetObj();
}
CToken& operator=(const CToken& other)
{
Unlock();
RemoveRef();
x0_objRef = other.x0_objRef;
if (x0_objRef)
{
++x0_objRef->x0_refCount;
if (other.x4_lockHeld)
Lock();
}
return *this;
}
CToken& operator=(CToken&& other)
{
Unlock();
RemoveRef();
x0_objRef = other.x0_objRef;
other.x0_objRef = nullptr;
x4_lockHeld = other.x4_lockHeld;
other.x4_lockHeld = false;
return *this;
}
CToken& operator=(const CToken& other);
CToken& operator=(CToken&& other);
CToken() = default;
CToken(const CToken& other)
: x0_objRef(other.x0_objRef)
{
if (x0_objRef)
++x0_objRef->x0_refCount;
}
CToken(CToken&& other)
: x0_objRef(other.x0_objRef), x4_lockHeld(other.x4_lockHeld)
{
other.x0_objRef = nullptr;
other.x4_lockHeld = false;
}
CToken(IObj* obj)
{
x0_objRef = new CObjectReference(std::unique_ptr<IObj>(obj));
++x0_objRef->x0_refCount;
Lock();
}
CToken(std::unique_ptr<IObj>&& obj)
{
x0_objRef = new CObjectReference(std::move(obj));
++x0_objRef->x0_refCount;
Lock();
}
const SObjectTag* GetObjectTag() const
{
if (!x0_objRef)
return nullptr;
return &x0_objRef->GetObjectTag();
}
~CToken()
{
if (x0_objRef)
{
if (x4_lockHeld)
x0_objRef->Unlock();
RemoveRef();
}
}
CToken(const CToken& other);
CToken(CToken&& other);
CToken(IObj* obj);
CToken(std::unique_ptr<IObj>&& obj);
const SObjectTag* GetObjectTag() const;
~CToken();
};
template <class T>

2
hecl

@ -1 +1 @@
Subproject commit 3a5bdade6f3e26b82eded9bee1bdcec2856fb0df
Subproject commit 1326eacf98fdc94d7fd692d248d54de9a09a2fb8