metaforce/Editor/ProjectResourceFactoryBase.cpp

468 lines
18 KiB
C++

#include "Editor/ProjectResourceFactoryBase.hpp"
#include "Runtime/IObj.hpp"
#include <logvisor/logvisor.hpp>
#undef min
#undef max
namespace metaforce {
static logvisor::Module Log("metaforce::ProjectResourceFactoryBase");
void ProjectResourceFactoryBase::BeginBackgroundIndex(hecl::Database::Project& proj,
const hecl::Database::DataSpecEntry& origSpec,
const hecl::Database::DataSpecEntry& pcSpec) {
CancelBackgroundIndex();
m_proj = &proj;
m_origSpec = &origSpec;
m_pcSpec = &pcSpec;
m_cookSpec = pcSpec.m_factory(proj, hecl::Database::DataSpecTool::Cook);
return static_cast<DataSpec::SpecBase&>(*m_cookSpec).beginBackgroundIndex();
}
bool ProjectResourceFactoryBase::SyncCook(const hecl::ProjectPath& working) {
Log.report(logvisor::Warning, FMT_STRING(_SYS_STR("sync-cooking {}")), working.getRelativePath());
return m_clientProc.syncCook(working, m_cookSpec.get(), hecl::blender::SharedBlenderToken, false, false);
}
CFactoryFnReturn ProjectResourceFactoryBase::BuildSync(const SObjectTag& tag, const hecl::ProjectPath& path,
const CVParamTransfer& paramXfer, CObjectReference* selfRef) {
/* Ensure cooked rep is on the filesystem */
std::optional<athena::io::FileReader> fr;
if (!PrepForReadSync(tag, path, fr))
return {};
/* All good, build resource */
if (m_factoryMgr.CanMakeMemory(tag)) {
u32 length = fr->length();
std::unique_ptr<u8[]> memBuf = fr->readUBytes(length);
CFactoryFnReturn ret = m_factoryMgr.MakeObjectFromMemory(tag, std::move(memBuf), length, false, paramXfer, selfRef);
Log.report(logvisor::Info, FMT_STRING("sync-built {}"), tag);
return ret;
}
CFactoryFnReturn ret = m_factoryMgr.MakeObject(tag, *fr, paramXfer, selfRef);
Log.report(logvisor::Info, FMT_STRING("sync-built {}"), tag);
return ret;
}
void ProjectResourceFactoryBase::AsyncTask::EnsurePath(const metaforce::SObjectTag& tag, const hecl::ProjectPath& path) {
if (!m_workingPath) {
m_workingPath = path;
/* Ensure requested resource is on the filesystem */
if (!path.isFileOrGlob()) {
Log.report(logvisor::Error, FMT_STRING(_SYS_STR("unable to find resource path '{}'")), path.getRelativePath());
m_failed = true;
return;
}
/* Cached resolve (try PC first, then original) */
m_cookedPath = path.getCookedPath(*m_parent.m_pcSpec);
if (!m_cookedPath.isFile())
m_cookedPath = path.getCookedPath(*m_parent.m_origSpec);
if (!m_cookedPath.isFile() || m_cookedPath.getModtime() < path.getModtime()) {
/* Last chance type validation */
metaforce::SObjectTag verifyTag = m_parent.TagFromPath(path);
if (verifyTag.type != tag.type) {
Log.report(logvisor::Error, FMT_STRING(_SYS_STR("{}: expected type '{}', found '{}'")), path.getRelativePath(),
tag.type, verifyTag.type);
m_failed = true;
return;
}
/* Get cooked representation path */
m_cookedPath = m_parent.GetCookedPath(path, true);
/* Perform mod-time comparison */
if (!m_cookedPath.isFile() || m_cookedPath.getModtime() < path.getModtime()) {
/* Start a background cook here */
m_cookTransaction = m_parent.m_clientProc.addCookTransaction(path, false, false, m_parent.m_cookSpec.get());
return;
}
}
CookComplete();
}
}
void ProjectResourceFactoryBase::AsyncTask::CookComplete() {
/* Ensure cooked rep is on the filesystem */
athena::io::FileReader fr(m_cookedPath.getAbsolutePath(), 32 * 1024, false);
if (fr.hasError()) {
Log.report(logvisor::Error, FMT_STRING(_SYS_STR("unable to open cooked resource path '{}'")), m_cookedPath.getAbsolutePath());
m_failed = true;
return;
}
/* Ready for buffer transaction at this point */
u32 availSz = std::max(0, s32(fr.length()) - s32(x14_resOffset));
x14_resSize = std::min(x14_resSize, availSz);
if (xc_targetDataRawPtr) {
m_bufTransaction =
m_parent.m_clientProc.addBufferTransaction(m_cookedPath, xc_targetDataRawPtr, x14_resSize, x14_resOffset);
} else if (xc_targetDataPtr || xc_targetObjPtr) {
x10_loadBuffer.reset(new u8[x14_resSize]);
m_bufTransaction =
m_parent.m_clientProc.addBufferTransaction(m_cookedPath, x10_loadBuffer.get(), x14_resSize, x14_resOffset);
} else {
/* Skip buffer transaction if no target pointers set */
m_complete = true;
}
}
bool ProjectResourceFactoryBase::AsyncTask::AsyncPump() {
if (m_failed || m_complete)
return true;
if (m_bufTransaction) {
if (m_bufTransaction->m_complete) {
m_complete = true;
return true;
}
} else if (m_cookTransaction) {
if (m_cookTransaction->m_complete)
CookComplete();
}
return m_failed;
}
void ProjectResourceFactoryBase::AsyncTask::WaitUntilComplete() {
using ItType = std::unordered_map<SObjectTag, std::list<std::shared_ptr<AsyncTask>>::iterator>::iterator;
ItType search = m_parent.m_asyncLoadMap.find(x0_tag);
if (search == m_parent.m_asyncLoadMap.end())
return;
for (ItType tmp = search; !m_parent.AsyncPumpTask(tmp); tmp = search) {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
}
using AsyncTask = ProjectResourceFactoryBase::AsyncTask;
std::shared_ptr<AsyncTask> ProjectResourceFactoryBase::_AddTask(const std::shared_ptr<AsyncTask>& ptr) {
m_asyncLoadMap.insert({ptr->x0_tag, m_asyncLoadList.insert(m_asyncLoadList.end(), ptr)});
return ptr;
}
std::list<std::shared_ptr<AsyncTask>>::iterator
ProjectResourceFactoryBase::_RemoveTask(std::list<std::shared_ptr<AsyncTask>>::iterator it) {
m_asyncLoadMap.erase((*it)->x0_tag);
return m_asyncLoadList.erase(it);
}
std::unordered_map<SObjectTag, std::list<std::shared_ptr<AsyncTask>>::iterator>::iterator
ProjectResourceFactoryBase::_RemoveTask(
std::unordered_map<SObjectTag, std::list<std::shared_ptr<AsyncTask>>::iterator>::iterator it) {
if (it != m_asyncLoadMap.end()) {
m_asyncLoadList.erase(it->second);
return m_asyncLoadMap.erase(it);
}
return it;
};
std::unordered_map<SObjectTag, std::list<std::shared_ptr<AsyncTask>>::iterator>::iterator
ProjectResourceFactoryBase::_RemoveTask(const SObjectTag& tag) {
return _RemoveTask(m_asyncLoadMap.find(tag));
};
bool ProjectResourceFactoryBase::PrepForReadSync(const SObjectTag& tag, const hecl::ProjectPath& path,
std::optional<athena::io::FileReader>& fr) {
/* Ensure requested resource is on the filesystem */
if (!path.isFileOrGlob()) {
Log.report(logvisor::Error, FMT_STRING(_SYS_STR("unable to find resource path '{}'")), path.getAbsolutePath());
return false;
}
/* Cached resolve (try PC first, then original) */
hecl::ProjectPath cooked = path.getCookedPath(*m_pcSpec);
if (!cooked.isFile())
cooked = path.getCookedPath(*m_origSpec);
if (!cooked.isFile() || cooked.getModtime() < path.getModtime()) {
/* Last chance type validation */
metaforce::SObjectTag verifyTag = TagFromPath(path);
if (verifyTag.type != tag.type) {
Log.report(logvisor::Error, FMT_STRING(_SYS_STR("{}: expected type '{}', found '{}'")), path.getRelativePath(),
tag.type, verifyTag.type);
return false;
}
/* Get cooked representation path */
cooked = GetCookedPath(path, true);
/* Perform mod-time comparison */
if (!cooked.isFile() || cooked.getModtime() < path.getModtime()) {
/* Do a blocking cook here */
if (!SyncCook(path)) {
Log.report(logvisor::Error, FMT_STRING(_SYS_STR("unable to cook resource path '{}'")), path.getAbsolutePath());
return false;
}
}
}
/* Ensure cooked rep is on the filesystem */
fr.emplace(cooked.getAbsolutePath(), 32 * 1024, false);
if (fr->hasError()) {
Log.report(logvisor::Error, FMT_STRING(_SYS_STR("unable to open cooked resource path '{}'")), cooked.getAbsolutePath());
return false;
}
return true;
}
std::unique_ptr<metaforce::IObj> ProjectResourceFactoryBase::Build(const metaforce::SObjectTag& tag,
const metaforce::CVParamTransfer& paramXfer,
CObjectReference* selfRef) {
if (!tag.id.IsValid())
Log.report(logvisor::Fatal, FMT_STRING("attempted to access null id on type '{}'"), tag.type);
const hecl::ProjectPath* resPath = nullptr;
if (!WaitForTagReady(tag, resPath))
return {};
auto asyncSearch = m_asyncLoadMap.find(tag);
if (asyncSearch != m_asyncLoadMap.end()) {
/* Async spinloop */
AsyncTask& task = **asyncSearch->second;
task.EnsurePath(task.x0_tag, *resPath);
/* Pump load pipeline (cooking if needed) */
while (!task.AsyncPump()) {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
if (task.m_complete && task.x10_loadBuffer) {
/* Load complete, build resource */
std::unique_ptr<IObj> newObj;
if (m_factoryMgr.CanMakeMemory(task.x0_tag)) {
newObj = m_factoryMgr.MakeObjectFromMemory(tag, std::move(task.x10_loadBuffer), task.x14_resSize, false,
task.x18_cvXfer, selfRef);
} else {
athena::io::MemoryReader mr(task.x10_loadBuffer.get(), task.x14_resSize);
newObj = m_factoryMgr.MakeObject(task.x0_tag, mr, task.x18_cvXfer, selfRef);
}
//*task.xc_targetObjPtr = newObj.get();
Log.report(logvisor::Warning, FMT_STRING("spin-built {}"), task.x0_tag);
_RemoveTask(asyncSearch);
return newObj;
} else if (task.m_complete) {
Log.report(logvisor::Error, FMT_STRING("unable to spin-build {}; Resource requested as cook-only"), task.x0_tag);
} else {
Log.report(logvisor::Error, FMT_STRING("unable to spin-build {}"), task.x0_tag);
}
_RemoveTask(asyncSearch);
return {};
}
/* Fall-back to sync build */
return BuildSync(tag, *resPath, paramXfer, selfRef);
}
std::shared_ptr<AsyncTask> ProjectResourceFactoryBase::BuildAsyncInternal(const metaforce::SObjectTag& tag,
const metaforce::CVParamTransfer& paramXfer,
std::unique_ptr<metaforce::IObj>* objOut,
CObjectReference* selfRef) {
if (m_asyncLoadMap.find(tag) != m_asyncLoadMap.end())
return {};
return _AddTask(std::make_unique<AsyncTask>(*this, tag, objOut, paramXfer, selfRef));
}
void ProjectResourceFactoryBase::BuildAsync(const metaforce::SObjectTag& tag, const metaforce::CVParamTransfer& paramXfer,
std::unique_ptr<metaforce::IObj>* objOut, CObjectReference* selfRef) {
if (!tag.id.IsValid())
Log.report(logvisor::Fatal, FMT_STRING("attempted to access null id on type '{}'"), tag.type);
BuildAsyncInternal(tag, paramXfer, objOut, selfRef);
}
u32 ProjectResourceFactoryBase::ResourceSize(const SObjectTag& tag) {
if (!tag.id.IsValid())
Log.report(logvisor::Fatal, FMT_STRING("attempted to access null id on type '{}'"), tag.type);
/* Ensure resource at requested path is indexed and not cooking */
const hecl::ProjectPath* resPath = nullptr;
if (!WaitForTagReady(tag, resPath))
return {};
/* Ensure cooked rep is on the filesystem */
std::optional<athena::io::FileReader> fr;
if (!PrepForReadSync(tag, *resPath, fr))
return {};
return fr->length();
}
std::shared_ptr<metaforce::IDvdRequest> ProjectResourceFactoryBase::LoadResourceAsync(const metaforce::SObjectTag& tag,
void* target) {
if (!tag.id.IsValid())
Log.report(logvisor::Fatal, FMT_STRING("attempted to access null id"));
if (m_asyncLoadMap.find(tag) != m_asyncLoadMap.end())
return {};
return std::static_pointer_cast<metaforce::IDvdRequest>(
_AddTask(std::make_shared<AsyncTask>(*this, tag, reinterpret_cast<u8*>(target))));
}
std::shared_ptr<metaforce::IDvdRequest> ProjectResourceFactoryBase::LoadResourcePartAsync(const metaforce::SObjectTag& tag,
u32 off, u32 size, void* target) {
if (!tag.id.IsValid())
Log.report(logvisor::Fatal, FMT_STRING("attempted to access null id"));
if (m_asyncLoadMap.find(tag) != m_asyncLoadMap.end())
return {};
return std::static_pointer_cast<metaforce::IDvdRequest>(
_AddTask(std::make_shared<AsyncTask>(*this, tag, reinterpret_cast<u8*>(target), size, off)));
}
std::unique_ptr<u8[]> ProjectResourceFactoryBase::LoadResourceSync(const metaforce::SObjectTag& tag) {
if (!tag.id.IsValid())
Log.report(logvisor::Fatal, FMT_STRING("attempted to access null id"));
/* Ensure resource at requested path is indexed and not cooking */
const hecl::ProjectPath* resPath = nullptr;
if (!WaitForTagReady(tag, resPath))
return {};
/* Ensure cooked rep is on the filesystem */
std::optional<athena::io::FileReader> fr;
if (!PrepForReadSync(tag, *resPath, fr))
return {};
return fr->readUBytes(fr->length());
}
std::unique_ptr<u8[]> ProjectResourceFactoryBase::LoadNewResourcePartSync(const metaforce::SObjectTag& tag, u32 off,
u32 size) {
if (!tag.id.IsValid())
Log.report(logvisor::Fatal, FMT_STRING("attempted to access null id"));
/* Ensure resource at requested path is indexed and not cooking */
const hecl::ProjectPath* resPath = nullptr;
if (!WaitForTagReady(tag, resPath))
return {};
/* Ensure cooked rep is on the filesystem */
std::optional<athena::io::FileReader> fr;
if (!PrepForReadSync(tag, *resPath, fr))
return {};
s32 sz = std::min(s32(size), std::max(0, s32(fr->length()) - s32(off)));
fr->seek(off, athena::SeekOrigin::Begin);
return fr->readUBytes(sz);
}
std::shared_ptr<AsyncTask> ProjectResourceFactoryBase::CookResourceAsync(const metaforce::SObjectTag& tag) {
if (!tag.id.IsValid())
Log.report(logvisor::Fatal, FMT_STRING("attempted to access null id"));
if (m_asyncLoadMap.find(tag) != m_asyncLoadMap.end())
return {};
return _AddTask(std::make_shared<AsyncTask>(*this, tag));
}
void ProjectResourceFactoryBase::CancelBuild(const metaforce::SObjectTag& tag) { _RemoveTask(tag); }
bool ProjectResourceFactoryBase::CanBuild(const metaforce::SObjectTag& tag) {
if (!tag.id.IsValid())
Log.report(logvisor::Fatal, FMT_STRING("attempted to access null id"));
const hecl::ProjectPath* resPath = nullptr;
if (!WaitForTagReady(tag, resPath))
return false;
if (resPath->isFile())
return true;
return false;
}
const metaforce::SObjectTag* ProjectResourceFactoryBase::GetResourceIdByName(std::string_view name) const {
return static_cast<DataSpec::SpecBase&>(*m_cookSpec).getResourceIdByName(name);
}
FourCC ProjectResourceFactoryBase::GetResourceTypeById(CAssetId id) const {
return static_cast<DataSpec::SpecBase&>(*m_cookSpec).getResourceTypeById(id);
}
void ProjectResourceFactoryBase::EnumerateResources(const std::function<bool(const SObjectTag&)>& lambda) const {
return static_cast<DataSpec::SpecBase&>(*m_cookSpec).enumerateResources(lambda);
}
void ProjectResourceFactoryBase::EnumerateNamedResources(
const std::function<bool(std::string_view, const SObjectTag&)>& lambda) const {
return static_cast<DataSpec::SpecBase&>(*m_cookSpec).enumerateNamedResources(lambda);
}
template <typename ItType>
bool ProjectResourceFactoryBase::AsyncPumpTask(ItType& it) {
/* Ensure requested resource is in the index */
AsyncTask& task = _GetAsyncTask(it);
hecl::ProjectPath path = static_cast<DataSpec::SpecBase&>(*m_cookSpec).pathFromTag(task.x0_tag);
if (!path) {
if (!static_cast<DataSpec::SpecBase&>(*m_cookSpec).backgroundIndexRunning()) {
Log.report(logvisor::Error, FMT_STRING(_SYS_STR("unable to find async load resource ({})")), task.x0_tag);
it = _RemoveTask(it);
}
return true;
}
task.EnsurePath(task.x0_tag, path);
/* Pump load pipeline (cooking if needed) */
if (task.AsyncPump()) {
if (task.m_complete) {
/* Load complete, build resource */
if (task.xc_targetObjPtr) {
/* Factory build */
std::unique_ptr<IObj> newObj;
if (m_factoryMgr.CanMakeMemory(task.x0_tag)) {
newObj = m_factoryMgr.MakeObjectFromMemory(task.x0_tag, std::move(task.x10_loadBuffer), task.x14_resSize,
false, task.x18_cvXfer, task.m_selfRef);
} else {
athena::io::MemoryReader mr(task.x10_loadBuffer.get(), task.x14_resSize);
newObj = m_factoryMgr.MakeObject(task.x0_tag, mr, task.x18_cvXfer, task.m_selfRef);
}
*task.xc_targetObjPtr = std::move(newObj);
Log.report(logvisor::Info, FMT_STRING("async-built {}"), task.x0_tag);
} else if (task.xc_targetDataPtr) {
/* Buffer only */
*task.xc_targetDataPtr = std::move(task.x10_loadBuffer);
Log.report(logvisor::Info, FMT_STRING("async-loaded {}"), task.x0_tag);
} else if (task.xc_targetDataRawPtr) {
/* Buffer only raw */
Log.report(logvisor::Info, FMT_STRING("async-loaded {}"), task.x0_tag);
}
}
it = _RemoveTask(it);
return true;
}
++it;
return false;
}
template bool ProjectResourceFactoryBase::AsyncPumpTask<std::list<std::shared_ptr<AsyncTask>>::iterator>(
std::list<std::shared_ptr<AsyncTask>>::iterator& it);
template bool ProjectResourceFactoryBase::AsyncPumpTask<
std::unordered_map<SObjectTag, std::list<std::shared_ptr<AsyncTask>>::iterator>::iterator>(
std::unordered_map<SObjectTag, std::list<std::shared_ptr<AsyncTask>>::iterator>::iterator& it);
void ProjectResourceFactoryBase::AsyncIdle() {
/* Consume completed transactions, they will be processed this cycle at the latest */
std::list<std::shared_ptr<hecl::ClientProcess::Transaction>> completed;
m_clientProc.swapCompletedQueue(completed);
/* Begin self-profiling loop */
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
for (auto it = m_asyncLoadList.begin(); it != m_asyncLoadList.end();) {
/* Allow 8 milliseconds (roughly 1/2 frame-time) for each async build cycle */
std::chrono::steady_clock::time_point resStart = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::milliseconds>(resStart - start).count() > 8)
break;
AsyncPumpTask(it);
}
}
} // namespace metaforce