mirror of
https://github.com/open-goal/jak-project
synced 2026-05-30 00:47:20 -04:00
576 lines
18 KiB
C++
576 lines
18 KiB
C++
#include "Loader.h"
|
|
|
|
#include "common/global_profiler/GlobalProfiler.h"
|
|
#include "common/util/FileUtil.h"
|
|
#include "common/util/Timer.h"
|
|
#include "common/util/compress.h"
|
|
|
|
#include "game/graphics/opengl_renderer/loader/LoaderStages.h"
|
|
|
|
#include "third-party/imgui/imgui.h"
|
|
|
|
Loader::Loader(const fs::path& base_path, int max_levels)
|
|
: m_base_path(base_path), m_max_levels(max_levels) {
|
|
m_loader_thread = std::thread(&Loader::loader_thread, this);
|
|
m_loader_stages = make_loader_stages();
|
|
}
|
|
|
|
Loader::~Loader() {
|
|
{
|
|
std::lock_guard<std::mutex> lk(m_loader_mutex);
|
|
m_want_shutdown = true;
|
|
m_loader_cv.notify_all();
|
|
}
|
|
m_loader_thread.join();
|
|
}
|
|
|
|
/*!
|
|
* Try to get a loaded level by name. It may fail and return nullptr.
|
|
* Getting a level will reset the counter for the level and prevent it from being kicked out
|
|
* for a little while.
|
|
*
|
|
* This is safe to call from the graphics thread
|
|
*/
|
|
const LevelData* Loader::get_tfrag3_level(const std::string& level_name) {
|
|
std::unique_lock<std::mutex> lk(m_loader_mutex);
|
|
const auto& existing = m_loaded_tfrag3_levels.find(level_name);
|
|
if (existing == m_loaded_tfrag3_levels.end()) {
|
|
return nullptr;
|
|
} else {
|
|
existing->second->frames_since_last_used = 0;
|
|
return existing->second.get();
|
|
}
|
|
}
|
|
|
|
void Loader::debug_print_loaded_levels() {
|
|
std::unique_lock<std::mutex> lk(m_loader_mutex);
|
|
for (const auto& [name, _] : m_loaded_tfrag3_levels) {
|
|
fmt::print("{}\n", name);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* The game calls this to give the loader a hint on which levels we want.
|
|
* If the loader is not busy, it will begin loading the level.
|
|
* This should be called on every frame.
|
|
*/
|
|
void Loader::set_want_levels(const std::vector<std::string>& levels) {
|
|
std::unique_lock<std::mutex> lk(m_loader_mutex);
|
|
m_desired_levels = levels;
|
|
if (!m_level_to_load.empty()) {
|
|
// can't do anything, we're loading a level right now
|
|
return;
|
|
}
|
|
|
|
if (!m_initializing_tfrag3_levels.empty()) {
|
|
// can't do anything, we're initializing a level right now
|
|
return;
|
|
}
|
|
|
|
// loader isn't busy, try to load one of the requested levels.
|
|
for (auto& lev : levels) {
|
|
auto it = m_loaded_tfrag3_levels.find(lev);
|
|
if (it == m_loaded_tfrag3_levels.end()) {
|
|
// we haven't loaded it yet. Request this level to load and wake up the thread.
|
|
m_level_to_load = lev;
|
|
lk.unlock();
|
|
m_loader_cv.notify_all();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* The game calls this to tell the loader that we absolutely want these levels active.
|
|
* This will NOT trigger a load!
|
|
*/
|
|
void Loader::set_active_levels(const std::vector<std::string>& levels) {
|
|
std::unique_lock<std::mutex> lk(m_loader_mutex);
|
|
m_active_levels = levels;
|
|
}
|
|
|
|
/*!
|
|
* Get all levels that are in memory and used very recently.
|
|
*/
|
|
std::vector<LevelData*> Loader::get_in_use_levels() {
|
|
std::vector<LevelData*> result;
|
|
std::unique_lock<std::mutex> lk(m_loader_mutex);
|
|
|
|
for (auto& [name, lev] : m_loaded_tfrag3_levels) {
|
|
if (lev->frames_since_last_used < 5) {
|
|
result.push_back(lev.get());
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void Loader::draw_debug_window() {
|
|
ImGui::Begin("Loader");
|
|
std::unique_lock<std::mutex> lk(m_loader_mutex);
|
|
ImVec4 blue(0.3, 0.3, 0.8, 1.0);
|
|
ImVec4 red(0.8, 0.3, 0.3, 1.0);
|
|
ImVec4 green(0.3, 0.8, 0.3, 1.0);
|
|
|
|
if (!m_desired_levels.empty()) {
|
|
ImGui::Text("desired levels");
|
|
for (auto& lev : m_desired_levels) {
|
|
auto lev_color = red;
|
|
if (m_initializing_tfrag3_levels.find(lev) != m_initializing_tfrag3_levels.end()) {
|
|
lev_color = blue;
|
|
}
|
|
if (m_loaded_tfrag3_levels.find(lev) != m_loaded_tfrag3_levels.end()) {
|
|
lev_color = green;
|
|
}
|
|
ImGui::TextColored(lev_color, "%s", lev.c_str());
|
|
ImGui::SameLine();
|
|
}
|
|
ImGui::NewLine();
|
|
ImGui::Separator();
|
|
}
|
|
|
|
if (!m_initializing_tfrag3_levels.empty()) {
|
|
ImGui::Text("init levels");
|
|
for (auto& lev : m_initializing_tfrag3_levels) {
|
|
ImGui::TextColored(blue, "%s", lev.first.c_str());
|
|
ImGui::SameLine();
|
|
}
|
|
ImGui::NewLine();
|
|
ImGui::Separator();
|
|
}
|
|
|
|
if (!m_loaded_tfrag3_levels.empty()) {
|
|
ImGui::Text("loaded levels");
|
|
for (auto& lev : m_loaded_tfrag3_levels) {
|
|
auto lev_color = green;
|
|
if (lev.second->frames_since_last_used > 0) {
|
|
lev_color = blue;
|
|
}
|
|
if (lev.second->frames_since_last_used > 180) {
|
|
lev_color = red;
|
|
}
|
|
ImGui::TextColored(lev_color, "%20s : %3d", lev.first.c_str(),
|
|
lev.second->frames_since_last_used);
|
|
ImGui::Text(" %d textures", (int)lev.second->textures.size());
|
|
ImGui::Text(" %d merc", (int)lev.second->merc_model_lookup.size());
|
|
ImGui::Text(" %d shadow", (int)lev.second->shadow_model_lookup.size());
|
|
}
|
|
ImGui::NewLine();
|
|
ImGui::Separator();
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
/*!
|
|
* Loader function that runs in a completely separate thread.
|
|
* This is used for file I/O and unpacking.
|
|
*/
|
|
void Loader::loader_thread() {
|
|
try {
|
|
while (!m_want_shutdown) {
|
|
prof().root_event();
|
|
std::unique_lock<std::mutex> lk(m_loader_mutex);
|
|
|
|
// this will keep us asleep until we've got a level to load.
|
|
m_loader_cv.wait(lk, [&] { return !m_level_to_load.empty() || m_want_shutdown; });
|
|
if (m_want_shutdown) {
|
|
return;
|
|
}
|
|
std::string lev = m_level_to_load;
|
|
// don't hold the lock while reading the file.
|
|
lk.unlock();
|
|
|
|
// simulate slower hard drive (so that the loader thread can lose to the game loads)
|
|
// std::this_thread::sleep_for(std::chrono::milliseconds(1500));
|
|
|
|
// load the fr3 file
|
|
prof().begin_event("read-file");
|
|
Timer disk_timer;
|
|
auto data = file_util::read_binary_file(m_base_path / fmt::format("{}.fr3", lev));
|
|
double disk_load_time = disk_timer.getSeconds();
|
|
prof().end_event();
|
|
|
|
// the FR3 files are compressed
|
|
prof().begin_event("decompress-file");
|
|
Timer decomp_timer;
|
|
auto decomp_data = compression::decompress_zstd(data.data(), data.size());
|
|
double decomp_time = decomp_timer.getSeconds();
|
|
prof().end_event();
|
|
|
|
// Read back into the tfrag3::Level structure
|
|
prof().begin_event("deserialize");
|
|
Timer import_timer;
|
|
auto result = std::make_unique<tfrag3::Level>();
|
|
Serializer ser(decomp_data.data(), decomp_data.size());
|
|
result->serialize(ser);
|
|
double import_time = import_timer.getSeconds();
|
|
prof().end_event();
|
|
|
|
// and finally "unpack", which creates the vertex data we'll upload to the GPU
|
|
|
|
Timer unpack_timer;
|
|
{
|
|
auto p = scoped_prof("tie-unpack");
|
|
for (auto& tie_tree : result->tie_trees) {
|
|
for (auto& tree : tie_tree) {
|
|
tree.unpack();
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
auto p = scoped_prof("tfrag-unpack");
|
|
for (auto& t_tree : result->tfrag_trees) {
|
|
for (auto& tree : t_tree) {
|
|
tree.unpack();
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
auto p = scoped_prof("shrub-unpack");
|
|
for (auto& shrub_tree : result->shrub_trees) {
|
|
shrub_tree.unpack();
|
|
}
|
|
}
|
|
|
|
fmt::print(
|
|
"------------> Load from file: {:.3f}s, import {:.3f}s, decomp {:.3f}s unpack {:.3f}s\n",
|
|
disk_load_time, import_time, decomp_time, unpack_timer.getSeconds());
|
|
|
|
// grab the lock again
|
|
lk.lock();
|
|
// move this level to "initializing" state.
|
|
m_initializing_tfrag3_levels[lev] = std::make_unique<LevelData>(); // reset load state
|
|
m_initializing_tfrag3_levels[lev]->level = std::move(result);
|
|
m_level_to_load = "";
|
|
m_file_load_done_cv.notify_all();
|
|
}
|
|
} catch (std::exception& e) {
|
|
ASSERT_MSG(false, fmt::format("Exception {} encountered in loader_thread", e.what()));
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Load a "common" FR3 file that has non-level textures.
|
|
* This should be called during initialization, before any threaded loading goes on.
|
|
*/
|
|
const tfrag3::Level& Loader::load_common(TexturePool& tex_pool, const std::string& name) {
|
|
auto data = file_util::read_binary_file(m_base_path / fmt::format("{}.fr3", name));
|
|
|
|
auto decomp_data = compression::decompress_zstd(data.data(), data.size());
|
|
Serializer ser(decomp_data.data(), decomp_data.size());
|
|
m_common_level.level = std::make_unique<tfrag3::Level>();
|
|
m_common_level.level->serialize(ser);
|
|
for (auto& tex : m_common_level.level->textures) {
|
|
m_common_level.textures.push_back(add_texture(tex_pool, tex, true));
|
|
}
|
|
|
|
Timer tim;
|
|
MercLoaderStage mls;
|
|
ShadowLoaderStage sls;
|
|
LoaderInput input;
|
|
input.tex_pool = &tex_pool;
|
|
input.mercs = &m_all_merc_models;
|
|
input.shadows = &m_all_shadow_models;
|
|
input.lev_data = &m_common_level;
|
|
bool done = false;
|
|
while (!done) {
|
|
done = mls.run(tim, input);
|
|
}
|
|
done = false;
|
|
while (!done) {
|
|
done = sls.run(tim, input);
|
|
}
|
|
return *m_common_level.level;
|
|
}
|
|
|
|
bool Loader::upload_textures(Timer& timer, LevelData& data, TexturePool& texture_pool) {
|
|
// try to move level from initializing to initialized:
|
|
|
|
auto evt = scoped_prof("upload-textures");
|
|
constexpr int MAX_TEX_BYTES_PER_FRAME = 1024 * 128;
|
|
|
|
int bytes_this_run = 0;
|
|
int tex_this_run = 0;
|
|
if (data.textures.size() < data.level->textures.size()) {
|
|
std::unique_lock<std::mutex> tpool_lock(texture_pool.mutex());
|
|
while (data.textures.size() < data.level->textures.size()) {
|
|
auto& tex = data.level->textures[data.textures.size()];
|
|
data.textures.push_back(add_texture(texture_pool, tex, false));
|
|
bytes_this_run += tex.w * tex.h * 4;
|
|
tex_this_run++;
|
|
if (tex_this_run > 20) {
|
|
break;
|
|
}
|
|
if (bytes_this_run > MAX_TEX_BYTES_PER_FRAME || timer.getMs() > SHARED_TEXTURE_LOAD_BUDGET) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return data.textures.size() == data.level->textures.size();
|
|
}
|
|
|
|
void Loader::update_blocking(TexturePool& tex_pool) {
|
|
fmt::print("NOTE: coming out of blackout on next frame, doing all loads now...\n");
|
|
|
|
bool missing_levels = true;
|
|
while (missing_levels) {
|
|
bool needs_run = true;
|
|
|
|
while (needs_run) {
|
|
needs_run = false;
|
|
{
|
|
std::unique_lock<std::mutex> lk(m_loader_mutex);
|
|
if (!m_level_to_load.empty()) {
|
|
m_file_load_done_cv.wait(lk, [&]() { return m_level_to_load.empty(); });
|
|
}
|
|
}
|
|
}
|
|
|
|
needs_run = true;
|
|
|
|
while (needs_run) {
|
|
needs_run = false;
|
|
{
|
|
std::unique_lock<std::mutex> lk(m_loader_mutex);
|
|
if (!m_initializing_tfrag3_levels.empty()) {
|
|
needs_run = true;
|
|
}
|
|
}
|
|
|
|
if (needs_run) {
|
|
update(tex_pool);
|
|
}
|
|
}
|
|
|
|
{
|
|
std::unique_lock<std::mutex> lk(m_loader_mutex);
|
|
missing_levels = false;
|
|
for (auto& des : m_desired_levels) {
|
|
if (m_loaded_tfrag3_levels.find(des) == m_loaded_tfrag3_levels.end()) {
|
|
fmt::print("blackout loader doing additional level {}...\n", des);
|
|
missing_levels = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (missing_levels) {
|
|
set_want_levels(m_desired_levels);
|
|
}
|
|
}
|
|
|
|
fmt::print("Blackout loads done. Current status:");
|
|
std::unique_lock<std::mutex> lk(m_loader_mutex);
|
|
for (auto& ld : m_loaded_tfrag3_levels) {
|
|
fmt::print(" {} is loaded.\n", ld.first);
|
|
}
|
|
}
|
|
|
|
const std::string* Loader::get_most_unloadable_level() {
|
|
for (auto& [name, lev] : m_loaded_tfrag3_levels) {
|
|
if (lev->frames_since_last_used > 180 &&
|
|
std::find(m_desired_levels.begin(), m_desired_levels.end(), name) ==
|
|
m_desired_levels.end()) {
|
|
return &name;
|
|
}
|
|
}
|
|
|
|
for (const auto& [name, lev] : m_loaded_tfrag3_levels) {
|
|
if (lev->frames_since_last_used > 180) {
|
|
return &name;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void Loader::update(TexturePool& texture_pool) {
|
|
Timer loader_timer;
|
|
|
|
{
|
|
// lock because we're accessing m_active_levels
|
|
std::unique_lock<std::mutex> lk(m_loader_mutex);
|
|
// only main thread can touch this.
|
|
for (auto& [name, lev] : m_loaded_tfrag3_levels) {
|
|
if (std::find(m_active_levels.begin(), m_active_levels.end(), name) ==
|
|
m_active_levels.end()) {
|
|
lev->frames_since_last_used++;
|
|
} else {
|
|
lev->frames_since_last_used = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool did_gpu_stuff = false;
|
|
|
|
// work on moving initializing to initialized.
|
|
{
|
|
// accessing initializing, should lock
|
|
std::unique_lock<std::mutex> lk(m_loader_mutex);
|
|
// grab the first initializing level:
|
|
const auto& it = m_initializing_tfrag3_levels.begin();
|
|
if (it != m_initializing_tfrag3_levels.end()) {
|
|
did_gpu_stuff = true;
|
|
std::string name = it->first;
|
|
auto& lev = it->second;
|
|
if (it->second->load_id == UINT64_MAX) {
|
|
it->second->load_id = m_id++;
|
|
}
|
|
|
|
// we're the only place that erases, so it's okay to unlock and hold a reference
|
|
lk.unlock();
|
|
bool done = true;
|
|
LoaderInput loader_input;
|
|
loader_input.lev_data = lev.get();
|
|
loader_input.mercs = &m_all_merc_models;
|
|
loader_input.shadows = &m_all_shadow_models;
|
|
loader_input.tex_pool = &texture_pool;
|
|
|
|
for (auto& stage : m_loader_stages) {
|
|
auto evt = scoped_prof(fmt::format("stage-{}", stage->name()).c_str());
|
|
Timer stage_timer;
|
|
done = stage->run(loader_timer, loader_input);
|
|
if (stage_timer.getMs() > 5.f) {
|
|
fmt::print("stage {} took {:.2f} ms\n", stage->name(), stage_timer.getMs());
|
|
}
|
|
if (!done) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (done) {
|
|
auto evt = scoped_prof("finish-stages");
|
|
lk.lock();
|
|
m_loaded_tfrag3_levels[name] = std::move(lev);
|
|
m_initializing_tfrag3_levels.erase(it);
|
|
|
|
for (auto& stage : m_loader_stages) {
|
|
stage->reset();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!did_gpu_stuff) {
|
|
auto evt = scoped_prof("gpu-unload");
|
|
// try to remove levels.
|
|
Timer unload_timer;
|
|
if ((int)m_loaded_tfrag3_levels.size() >= m_max_levels) {
|
|
auto to_unload = get_most_unloadable_level();
|
|
if (to_unload) {
|
|
auto& lev = m_loaded_tfrag3_levels.at(*to_unload);
|
|
std::unique_lock<std::mutex> lk(texture_pool.mutex());
|
|
fmt::print("------------------------- PC unloading {}\n", *to_unload);
|
|
for (size_t i = 0; i < lev->level->textures.size(); i++) {
|
|
auto& tex = lev->level->textures[i];
|
|
if (tex.load_to_pool) {
|
|
texture_pool.unload_texture(PcTextureId::from_combo_id(tex.combo_id),
|
|
lev->textures.at(i));
|
|
}
|
|
}
|
|
lk.unlock();
|
|
for (auto tex : lev->textures) {
|
|
if (EXTRA_TEX_DEBUG) {
|
|
for (auto& slot : texture_pool.all_textures()) {
|
|
if (slot.source) {
|
|
ASSERT(slot.gpu_texture != tex);
|
|
} else {
|
|
ASSERT(slot.gpu_texture != tex);
|
|
}
|
|
}
|
|
}
|
|
m_garbage_textures.push_back(tex);
|
|
}
|
|
|
|
for (auto& tie_geo : lev->tie_data) {
|
|
for (auto& tie_tree : tie_geo) {
|
|
m_garbage_buffers.push_back(tie_tree.vertex_buffer);
|
|
if (tie_tree.has_wind) {
|
|
m_garbage_buffers.push_back(tie_tree.wind_indices);
|
|
}
|
|
m_garbage_buffers.push_back(tie_tree.index_buffer);
|
|
}
|
|
}
|
|
|
|
for (auto& tfrag_geo : lev->tfrag_vertex_data) {
|
|
for (auto& tfrag_buff : tfrag_geo) {
|
|
m_garbage_buffers.push_back(tfrag_buff);
|
|
}
|
|
}
|
|
|
|
m_garbage_buffers.push_back(lev->hfrag_indices);
|
|
m_garbage_buffers.push_back(lev->hfrag_indices);
|
|
|
|
m_garbage_buffers.push_back(lev->collide_vertices);
|
|
m_garbage_buffers.push_back(lev->merc_vertices);
|
|
m_garbage_buffers.push_back(lev->merc_indices);
|
|
m_garbage_buffers.push_back(lev->shadow_indices);
|
|
m_garbage_buffers.push_back(lev->shadow_vertices);
|
|
|
|
for (auto& model : lev->level->merc_data.models) {
|
|
auto& mercs = m_all_merc_models.at(model.name);
|
|
MercRef ref{&model, lev->load_id};
|
|
auto it = std::find(mercs.begin(), mercs.end(), ref);
|
|
ASSERT_MSG(it != mercs.end(), fmt::format("missing merc: {}\n", model.name));
|
|
mercs.erase(it);
|
|
}
|
|
|
|
for (auto& model : lev->level->shadow_data.models) {
|
|
auto& shadows = m_all_shadow_models.at(model.name);
|
|
ShadowRef ref{&model, lev->load_id};
|
|
auto it = std::find(shadows.begin(), shadows.end(), ref);
|
|
ASSERT_MSG(it != shadows.end(), fmt::format("missing shadow: {}\n", model.name));
|
|
shadows.erase(it);
|
|
}
|
|
|
|
m_loaded_tfrag3_levels.erase(*to_unload);
|
|
}
|
|
}
|
|
|
|
if (unload_timer.getMs() > 5.f) {
|
|
fmt::print("Unload took {:.2f}ms\n", unload_timer.getMs());
|
|
}
|
|
|
|
if (!m_garbage_buffers.empty()) {
|
|
did_gpu_stuff = true;
|
|
for (int i = 0; i < 5 && !m_garbage_buffers.empty(); i++) {
|
|
glDeleteBuffers(1, &m_garbage_buffers.back());
|
|
m_garbage_buffers.pop_back();
|
|
}
|
|
}
|
|
|
|
if (!did_gpu_stuff && !m_garbage_textures.empty()) {
|
|
for (int i = 0; i < 20 && !m_garbage_textures.empty(); i++) {
|
|
glDeleteTextures(1, &m_garbage_textures.back());
|
|
m_garbage_textures.pop_back();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (loader_timer.getMs() > 5) {
|
|
fmt::print("Loader::update slow setup: {:.1f}ms\n", loader_timer.getMs());
|
|
}
|
|
}
|
|
|
|
std::optional<MercRef> Loader::get_merc_model(const char* model_name) {
|
|
// don't think we need to lock here...
|
|
const auto& it = m_all_merc_models.find(model_name);
|
|
if (it != m_all_merc_models.end() && !it->second.empty()) {
|
|
// it->second.front().parent_level->frames_since_last_used = 0;
|
|
return it->second.front();
|
|
} else {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
std::optional<ShadowRef> Loader::get_shadow_model(const char* model_name) {
|
|
// don't think we need to lock here...
|
|
const auto& it = m_all_shadow_models.find(model_name);
|
|
if (it != m_all_shadow_models.end() && !it->second.empty()) {
|
|
// it->second.front().parent_level->frames_since_last_used = 0;
|
|
return it->second.front();
|
|
} else {
|
|
return std::nullopt;
|
|
}
|
|
}
|