Files
jak-project/game/graphics/opengl_renderer/loader/Loader.cpp
T
water111 a06348fa9f wip
2025-06-15 18:26:56 -04:00

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;
}
}