Files
jak-project/game/graphics/opengl_renderer/loader/Loader.cpp
T
water111 2e31d82fb2 Loader improvements (#1378)
* tfrag3 data for merc2

* dma hooks for merc2

* start designing merc2 opengl, seems like the simple approach will be the best here

* before bone packing experiment

* fix up bones.gc

* use uniform buffer

* speedup, fix faces and eyes

* final fixes

* first pass at loader updates, tie is still bad

* temp

* improved loader

* run iop less often
2022-05-28 20:12:33 -04:00

410 lines
12 KiB
C++

#include "Loader.h"
#include "common/util/Timer.h"
#include "common/util/FileUtil.h"
#include "common/util/compress.h"
#include "game/graphics/opengl_renderer/loader/LoaderStages.h"
namespace {
std::string uppercase_string(const std::string& s) {
std::string result;
for (auto c : s) {
result.push_back(toupper(c));
}
return result;
}
} // namespace
Loader::Loader() {
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();
}
}
/*!
* 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;
}
}
}
/*!
* 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& lev : m_loaded_tfrag3_levels) {
if (lev.second->frames_since_last_used < 5) {
result.push_back(lev.second.get());
}
}
return result;
}
/*!
* Loader function that runs in a completely separate thread.
* This is used for file I/O and unpacking.
*/
void Loader::loader_thread() {
while (!m_want_shutdown) {
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
Timer disk_timer;
auto data = file_util::read_binary_file(
file_util::get_file_path({fmt::format("assets/{}.fr3", uppercase_string(lev))}));
double disk_load_time = disk_timer.getSeconds();
// the FR3 files are compressed
Timer decomp_timer;
auto decomp_data = compression::decompress_zstd(data.data(), data.size());
double decomp_time = decomp_timer.getSeconds();
// Read back into the tfrag3::Level structure
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();
// and finally "unpack", which creates the vertex data we'll upload to the GPU
Timer unpack_timer;
for (auto& tie_tree : result->tie_trees) {
for (auto& tree : tie_tree) {
tree.unpack();
}
}
for (auto& t_tree : result->tfrag_trees) {
for (auto& tree : t_tree) {
tree.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();
}
}
/*!
* Load a "common" FR3 file that has non-level textures.
* This should be called during initialization, before any threaded loading goes on.
*/
void Loader::load_common(TexturePool& tex_pool, const std::string& name) {
auto data =
file_util::read_binary_file(file_util::get_file_path({fmt::format("assets/{}.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;
LoaderInput input;
input.tex_pool = &tex_pool;
input.mercs = &m_all_merc_models;
input.lev_data = &m_common_level;
bool done = false;
while (!done) {
done = mls.run(tim, input);
}
}
bool Loader::upload_textures(Timer& timer, LevelData& data, TexturePool& texture_pool) {
// try to move level from initializing to initialized:
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);
}
}
void Loader::update(TexturePool& texture_pool) {
Timer loader_timer;
// only main thread can touch this.
for (auto& lev : m_loaded_tfrag3_levels) {
lev.second->frames_since_last_used++;
}
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.tex_pool = &texture_pool;
for (auto& stage : m_loader_stages) {
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) {
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) {
// try to remove levels.
Timer unload_timer;
if (m_loaded_tfrag3_levels.size() >= 3) {
for (auto& lev : m_loaded_tfrag3_levels) {
if (lev.second->frames_since_last_used > 180) {
std::unique_lock<std::mutex> lk(texture_pool.mutex());
fmt::print("------------------------- PC unloading {}\n", lev.first);
for (size_t i = 0; i < lev.second->level->textures.size(); i++) {
auto& tex = lev.second->level->textures[i];
if (tex.load_to_pool) {
texture_pool.unload_texture(PcTextureId::from_combo_id(tex.combo_id),
lev.second->textures.at(i));
}
}
lk.unlock();
for (auto tex : lev.second->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);
}
}
}
glBindTexture(GL_TEXTURE_2D, tex);
glDeleteTextures(1, &tex);
}
for (auto& tie_geo : lev.second->tie_data) {
for (auto& tie_tree : tie_geo) {
glDeleteBuffers(1, &tie_tree.vertex_buffer);
if (tie_tree.has_wind) {
glDeleteBuffers(1, &tie_tree.wind_indices);
}
}
}
for (auto& tfrag_geo : lev.second->tfrag_vertex_data) {
for (auto& tfrag_buff : tfrag_geo) {
glDeleteBuffers(1, &tfrag_buff);
}
}
glDeleteBuffers(1, &lev.second->collide_vertices);
glDeleteBuffers(1, &lev.second->merc_vertices);
glDeleteBuffers(1, &lev.second->merc_indices);
for (auto& model : lev.second->level->merc_data.models) {
auto& mercs = m_all_merc_models.at(model.name);
MercRef ref{&model, lev.second->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);
}
m_loaded_tfrag3_levels.erase(lev.first);
break;
}
}
}
if (unload_timer.getMs() > 5.f) {
fmt::print("Unload took {:.2f}\n", unload_timer.getMs());
}
}
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;
}
}