Files
jak-project/game/graphics/texture/TexturePool.cpp
T
Hat Kid 93afb02cf4 decomp3: spawn target, add merc and particle buckets and some temporary hacks (#3445)
This includes all the collision stuff needed to spawn `target`,
decompiles the sparticle code and adds some of the PC hacks needed for
merc to run (it doesn't work quite right and looks bad, likely due to a
combination of code copied from Jak 2 and the time of day hacks).

There are a bunch of temporary hacks (see commits) in place to prevent
the game from crashing quite as much, but it is still extremely prone to
doing so due to lots of missing functions/potentially bad decomp.

---------

Co-authored-by: water <awaterford111445@gmail.com>
2024-04-05 00:07:39 -04:00

418 lines
14 KiB
C++

#include "TexturePool.h"
#include <algorithm>
#include <regex>
#include "common/log/log.h"
#include "common/util/Assert.h"
#include "common/util/Timer.h"
#include "game/graphics/pipelines/opengl.h"
#include "game/graphics/texture/jak1_tpage_dir.h"
#include "game/graphics/texture/jak2_tpage_dir.h"
#include "game/graphics/texture/jak3_tpage_dir.h"
#include "fmt/core.h"
#include "third-party/imgui/imgui.h"
namespace {
const char empty_string[] = "";
const char* goal_string(u32 ptr, const u8* memory_base) {
if (ptr == 0) {
return empty_string;
}
return (const char*)(memory_base + ptr + 4);
}
} // namespace
std::string GoalTexturePage::print() const {
return fmt::format("Tpage id {} textures {} seg0 {} {} seg1 {} {} seg2 {} {}\n", id, length,
segment[0].size, segment[0].dest, segment[1].size, segment[1].dest,
segment[2].size, segment[2].dest);
}
u64 upload_to_gpu(const u8* data, u16 w, u16 h) {
GLuint tex_id;
glGenTextures(1, &tex_id);
GLint old_tex;
glGetIntegerv(GL_ACTIVE_TEXTURE, &old_tex);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tex_id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, data);
glGenerateMipmap(GL_TEXTURE_2D);
float aniso = 0.0f;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY, &aniso);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, aniso);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glActiveTexture(old_tex);
return tex_id;
}
GpuTexture* TexturePool::give_texture(const TextureInput& in) {
// const auto& it = m_loaded_textures.find(in.name);
const auto existing = m_loaded_textures.lookup_or_insert(in.id);
if (!existing.second) {
// nothing references this texture yet.
existing.first->tex_id = in.id;
existing.first->w = in.w;
existing.first->h = in.h;
existing.first->is_common = in.common;
existing.first->gpu_textures = {{in.gpu_texture, in.src_data}};
existing.first->is_placeholder = false;
*m_id_to_name.lookup_or_insert(in.id).first =
fmt::format("{}/{}", in.debug_page_name, in.debug_name);
return existing.first;
} else {
if (!existing.first->is_placeholder) {
// two sources for texture. this is fine.
ASSERT(!existing.first->gpu_textures.empty());
} else {
ASSERT(existing.first->gpu_textures.empty());
}
existing.first->is_placeholder = false;
existing.first->w = in.w;
existing.first->h = in.h;
existing.first->gpu_textures.push_back({in.gpu_texture, in.src_data});
existing.first->is_common = in.common;
refresh_links(*existing.first);
return existing.first;
}
}
GpuTexture* TexturePool::give_texture_and_load_to_vram(const TextureInput& in, u32 vram_slot) {
auto tex = give_texture(in);
move_existing_to_vram(tex, vram_slot);
return tex;
}
void TexturePool::move_existing_to_vram(GpuTexture* tex, u32 slot_addr) {
ASSERT(!tex->is_placeholder);
ASSERT(!tex->gpu_textures.empty());
auto& slot = m_textures[slot_addr];
if (std::find(tex->slots.begin(), tex->slots.end(), slot_addr) == tex->slots.end()) {
tex->slots.push_back(slot_addr);
}
if (slot.source) {
if (slot.source == tex) {
// we already have it, no need to do anything
} else {
slot.source->remove_slot(slot_addr);
slot.source = tex;
slot.gpu_texture = tex->gpu_textures.front().gl;
}
} else {
slot.source = tex;
slot.gpu_texture = tex->gpu_textures.front().gl;
}
}
void TexturePool::update_gl_texture(GpuTexture* gpu_texture,
u32 new_w,
u32 new_h,
GLuint new_gl_texture) {
ASSERT(gpu_texture->gpu_textures.size() == 1);
gpu_texture->gpu_textures[0].gl = new_gl_texture;
gpu_texture->w = new_w;
gpu_texture->h = new_h;
for (int si : gpu_texture->slots) {
auto& slot = m_textures[si];
ASSERT(slot.source == gpu_texture);
slot.gpu_texture = new_gl_texture;
}
}
void TexturePool::refresh_links(GpuTexture& texture) {
u64 tex_to_use =
texture.is_placeholder ? m_placeholder_texture_id : texture.gpu_textures.front().gl;
for (auto slot : texture.slots) {
auto& t = m_textures[slot];
ASSERT(t.source == &texture);
t.gpu_texture = tex_to_use;
}
for (auto slot : texture.mt4hh_slots) {
for (auto& tex : m_mt4hh_textures) {
if (tex.slot == slot) {
tex.ref.gpu_texture = tex_to_use;
}
}
}
}
void TexturePool::unload_texture(PcTextureId tex_id, u64 gpu_id) {
auto* tex = m_loaded_textures.lookup_existing(tex_id);
ASSERT(tex);
if (tex->is_common) {
ASSERT(false);
return;
}
ASSERT_MSG(!tex->is_placeholder,
fmt::format("trying to unload something that was already placholdered: {} {}\n",
get_debug_texture_name(tex_id), tex->gpu_textures.size()));
auto it = std::find_if(tex->gpu_textures.begin(), tex->gpu_textures.end(),
[&](const auto& a) { return a.gl == gpu_id; });
ASSERT(it != tex->gpu_textures.end());
tex->gpu_textures.erase(it);
if (tex->gpu_textures.empty()) {
tex->is_placeholder = true;
}
refresh_links(*tex);
}
void GpuTexture::remove_slot(u32 slot) {
auto it = std::find(slots.begin(), slots.end(), slot);
ASSERT(it != slots.end());
slots.erase(it);
}
void GpuTexture::add_slot(u32 slot) {
ASSERT(std::find(slots.begin(), slots.end(), slot) == slots.end());
slots.push_back(slot);
}
/*!
* Handle a GOAL texture-page object being uploaded to VRAM.
* The strategy:
* - upload the texture-age to a fake 4MB VRAM, like the GOAL code would have done.
* - "download" each texture in a reasonable format for the PC Port (currently RGBA8888)
* - add this to the PC pool.
*
* The textures are scrambled around in a confusing way.
*
* NOTE: the actual conversion is currently done here, but this might be too slow.
* We could store textures in the right format to begin with, or spread the conversion out over
* multiple frames.
*/
void TexturePool::handle_upload_now(const u8* tpage,
int mode,
const u8* memory_base,
u32 s7_ptr,
bool debug) {
std::unique_lock<std::mutex> lk(m_mutex);
// extract the texture-page object. This is just a description of the page data.
GoalTexturePage texture_page;
memcpy(&texture_page, tpage, sizeof(GoalTexturePage));
bool has_segment[3] = {true, true, true};
if (mode == -1) {
} else if (mode == 2) {
has_segment[0] = false;
has_segment[1] = false;
} else if (mode == -2) {
has_segment[2] = false;
} else if (mode == 0) {
has_segment[1] = false;
has_segment[2] = false;
} else {
// no reason to skip this, other than
lg::error("TexturePool skipping upload now with mode {}.", mode);
return;
}
// loop over all texture in the tpage and download them.
for (int tex_idx = 0; tex_idx < texture_page.length; tex_idx++) {
GoalTexture tex;
if (texture_page.try_copy_texture_description(&tex, tex_idx, memory_base, tpage, s7_ptr)) {
if (debug) {
fmt::print("Pool upload {} to {}\n",
std::string(goal_string(texture_page.name_ptr, memory_base)) +
goal_string(tex.name_ptr, memory_base),
tex.dest[0]);
}
// each texture may have multiple mip levels.
for (int mip_idx = 0; mip_idx < tex.num_mips; mip_idx++) {
if (has_segment[tex.segment_of_mip(mip_idx)]) {
PcTextureId current_id(texture_page.id, tex_idx);
if (!m_id_to_name.lookup_existing(current_id)) {
auto name = std::string(goal_string(texture_page.name_ptr, memory_base)) +
goal_string(tex.name_ptr, memory_base);
*m_id_to_name.lookup_or_insert(current_id).first = name;
m_name_to_id[name] = current_id;
}
auto& slot = m_textures[tex.dest[mip_idx]];
if (slot.source) {
if (slot.source->tex_id == current_id) {
// we already have it, no need to do anything
} else {
slot.source->remove_slot(tex.dest[mip_idx]);
slot.source = get_gpu_texture_for_slot(current_id, tex.dest[mip_idx]);
ASSERT(slot.gpu_texture != (GLuint)-1);
}
} else {
slot.source = get_gpu_texture_for_slot(current_id, tex.dest[mip_idx]);
ASSERT(slot.gpu_texture != (GLuint)-1);
}
}
}
} else {
// texture was #f, skip it.
}
}
}
void TexturePool::relocate(u32 destination, u32 source, u32 format) {
std::unique_lock<std::mutex> lk(m_mutex);
GpuTexture* src = lookup_gpu_texture(source);
ASSERT(src);
if (format == 44) {
m_mt4hh_textures.emplace_back();
m_mt4hh_textures.back().slot = destination;
m_mt4hh_textures.back().ref.source = src;
m_mt4hh_textures.back().ref.gpu_texture = src->gpu_textures.at(0).gl;
src->mt4hh_slots.push_back(destination);
} else {
move_existing_to_vram(src, destination);
}
}
GpuTexture* TexturePool::get_gpu_texture_for_slot(PcTextureId id, u32 slot) {
auto it = m_loaded_textures.lookup_or_insert(id);
if (!it.second) {
GpuTexture& placeholder = *it.first;
placeholder.tex_id = id;
placeholder.is_placeholder = true;
placeholder.slots.push_back(slot);
// auto r = m_loaded_textures.insert({name, placeholder});
m_textures[slot].gpu_texture = m_placeholder_texture_id;
return it.first;
} else {
auto result = it.first;
result->add_slot(slot);
m_textures[slot].gpu_texture =
result->is_placeholder ? m_placeholder_texture_id : result->gpu_textures.at(0).gl;
return result;
}
}
std::optional<u64> TexturePool::lookup_mt4hh(u32 location) {
for (auto& t : m_mt4hh_textures) {
if (t.slot == location) {
if (t.ref.source) {
return t.ref.gpu_texture;
}
}
}
return {};
}
namespace {
const std::vector<u32>& get_tpage_dir(GameVersion version) {
switch (version) {
case GameVersion::Jak1:
return get_jak1_tpage_dir();
case GameVersion::Jak2:
return get_jak2_tpage_dir();
case GameVersion::Jak3:
return get_jak3_tpage_dir();
default:
ASSERT(false);
}
}
} // namespace
TexturePool::TexturePool(GameVersion version)
: m_loaded_textures(get_tpage_dir(version)),
m_id_to_name(get_tpage_dir(version)),
m_tpage_dir_size(get_tpage_dir(version).size()) {
m_placeholder_data.resize(16 * 16);
u32 c0 = 0xa0303030;
u32 c1 = 0xa0e0e0e0;
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
m_placeholder_data[i * 16 + j] = (((i / 4) & 1) ^ ((j / 4) & 1)) ? c1 : c0;
}
}
m_placeholder_texture_id = upload_to_gpu((const u8*)(m_placeholder_data.data()), 16, 16);
}
void TexturePool::draw_debug_window() {
int id = 0;
int total_vram_bytes = 0;
int total_textures = 0;
int total_displayed_textures = 0;
int total_uploaded_textures = 0;
ImGui::InputText("texture search", m_regex_input, sizeof(m_regex_input));
bool use_regex = m_regex_input[0];
std::regex regex(use_regex ? m_regex_input : ".*");
for (size_t i = 0; i < m_textures.size(); i++) {
auto& record = m_textures[i];
total_textures++;
if (record.source) {
if (!use_regex || std::regex_search(get_debug_texture_name(record.source->tex_id), regex)) {
ImGui::PushID(id++);
draw_debug_for_tex(get_debug_texture_name(record.source->tex_id), record.source, i);
ImGui::PopID();
total_displayed_textures++;
}
if (!record.source->gpu_textures.empty()) {
total_vram_bytes +=
record.source->w * record.source->h * 4; // todo, if we support other formats
}
total_uploaded_textures++;
}
}
// todo mt4hh
ImGui::Text("Total Textures: %d Uploaded: %d Shown: %d VRAM: %.3f MB", total_textures,
total_uploaded_textures, total_displayed_textures,
(float)total_vram_bytes / (1024 * 1024));
}
void TexturePool::draw_debug_for_tex(const std::string& name, GpuTexture* tex, u32 slot) {
if (tex->is_placeholder) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8, 0.3, 0.3, 1.0));
} else if (tex->gpu_textures.size() == 1) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3, 0.8, 0.3, 1.0));
} else {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8, 0.8, 0.3, 1.0));
}
if (ImGui::TreeNode(fmt::format("{}) {}", slot, name).c_str())) {
ImGui::Text("P: %s sz: %d x %d", get_debug_texture_name(tex->tex_id).c_str(), tex->w, tex->h);
if (!tex->is_placeholder) {
ImGui::Image((void*)(u64)tex->gpu_textures.at(0).gl, ImVec2(tex->w, tex->h));
} else {
ImGui::Text("PLACEHOLDER");
}
ImGui::TreePop();
ImGui::Separator();
}
ImGui::PopStyleColor();
}
PcTextureId TexturePool::allocate_pc_port_texture(GameVersion version) {
ASSERT(m_next_pc_texture_to_allocate < EXTRA_PC_PORT_TEXTURE_COUNT);
switch (version) {
case GameVersion::Jak1:
return PcTextureId(get_jak1_tpage_dir().size() - 1, m_next_pc_texture_to_allocate++);
case GameVersion::Jak2:
return PcTextureId(get_jak2_tpage_dir().size() - 1, m_next_pc_texture_to_allocate++);
case GameVersion::Jak3:
return PcTextureId(get_jak3_tpage_dir().size() - 1, m_next_pc_texture_to_allocate++);
default:
ASSERT_NOT_REACHED();
}
}
std::string TexturePool::get_debug_texture_name(PcTextureId id) {
auto it = m_id_to_name.lookup_existing(id);
if (it) {
return *it;
} else {
return "???";
}
}