[graphics] Rewrite of texture system (#1212)

* loading cleanup

* temp

* add texture replacement

* fix windows

* oops

* fix windows

* final cleanup
This commit is contained in:
water111 2022-03-02 20:01:37 -05:00 committed by GitHub
parent 3a54a521e3
commit a5b383c78a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1141 additions and 1137 deletions

View File

@ -164,9 +164,11 @@ void BVH::serialize(Serializer& ser) {
void Texture::serialize(Serializer& ser) {
ser.from_ptr(&w);
ser.from_ptr(&h);
ser.from_ptr(&combo_id);
ser.from_pod_vector(&data);
ser.from_str(&debug_name);
ser.from_str(&debug_tpage_name);
ser.from_ptr(&load_to_pool);
}
void Level::serialize(Serializer& ser) {

View File

@ -43,7 +43,7 @@ enum MemoryUsageCategory {
NUM_CATEGORIES
};
constexpr int TFRAG3_VERSION = 11;
constexpr int TFRAG3_VERSION = 12;
// These vertices should be uploaded to the GPU at load time and don't change
struct PreloadedVertex {
@ -210,6 +210,7 @@ struct Texture {
std::vector<u32> data;
std::string debug_name;
std::string debug_tpage_name;
bool load_to_pool = false;
void serialize(Serializer& ser);
};
@ -260,12 +261,15 @@ struct TieTree {
void unpack();
};
constexpr int TFRAG_GEOS = 3;
constexpr int TIE_GEOS = 4;
struct Level {
u16 version = TFRAG3_VERSION;
std::string level_name;
std::vector<Texture> textures;
std::array<std::vector<TfragTree>, 3> tfrag_trees;
std::array<std::vector<TieTree>, 4> tie_trees;
std::array<std::vector<TfragTree>, TFRAG_GEOS> tfrag_trees;
std::array<std::vector<TieTree>, TIE_GEOS> tie_trees;
u16 version2 = TFRAG3_VERSION;
void serialize(Serializer& ser);

View File

@ -2,6 +2,8 @@
#include "third-party/fmt/core.h"
#include "common/util/Assert.h"
#include "third-party/stb_image.h"
#include <filesystem>
namespace decompiler {
@ -11,7 +13,8 @@ void TextureDB::add_texture(u32 tpage,
u16 w,
u16 h,
const std::string& tex_name,
const std::string& tpage_name) {
const std::string& tpage_name,
const std::vector<std::string>& level_names) {
auto existing_tpage_name = tpage_names.find(tpage);
if (existing_tpage_name == tpage_names.end()) {
tpage_names[tpage] = tpage_name;
@ -35,6 +38,30 @@ void TextureDB::add_texture(u32 tpage,
new_tex.h = h;
new_tex.page = tpage;
}
for (const auto& level_name : level_names) {
texture_ids_per_level[level_name].insert(combo_id);
}
}
void TextureDB::replace_textures(const std::string& path) {
std::filesystem::path base_path(path);
for (auto& tex : textures) {
std::filesystem::path full_path =
base_path / tpage_names.at(tex.second.page) / (tex.second.name + ".png");
if (std::filesystem::exists(full_path)) {
fmt::print("Replacing {}\n", full_path.string().c_str());
int w, h;
auto data = stbi_load(full_path.string().c_str(), &w, &h, 0, 4); // rgba channels
if (!data) {
fmt::print("failed to load PNG file: {}\n", full_path.string().c_str());
continue;
}
tex.second.rgba_bytes.resize(w * h);
memcpy(tex.second.rgba_bytes.data(), data, w * h * 4);
tex.second.w = w;
tex.second.h = h;
stbi_image_free(data);
}
}
}
} // namespace decompiler

View File

@ -2,6 +2,7 @@
#include <vector>
#include <unordered_map>
#include <set>
#include <string>
#include "common/common_types.h"
@ -16,6 +17,7 @@ struct TextureDB {
std::unordered_map<u32, TextureData> textures;
std::unordered_map<u32, std::string> tpage_names;
std::unordered_map<std::string, std::set<u32>> texture_ids_per_level;
void add_texture(u32 tpage,
u32 texid,
@ -23,6 +25,9 @@ struct TextureDB {
u16 w,
u16 h,
const std::string& tex_name,
const std::string& tpage_name);
const std::string& tpage_name,
const std::vector<std::string>& level_names);
void replace_textures(const std::string& path);
};
} // namespace decompiler

View File

@ -418,6 +418,7 @@ TexturePage read_texture_page(ObjectFileData& data,
TPageResultStats process_tpage(ObjectFileData& data, TextureDB& texture_db) {
TPageResultStats stats;
auto& words = data.linked_data.words_by_seg.at(0);
const auto& level_names = data.dgo_names;
// at the beginning there's a texture-page object.
// find the size first.
@ -518,12 +519,11 @@ TPageResultStats process_tpage(ObjectFileData& data, TextureDB& texture_db) {
file_util::create_dir_if_needed(
file_util::get_file_path({"assets", "textures", texture_page.name}));
file_util::write_rgba_png(
fmt::format(file_util::get_file_path(
{"assets", "textures", texture_page.name, "{}-{}-{}-{}.png"}),
data.name_in_dgo, tex.name, tex.w, tex.h),
fmt::format(file_util::get_file_path({"assets", "textures", texture_page.name, "{}.png"}),
tex.name),
out.data(), tex.w, tex.h);
texture_db.add_texture(texture_page.id, tex_id, out, tex.w, tex.h, tex.name,
texture_page.name);
texture_page.name, level_names);
stats.successful_textures++;
} else if (tex.psm == int(PSM::PSMT8) && tex.clutpsm == int(CPSM::PSMCT16)) {
// will store output pixels, rgba (8888)
@ -566,12 +566,11 @@ TPageResultStats process_tpage(ObjectFileData& data, TextureDB& texture_db) {
file_util::create_dir_if_needed(
file_util::get_file_path({"assets", "textures", texture_page.name}));
file_util::write_rgba_png(
fmt::format(file_util::get_file_path(
{"assets", "textures", texture_page.name, "{}-{}-{}-{}.png"}),
data.name_in_dgo, tex.name, tex.w, tex.h),
fmt::format(file_util::get_file_path({"assets", "textures", texture_page.name, "{}.png"}),
tex.name),
out.data(), tex.w, tex.h);
texture_db.add_texture(texture_page.id, tex_id, out, tex.w, tex.h, tex.name,
texture_page.name);
texture_page.name, level_names);
stats.successful_textures++;
} else if (tex.psm == int(PSM::PSMCT16) && tex.clutpsm == 0) {
// not a clut.
@ -596,12 +595,11 @@ TPageResultStats process_tpage(ObjectFileData& data, TextureDB& texture_db) {
file_util::create_dir_if_needed(
file_util::get_file_path({"assets", "textures", texture_page.name}));
file_util::write_rgba_png(
fmt::format(file_util::get_file_path(
{"assets", "textures", texture_page.name, "{}-{}-{}-{}.png"}),
data.name_in_dgo, tex.name, tex.w, tex.h),
fmt::format(file_util::get_file_path({"assets", "textures", texture_page.name, "{}.png"}),
tex.name),
out.data(), tex.w, tex.h);
texture_db.add_texture(texture_page.id, tex_id, out, tex.w, tex.h, tex.name,
texture_page.name);
texture_page.name, level_names);
stats.successful_textures++;
} else if (tex.psm == int(PSM::PSMT4) && tex.clutpsm == int(CPSM::PSMCT16)) {
// will store output pixels, rgba (8888)
@ -642,12 +640,11 @@ TPageResultStats process_tpage(ObjectFileData& data, TextureDB& texture_db) {
file_util::create_dir_if_needed(
file_util::get_file_path({"assets", "textures", texture_page.name}));
file_util::write_rgba_png(
fmt::format(file_util::get_file_path(
{"assets", "textures", texture_page.name, "{}-{}-{}-{}.png"}),
data.name_in_dgo, tex.name, tex.w, tex.h),
fmt::format(file_util::get_file_path({"assets", "textures", texture_page.name, "{}.png"}),
tex.name),
out.data(), tex.w, tex.h);
texture_db.add_texture(texture_page.id, tex_id, out, tex.w, tex.h, tex.name,
texture_page.name);
texture_page.name, level_names);
stats.successful_textures++;
} else if (tex.psm == int(PSM::PSMT4) && tex.clutpsm == int(CPSM::PSMCT32)) {
// will store output pixels, rgba (8888)
@ -688,12 +685,11 @@ TPageResultStats process_tpage(ObjectFileData& data, TextureDB& texture_db) {
file_util::create_dir_if_needed(
file_util::get_file_path({"assets", "textures", texture_page.name}));
file_util::write_rgba_png(
fmt::format(file_util::get_file_path(
{"assets", "textures", texture_page.name, "{}-{}-{}-{}.png"}),
data.name_in_dgo, tex.name, tex.w, tex.h),
fmt::format(file_util::get_file_path({"assets", "textures", texture_page.name, "{}.png"}),
tex.name),
out.data(), tex.w, tex.h);
texture_db.add_texture(texture_page.id, tex_id, out, tex.w, tex.h, tex.name,
texture_page.name);
texture_page.name, level_names);
stats.successful_textures++;
}

View File

@ -87,6 +87,68 @@ void print_memory_usage(const tfrag3::Level& lev, int uncompressed_data_size) {
}
}
void add_all_textures_from_level(tfrag3::Level& lev,
const std::string& level_name,
TextureDB& tex_db) {
ASSERT(lev.textures.empty());
for (auto id : tex_db.texture_ids_per_level[level_name]) {
const auto& tex = tex_db.textures.at(id);
lev.textures.emplace_back();
auto& new_tex = lev.textures.back();
new_tex.combo_id = id;
new_tex.w = tex.w;
new_tex.h = tex.h;
new_tex.debug_tpage_name = tex_db.tpage_names.at(tex.page);
new_tex.debug_name = new_tex.debug_tpage_name + tex.name;
new_tex.data = tex.rgba_bytes;
new_tex.combo_id = id;
new_tex.load_to_pool = true;
}
}
void confirm_textures_identical(TextureDB& tex_db) {
std::unordered_map<std::string, std::vector<u32>> tex_dupl;
for (auto& tex : tex_db.textures) {
auto name = tex_db.tpage_names[tex.second.page] + tex.second.name;
auto it = tex_dupl.find(name);
if (it == tex_dupl.end()) {
tex_dupl.insert({name, tex.second.rgba_bytes});
} else {
bool ok = it->second == tex.second.rgba_bytes;
if (!ok) {
fmt::print("BAD duplicate: {} {} vs {}\n", name, tex.second.rgba_bytes.size(),
it->second.size());
ASSERT(false);
}
}
}
}
/*!
* Extract common textures found in GAME.CGO
*/
void extract_common(ObjectFileDB& db, TextureDB& tex_db, const std::string& dgo_name) {
if (db.obj_files_by_dgo.count(dgo_name) == 0) {
lg::warn("Skipping common extract for {} because the DGO was not part of the input", dgo_name);
return;
}
confirm_textures_identical(tex_db);
tfrag3::Level tfrag_level;
add_all_textures_from_level(tfrag_level, dgo_name, tex_db);
Serializer ser;
tfrag_level.serialize(ser);
auto compressed =
compression::compress_zstd(ser.get_save_result().first, ser.get_save_result().second);
print_memory_usage(tfrag_level, ser.get_save_result().second);
fmt::print("compressed: {} -> {} ({:.2f}%)\n", ser.get_save_result().second, compressed.size(),
100.f * compressed.size() / ser.get_save_result().second);
file_util::write_binary_file(file_util::get_file_path({fmt::format(
"assets/{}.fr3", dgo_name.substr(0, dgo_name.length() - 4))}),
compressed.data(), compressed.size());
}
void extract_from_level(ObjectFileDB& db,
TextureDB& tex_db,
const std::string& dgo_name,
@ -121,6 +183,8 @@ void extract_from_level(ObjectFileDB& db,
int i = 0;
tfrag3::Level tfrag_level;
add_all_textures_from_level(tfrag_level, dgo_name, tex_db);
for (auto& draw_tree : bsp_header.drawable_tree_array.trees) {
if (tfrag_trees.count(draw_tree->my_type())) {
auto as_tfrag_tree = dynamic_cast<level_tools::DrawableTreeTfrag*>(draw_tree.get());

View File

@ -11,4 +11,5 @@ void extract_from_level(ObjectFileDB& db,
const std::string& dgo_name,
const DecompileHacks& hacks,
bool dump_level);
}
void extract_common(ObjectFileDB& db, TextureDB& tex_db, const std::string& dgo_name);
} // namespace decompiler

View File

@ -198,6 +198,11 @@ int main(int argc, char** argv) {
}
fmt::print("[Mem] After textures: {} MB\n", get_peak_rss() / (1024 * 1024));
// todo config
auto replacements_path = file_util::get_file_path({"texture_replacements"});
if (std::filesystem::exists(replacements_path)) {
tex_db.replace_textures(replacements_path);
}
if (config.process_game_count) {
auto result = db.process_game_count_file();
@ -207,6 +212,7 @@ int main(int argc, char** argv) {
}
if (config.levels_extract) {
extract_common(db, tex_db, "GAME.CGO");
for (auto& lev : config.levels_to_extract) {
extract_from_level(db, tex_db, lev, config.hacks, config.rip_levels);
}

View File

@ -0,0 +1,26 @@
# How to replace textures
Textures to be replaced should be saved in
```
jak-project/texture_replacements/page_name/texture_name.png
```
Where `page_name` is the name of the folder in `assets/textures` and `texture_name.png` is the name of the texture. You'll have to create the `texture_replacements` folder yourself.
# Recommended use
To make this easier to set up, you can copy the default textures from `assets`, and then modify those.
For example, you can copy the `common` folder from `assets/textures` to `texture_replacements`. Then you can modify the png files in `texture_replacements/common`
# Rebuilding the game with modified textures
Run the decompiler again to rebuild with modified textures.
If it worked, you will see:
```
Replacing jak-project/texture_replacements/common/jng-precursor-metal-plain-01-lores.png
```
in part of the output.
# Restrictions
Do not change the resolution of the sky, clouds, or eye textures. Other textures should let you change the size. Using extremely large textures will use more VRAM and will load slower.
The PNG file should have an alpha channel. Some textures use their alpha channels for transparency, or for indicating which parts should have environment mapping applied. It may be useful to look at how the original texture uses the alpha channel first, especially for particle effects.

View File

@ -177,6 +177,12 @@ void texture_relocate(u32 destination, u32 source, u32 format) {
}
}
void set_levels(const std::vector<std::string>& levels) {
if (GetCurrentRenderer()) {
GetCurrentRenderer()->set_levels(levels);
}
}
void poll_events() {
GetCurrentRenderer()->poll_events();
}

View File

@ -38,7 +38,7 @@ struct GfxRendererModule {
std::function<void(const u8*, int, u32)> texture_upload_now;
std::function<void(u32, u32, u32)> texture_relocate;
std::function<void()> poll_events;
std::function<void(const std::vector<std::string>&)> set_levels;
GfxPipeline pipeline;
const char* name;
};
@ -95,6 +95,7 @@ u32 sync_path();
void send_chain(const void* data, u32 offset);
void texture_upload_now(const u8* tpage, int mode, u32 s7_ptr);
void texture_relocate(u32 destination, u32 source, u32 format);
void set_levels(const std::vector<std::string>& levels);
void poll_events();
u64 get_window_width();
u64 get_window_height();

View File

@ -63,6 +63,7 @@ void SharedRenderState::reset() {
for (auto& x : occlusion_vis) {
x.valid = false;
}
load_status_debug.clear();
}
RenderMux::RenderMux(const std::string& name,
@ -83,12 +84,6 @@ void RenderMux::render(DmaFollower& dma,
m_renderers[m_render_idx]->render(dma, render_state, prof);
}
void RenderMux::serialize(Serializer& ser) {
for (auto& r : m_renderers) {
r->serialize(ser);
}
}
void RenderMux::draw_debug_window() {
ImGui::ListBox("Pick", &m_render_idx, m_name_str_ptrs.data(), m_renderers.size());
ImGui::Separator();

View File

@ -70,14 +70,13 @@ struct LevelVis {
* The main renderer will contain a single SharedRenderState that's passed to all bucket renderers.
* This allows bucket renders to share textures and shaders.
*/
constexpr int EYE_TEX_WIDTH = 64;
constexpr int EYE_TEX_HEIGHT = 352;
struct SharedRenderState {
explicit SharedRenderState(std::shared_ptr<TexturePool> _texture_pool)
: texture_pool(_texture_pool) {}
explicit SharedRenderState(std::shared_ptr<TexturePool> _texture_pool,
std::shared_ptr<Loader> _loader)
: texture_pool(_texture_pool), loader(_loader) {}
ShaderLibrary shaders;
std::shared_ptr<TexturePool> texture_pool;
Loader loader;
std::shared_ptr<Loader> loader;
u32 buckets_base = 0; // address of buckets array.
u32 next_bucket = 0; // address of next bucket that we haven't started rendering in buckets
@ -85,7 +84,6 @@ struct SharedRenderState {
void* ee_main_memory = nullptr;
u32 offset_of_s7;
bool dump_playback = false;
bool use_sky_cpu = true;
bool use_occlusion_culling = true;
@ -100,6 +98,8 @@ struct SharedRenderState {
bool has_camera_planes = false;
LevelVis occlusion_vis[2];
math::Vector4f camera_planes[4];
std::string load_status_debug;
};
/*!
@ -116,8 +116,8 @@ class BucketRenderer {
bool& enabled() { return m_enabled; }
virtual bool empty() const { return false; }
virtual void draw_debug_window() = 0;
virtual void serialize(Serializer&) {}
virtual void init_shaders(ShaderLibrary&) {}
virtual void init_textures(TexturePool&) {}
protected:
std::string m_name;
@ -132,7 +132,6 @@ class RenderMux : public BucketRenderer {
std::vector<std::unique_ptr<BucketRenderer>> renderers);
void render(DmaFollower& dma, SharedRenderState* render_state, ScopedProfilerNode& prof) override;
void draw_debug_window() override;
void serialize(Serializer& ser) override;
private:
std::vector<std::unique_ptr<BucketRenderer>> m_renderers;

View File

@ -291,7 +291,7 @@ void DirectRenderer::update_gl_prim(SharedRenderState* render_state) {
}
void DirectRenderer::update_gl_texture(SharedRenderState* render_state, int unit) {
TextureRecord* tex = nullptr;
std::optional<u64> tex;
auto& state = m_buffered_tex_state[unit];
if (!state.used) {
// nothing used this state, don't bother binding the texture.
@ -308,21 +308,16 @@ void DirectRenderer::update_gl_texture(SharedRenderState* render_state, int unit
if (state.texture_base_ptr >= 8160 && state.texture_base_ptr <= 8600) {
fmt::print("Failed to find texture at {}, using random (eye zone)\n", state.texture_base_ptr);
tex = render_state->texture_pool->get_random_texture();
tex = render_state->texture_pool->get_placeholder_texture();
} else {
fmt::print("Failed to find texture at {}, using random\n", state.texture_base_ptr);
tex = render_state->texture_pool->get_random_texture();
tex = render_state->texture_pool->get_placeholder_texture();
}
}
ASSERT(tex);
// first: do we need to load the texture?
if (!tex->on_gpu) {
render_state->texture_pool->upload_to_gpu(tex);
}
glActiveTexture(GL_TEXTURE20 + unit);
glBindTexture(GL_TEXTURE_2D, tex->gpu_texture);
glBindTexture(GL_TEXTURE_2D, *tex);
// Note: CLAMP and CLAMP_TO_EDGE are different...
if (state.m_clamp_state.clamp_s) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);

View File

@ -336,7 +336,7 @@ void DirectRenderer2::setup_opengl_tex(u16 unit,
bool clamp_t,
SharedRenderState* render_state) {
// look up the texture
TextureRecord* tex = nullptr;
std::optional<u64> tex;
u32 tbp_to_lookup = tbp & 0x7fff;
bool use_mt4hh = tbp & 0x8000;
@ -351,19 +351,15 @@ void DirectRenderer2::setup_opengl_tex(u16 unit,
if (tbp_to_lookup >= 8160 && tbp_to_lookup <= 8600) {
fmt::print("Failed to find texture at {}, using random (eye zone)\n", tbp_to_lookup);
tex = render_state->texture_pool->get_random_texture();
tex = render_state->texture_pool->get_placeholder_texture();
} else {
fmt::print("Failed to find texture at {}, using random\n", tbp_to_lookup);
tex = render_state->texture_pool->get_random_texture();
tex = render_state->texture_pool->get_placeholder_texture();
}
}
if (!tex->on_gpu) {
render_state->texture_pool->upload_to_gpu(tex);
}
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, tex->gpu_texture);
glBindTexture(GL_TEXTURE_2D, *tex);
if (clamp_s) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
} else {

View File

@ -6,6 +6,24 @@
EyeRenderer::EyeRenderer(const std::string& name, BucketId id) : BucketRenderer(name, id) {}
void EyeRenderer::init_textures(TexturePool& texture_pool) {
for (int pair_idx = 0; pair_idx < NUM_EYE_PAIRS; pair_idx++) {
for (int lr = 0; lr < 2; lr++) {
GLuint gl_tex;
glGenTextures(1, &gl_tex);
u32 tbp = EYE_BASE_BLOCK + pair_idx * 2 + lr;
TextureInput in;
in.gpu_texture = gl_tex;
in.w = 32;
in.h = 32;
in.page_name = "PC-EYES";
in.name = fmt::format("{}-eye-{}", lr ? "left" : "right", pair_idx);
auto* gpu_tex = texture_pool.give_texture_and_load_to_vram(in, tbp);
m_eye_textures[pair_idx * 2 + lr] = {gl_tex, gpu_tex, tbp};
}
}
}
void EyeRenderer::render(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof) {
@ -211,7 +229,7 @@ u32 bilinear_sample_eye(const u8* tex, float tx, float ty, int texw) {
template <bool blend, bool bilinear>
void draw_eye_impl(u32* out,
const EyeDraw& draw,
const TextureRecord& tex,
const GpuTexture& tex,
int pair,
int lr,
bool flipx) {
@ -254,10 +272,10 @@ void draw_eye_impl(u32* out,
for (int xd = x0s; xd < x1s; xd++) {
u32 val;
if (bilinear) {
val = bilinear_sample_eye(tex.data.data(), tx, ty, tex.w);
val = bilinear_sample_eye(tex.get_data_ptr(), tx, ty, tex.w);
} else {
int tc = int(tx) + tex.w * int(ty);
memcpy(&val, tex.data.data() + (4 * tc), 4);
memcpy(&val, tex.get_data_ptr() + (4 * tc), 4);
}
if (blend) {
if ((val >> 24) != 0) {
@ -275,7 +293,7 @@ void draw_eye_impl(u32* out,
template <bool blend>
void draw_eye(u32* out,
const EyeDraw& draw,
const TextureRecord& tex,
const GpuTexture& tex,
int pair,
int lr,
bool flipx,
@ -321,7 +339,7 @@ void EyeRenderer::handle_eye_dma2(DmaFollower& dma,
ASSERT(adgif0_dma.vif0() == 0);
ASSERT(adgif0_dma.vifcode1().kind == VifCode::Kind::DIRECT);
AdgifHelper adgif0(adgif0_dma.data + 16);
auto tex0 = render_state->texture_pool->lookup(adgif0.tex0().tbp0());
auto tex0 = render_state->texture_pool->lookup_gpu_texture(adgif0.tex0().tbp0());
if (DEBUG) {
m_debug += fmt::format("ADGIF0:\n{}\n", adgif0.print());
m_debug += fmt::format("tex: {}\n", tex0->name);
@ -339,16 +357,18 @@ void EyeRenderer::handle_eye_dma2(DmaFollower& dma,
if (DEBUG) {
m_debug += fmt::format("DRAW0\n{}", draw0.print());
}
u32 tex_val;
memcpy(&tex_val, tex0->data.data(), 4);
u32 y0 = (draw0.sprite.xyz0[1] - 512) >> 4;
pair_idx = y0 / SINGLE_EYE_SIZE;
for (auto& x : m_left) {
x = tex_val;
}
for (auto& x : m_right) {
x = tex_val;
if (tex0->get_data_ptr()) {
u32 tex_val;
memcpy(&tex_val, tex0->get_data_ptr(), 4);
for (auto& x : m_left) {
x = tex_val;
}
for (auto& x : m_right) {
x = tex_val;
}
}
}
@ -360,8 +380,10 @@ void EyeRenderer::handle_eye_dma2(DmaFollower& dma,
m_debug += fmt::format("DRAW1\n{}", draw1.print());
m_debug += fmt::format("DRAW2\n{}", draw2.print());
}
draw_eye<false>(m_left, draw1, *tex0, pair_idx, 0, false, m_use_bilinear);
draw_eye<false>(m_right, draw2, *tex0, pair_idx, 1, false, m_use_bilinear);
if (tex0->get_data_ptr()) {
draw_eye<false>(m_left, draw1, *tex0, pair_idx, 0, false, m_use_bilinear);
draw_eye<false>(m_right, draw2, *tex0, pair_idx, 1, false, m_use_bilinear);
}
}
// now we'll draw the iris on top of that
@ -372,7 +394,7 @@ void EyeRenderer::handle_eye_dma2(DmaFollower& dma,
ASSERT(adgif1_dma.vif0() == 0);
ASSERT(adgif1_dma.vifcode1().kind == VifCode::Kind::DIRECT);
AdgifHelper adgif1(adgif1_dma.data + 16);
auto tex1 = render_state->texture_pool->lookup(adgif1.tex0().tbp0());
auto tex1 = render_state->texture_pool->lookup_gpu_texture(adgif1.tex0().tbp0());
if (DEBUG) {
m_debug += fmt::format("ADGIF1:\n{}\n", adgif1.print());
m_debug += fmt::format("tex: {}\n", tex1->name);
@ -385,8 +407,10 @@ void EyeRenderer::handle_eye_dma2(DmaFollower& dma,
m_debug += fmt::format("DRAW1\n{}", draw1.print());
m_debug += fmt::format("DRAW2\n{}", draw2.print());
}
draw_eye<true>(m_left, draw1, *tex1, pair_idx, 0, false, m_use_bilinear);
draw_eye<true>(m_right, draw2, *tex1, pair_idx, 1, false, m_use_bilinear);
if (tex1->get_data_ptr()) {
draw_eye<true>(m_left, draw1, *tex1, pair_idx, 0, false, m_use_bilinear);
draw_eye<true>(m_right, draw2, *tex1, pair_idx, 1, false, m_use_bilinear);
}
}
// and finally the eyelid
@ -397,7 +421,7 @@ void EyeRenderer::handle_eye_dma2(DmaFollower& dma,
ASSERT(adgif2_dma.vif0() == 0);
ASSERT(adgif2_dma.vifcode1().kind == VifCode::Kind::DIRECT);
AdgifHelper adgif2(adgif2_dma.data + 16);
auto tex2 = render_state->texture_pool->lookup(adgif2.tex0().tbp0());
auto tex2 = render_state->texture_pool->lookup_gpu_texture(adgif2.tex0().tbp0());
if (DEBUG) {
m_debug += fmt::format("ADGIF2:\n{}\n", adgif2.print());
m_debug += fmt::format("tex: {}\n", tex2->name);
@ -410,8 +434,10 @@ void EyeRenderer::handle_eye_dma2(DmaFollower& dma,
m_debug += fmt::format("DRAW1\n{}", draw1.print());
m_debug += fmt::format("DRAW2\n{}", draw2.print());
}
draw_eye<false>(m_left, draw1, *tex2, pair_idx, 0, false, m_use_bilinear);
draw_eye<false>(m_right, draw2, *tex2, pair_idx, 1, true, m_use_bilinear);
if (tex2->get_data_ptr()) {
draw_eye<false>(m_left, draw1, *tex2, pair_idx, 0, false, m_use_bilinear);
draw_eye<false>(m_right, draw2, *tex2, pair_idx, 1, true, m_use_bilinear);
}
}
auto end = dma.read_and_advance();
@ -429,39 +455,20 @@ void EyeRenderer::handle_eye_dma2(DmaFollower& dma,
}
}
// upload to GPU:
auto left_tex = get_eye_tex(render_state, pair_idx, 0);
auto right_tex = get_eye_tex(render_state, pair_idx, 1);
glBindTexture(GL_TEXTURE_2D, left_tex->gpu_texture);
// update GPU:
auto& l = m_eye_textures[pair_idx * 2];
auto& r = m_eye_textures[pair_idx * 2 + 1];
glBindTexture(GL_TEXTURE_2D, l.gl_tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 32, 32, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV,
m_left);
glBindTexture(GL_TEXTURE_2D, right_tex->gpu_texture);
glBindTexture(GL_TEXTURE_2D, r.gl_tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 32, 32, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV,
m_right);
// make sure they are still in vram
render_state->texture_pool->move_existing_to_vram(l.gpu_tex, l.tbp);
render_state->texture_pool->move_existing_to_vram(r.gpu_tex, r.tbp);
}
float time_ms = timer.getMs();
m_average_time_ms = m_average_time_ms * 0.95 + time_ms * 0.05;
}
TextureRecord* EyeRenderer::get_eye_tex(SharedRenderState* render_state, int pair_idx, int lr) {
int tbp = EYE_BASE_BLOCK + pair_idx * 2 + lr;
TextureRecord* existing = render_state->texture_pool->lookup(tbp);
if (existing) {
return existing;
}
auto tex = std::make_shared<TextureRecord>();
tex->name = fmt::format("{}-eye-{}", lr ? "left" : "right", pair_idx);
tex->only_on_gpu = true;
tex->on_gpu = true;
tex->do_gc = false;
tex->w = 32;
tex->h = 32;
GLuint gl_tex;
glGenTextures(1, &gl_tex);
tex->gpu_texture = gl_tex;
render_state->texture_pool->set_texture(tbp, tex);
return tex.get();
}

View File

@ -13,12 +13,11 @@ class EyeRenderer : public BucketRenderer {
EyeRenderer(const std::string& name, BucketId id);
void render(DmaFollower& dma, SharedRenderState* render_state, ScopedProfilerNode& prof) override;
void draw_debug_window() override;
void init_textures(TexturePool& texture_pool) override;
template <bool DEBUG>
void handle_eye_dma2(DmaFollower& dma, SharedRenderState* render_state, ScopedProfilerNode& prof);
TextureRecord* get_eye_tex(SharedRenderState* render_state, int pair_idx, int lr);
private:
std::string m_debug;
float m_average_time_ms = 0;
@ -28,4 +27,11 @@ class EyeRenderer : public BucketRenderer {
u32 m_left[SINGLE_EYE_SIZE * SINGLE_EYE_SIZE];
u32 m_right[SINGLE_EYE_SIZE * SINGLE_EYE_SIZE];
struct EyeTex {
u64 gl_tex;
GpuTexture* gpu_tex;
u32 tbp;
};
EyeTex m_eye_textures[NUM_EYE_PAIRS * 2];
};

View File

@ -17,21 +17,36 @@ const Loader::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()) {
if (m_level_to_load.empty() && m_initializing_tfrag3_levels.count(level_name) == 0) {
fmt::print("[pc loader] starting load for {}\n", level_name);
m_level_to_load = level_name;
lk.unlock();
m_loader_cv.notify_all();
return nullptr;
} else {
return nullptr;
}
return nullptr;
} else {
existing->second.frames_since_last_used = 0;
return &existing->second.data;
}
}
void Loader::set_want_levels(const std::vector<std::string>& levels) {
std::unique_lock<std::mutex> lk(m_loader_mutex);
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;
}
for (auto& lev : levels) {
auto it = m_loaded_tfrag3_levels.find(lev);
if (it == m_loaded_tfrag3_levels.end()) {
m_level_to_load = lev;
lk.unlock();
m_loader_cv.notify_all();
return;
}
}
}
void Loader::loader_thread() {
while (!m_want_shutdown) {
std::unique_lock<std::mutex> lk(m_loader_mutex);
@ -84,11 +99,52 @@ void Loader::loader_thread() {
}
}
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.serialize(ser);
for (auto& tex : m_common_level.textures) {
add_texture(tex_pool, tex, true);
}
}
u64 Loader::add_texture(TexturePool& pool, const tfrag3::Texture& tex, bool is_common) {
GLuint gl_tex;
glGenTextures(1, &gl_tex);
glBindTexture(GL_TEXTURE_2D, gl_tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tex.w, tex.h, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV,
tex.data.data());
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, gl_tex);
glGenerateMipmap(GL_TEXTURE_2D);
float aniso = 0.0f;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY, &aniso);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, aniso);
if (tex.load_to_pool) {
TextureInput in;
in.page_name = tex.debug_tpage_name;
in.name = tex.debug_name;
in.w = tex.w;
in.h = tex.h;
in.gpu_texture = gl_tex;
in.common = is_common;
in.combo_id = tex.combo_id;
in.src_data = (const u8*)tex.data.data();
pool.give_texture(in);
}
return gl_tex;
}
Loader::Loader() {
m_loader_thread = std::thread(&Loader::loader_thread, this);
}
void Loader::update() {
void Loader::update(std::string& status_out, TexturePool& texture_pool) {
Timer loader_timer;
// only main thread can touch this.
@ -104,28 +160,24 @@ void Loader::update() {
const auto& it = m_initializing_tfrag3_levels.begin();
if (it != m_initializing_tfrag3_levels.end()) {
did_gpu_stuff = true;
constexpr int MAX_TEX_BYTES_PER_FRAME = 1024 * 1024;
constexpr int MAX_TEX_BYTES_PER_FRAME = 1024 * 128;
auto& data = it->second.data;
int bytes_this_run = 0;
int tex_this_run = 0;
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()];
GLuint gl_tex;
glGenTextures(1, &gl_tex);
glBindTexture(GL_TEXTURE_2D, gl_tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tex.w, tex.h, 0, GL_RGBA,
GL_UNSIGNED_INT_8_8_8_8_REV, tex.data.data());
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, gl_tex);
glGenerateMipmap(GL_TEXTURE_2D);
float aniso = 0.0f;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY, &aniso);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, aniso);
it->second.data.textures.push_back(gl_tex);
it->second.data.textures.push_back(add_texture(texture_pool, tex, false));
bytes_this_run += tex.w * tex.h * 4;
if (bytes_this_run > MAX_TEX_BYTES_PER_FRAME) {
tex_this_run++;
if (tex_this_run > 20) {
status_out += fmt::format("LOAD tex {} kB\n", bytes_this_run / 1024);
break;
}
if (bytes_this_run > MAX_TEX_BYTES_PER_FRAME ||
loader_timer.getMs() > SHARED_TEXTURE_LOAD_BUDGET) {
status_out += fmt::format("LOAD tex {} kB\n", bytes_this_run / 1024);
break;
}
}
@ -144,11 +196,29 @@ void Loader::update() {
if (m_loaded_tfrag3_levels.size() >= 3) {
for (const 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.data.level->textures.size(); i++) {
auto& tex = lev.second.data.level->textures[i];
if (tex.load_to_pool) {
texture_pool.unload_texture(tex.debug_name, lev.second.data.textures.at(i));
}
}
for (auto tex : lev.second.data.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);
}
m_loaded_tfrag3_levels.erase(lev.first);
break;
}

View File

@ -5,14 +5,16 @@
#include <condition_variable>
#include "game/graphics/pipelines/opengl.h"
#include "game/graphics/texture/TexturePool.h"
#include "common/custom_data/Tfrag3Data.h"
class Loader {
public:
static constexpr float TIE_LOAD_BUDGET = 1.5f;
static constexpr float SHARED_TEXTURE_LOAD_BUDGET = 3.f;
Loader();
~Loader();
void update();
void update(std::string& status_out, TexturePool& tex_pool);
struct LevelData {
std::unique_ptr<tfrag3::Level> level;
@ -22,6 +24,9 @@ class Loader {
const LevelData* get_tfrag3_level(const std::string& level_name);
void hack_scramble_textures();
void load_common(TexturePool& tex_pool, const std::string& name);
void set_want_levels(const std::vector<std::string>& levels);
private:
struct Level {
@ -30,10 +35,13 @@ class Loader {
};
void loader_thread();
u64 add_texture(TexturePool& pool, const tfrag3::Texture& tex, bool is_common);
std::unordered_map<std::string, Level> m_loaded_tfrag3_levels;
std::unordered_map<std::string, Level> m_initializing_tfrag3_levels;
tfrag3::Level m_common_level;
std::string m_level_to_load;
std::thread m_loader_thread;

View File

@ -47,8 +47,9 @@ void GLAPIENTRY opengl_error_callback(GLenum source,
}
}
OpenGLRenderer::OpenGLRenderer(std::shared_ptr<TexturePool> texture_pool)
: m_render_state(texture_pool) {
OpenGLRenderer::OpenGLRenderer(std::shared_ptr<TexturePool> texture_pool,
std::shared_ptr<Loader> loader)
: m_render_state(texture_pool, loader) {
// setup OpenGL errors
glEnable(GL_DEBUG_OUTPUT);
@ -222,7 +223,10 @@ void OpenGLRenderer::init_bucket_renderers() {
}
m_bucket_renderers[i]->init_shaders(m_render_state.shaders);
m_bucket_renderers[i]->init_textures(*m_render_state.texture_pool);
}
sky_cpu_blender->init_textures(*m_render_state.texture_pool);
m_render_state.loader->load_common(*m_render_state.texture_pool, "GAME");
}
/*!
@ -231,8 +235,7 @@ void OpenGLRenderer::init_bucket_renderers() {
void OpenGLRenderer::render(DmaFollower dma, const RenderOptions& settings) {
m_profiler.clear();
m_render_state.reset();
m_render_state.dump_playback = settings.playing_from_dump;
m_render_state.ee_main_memory = settings.playing_from_dump ? nullptr : g_ee_main_mem;
m_render_state.ee_main_memory = g_ee_main_mem;
m_render_state.offset_of_s7 = offset_of_s7();
m_render_state.has_camera_planes = false;
@ -241,10 +244,6 @@ void OpenGLRenderer::render(DmaFollower dma, const RenderOptions& settings) {
setup_frame(settings.window_width_px, settings.window_height_px, settings.lbox_width_px,
settings.lbox_height_px);
}
{
auto prof = m_profiler.root()->make_scoped_child("texture-gc");
m_render_state.texture_pool->remove_garbage_textures();
}
// draw_test_triangle();
// render the buckets!
@ -257,9 +256,12 @@ void OpenGLRenderer::render(DmaFollower dma, const RenderOptions& settings) {
auto prof = m_profiler.root()->make_scoped_child("render-window");
draw_renderer_selection_window();
// add a profile bar for the imgui stuff
if (!m_render_state.dump_playback) {
vif_interrupt_callback();
}
vif_interrupt_callback();
}
{
auto prof = m_profiler.root()->make_scoped_child("loader");
m_render_state.loader->update(m_render_state.load_status_debug, *m_render_state.texture_pool);
}
m_profiler.finish();
@ -267,23 +269,19 @@ void OpenGLRenderer::render(DmaFollower dma, const RenderOptions& settings) {
m_profiler.draw();
}
// if (m_profiler.root_time() > 0.018) {
// fmt::print("Slow frame: {:.2f} ms\n", m_profiler.root_time() * 1000);
// fmt::print("{}\n", m_profiler.to_string());
// }
if (settings.draw_small_profiler_window) {
m_profiler.draw_small_window();
m_profiler.draw_small_window(m_render_state.load_status_debug);
}
if (settings.save_screenshot) {
finish_screenshot(settings.screenshot_path, settings.window_width_px, settings.window_height_px,
settings.lbox_width_px, settings.lbox_height_px);
}
m_render_state.loader.update();
}
void OpenGLRenderer::serialize(Serializer& ser) {
m_render_state.texture_pool->serialize(ser);
for (auto& renderer : m_bucket_renderers) {
renderer->serialize(ser);
}
}
/*!
@ -378,10 +376,7 @@ void OpenGLRenderer::dispatch_buckets(DmaFollower dma, ScopedProfilerNode& prof)
// should have ended at the start of the next chain
ASSERT(dma.current_tag_offset() == m_render_state.next_bucket);
m_render_state.next_bucket += 16;
if (!m_render_state.dump_playback) {
vif_interrupt_callback();
}
vif_interrupt_callback();
}
g_current_render = "";

View File

@ -16,7 +16,6 @@ struct RenderOptions {
bool draw_render_debug_window = false;
bool draw_profiler_window = false;
bool draw_small_profiler_window = false;
bool playing_from_dump = false;
bool save_screenshot = false;
std::string screenshot_path;
@ -24,9 +23,8 @@ struct RenderOptions {
class OpenGLRenderer {
public:
OpenGLRenderer(std::shared_ptr<TexturePool> texture_pool);
OpenGLRenderer(std::shared_ptr<TexturePool> texture_pool, std::shared_ptr<Loader> loader);
void render(DmaFollower dma, const RenderOptions& settings);
void serialize(Serializer& ser);
private:
void setup_frame(int window_width_px, int window_height_px, int offset_x, int offset_y);

View File

@ -88,7 +88,7 @@ void Profiler::draw() {
ImGui::End();
}
void Profiler::draw_small_window() {
void Profiler::draw_small_window(const std::string& status) {
ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoSavedSettings |
@ -109,6 +109,9 @@ void Profiler::draw_small_window() {
if (ImGui::Begin("Profiler (short)", p_open, window_flags)) {
ImGui::Text(" tri: %7d\n", m_root.m_stats.triangles);
ImGui::Text(" DC: %4d\n", m_root.m_stats.draw_calls);
if (!status.empty()) {
ImGui::Text("%s", status.c_str());
}
}
ImGui::End();
}
@ -167,4 +170,19 @@ void Profiler::draw_node(ProfilerNode& node, bool expand, int depth, float start
}
ImGui::PopStyleColor();
}
std::string Profiler::to_string() {
m_root.sort(ProfilerSort::TIME);
std::string str;
m_root.to_string_helper(str, 0);
return str;
}
void ProfilerNode::to_string_helper(std::string& str, int depth) const {
str +=
fmt::format("{}{:.2f} ms {:30s}\n", std::string(depth, ' '), m_stats.duration * 1000, m_name);
for (const auto& child : m_children) {
child.to_string_helper(str, depth + 1);
}
}

View File

@ -37,6 +37,8 @@ class ProfilerNode {
private:
friend class Profiler;
void to_string_helper(std::string& str, int depth) const;
std::string m_name;
ProfilerStats m_stats;
std::vector<ProfilerNode> m_children;
@ -67,8 +69,12 @@ class Profiler {
Profiler();
void clear();
void draw();
void draw_small_window();
void draw_small_window(const std::string& status);
void finish();
float root_time() const { return m_root.m_stats.duration; }
std::string to_string();
ProfilerNode* root() { return &m_root; }
private:

View File

@ -92,13 +92,9 @@ SkyBlendStats SkyBlendCPU::do_sky_blends(DmaFollower& dma,
ScopedProfilerNode& /*prof*/) {
SkyBlendStats stats;
Timer sky_timer;
while (dma.current_tag().qwc == 6) {
// assuming that the vif and gif-tag is correct
auto setup_data = dma.read_and_advance();
if (render_state->dump_playback) {
// continue;
}
// first is an adgif
AdgifHelper adgif(setup_data.data + 16);
@ -133,9 +129,8 @@ SkyBlendStats SkyBlendCPU::do_sky_blends(DmaFollower& dma,
}
// look up the source texture
auto tex = render_state->texture_pool->lookup(adgif.tex0().tbp0());
auto tex = render_state->texture_pool->lookup_gpu_texture(adgif.tex0().tbp0());
ASSERT(tex);
ASSERT(!tex->only_on_gpu); // we need the actual data!!
// slow version
/*
@ -153,59 +148,48 @@ SkyBlendStats SkyBlendCPU::do_sky_blends(DmaFollower& dma,
m_texture_data[buffer_idx][i] += val;
}
*/
if (is_first_draw) {
blend_sky_initial_fast(intensity, m_texture_data[buffer_idx].data(), tex->data.data(),
tex->data.size());
} else {
blend_sky_fast(intensity, m_texture_data[buffer_idx].data(), tex->data.data(),
tex->data.size());
}
if (buffer_idx == 0) {
if (tex->get_data_ptr()) {
if (is_first_draw) {
stats.sky_draws++;
blend_sky_initial_fast(intensity, m_texture_data[buffer_idx].data(), tex->get_data_ptr(),
tex->data_size());
} else {
stats.sky_blends++;
blend_sky_fast(intensity, m_texture_data[buffer_idx].data(), tex->get_data_ptr(),
tex->data_size());
}
} else {
if (is_first_draw) {
stats.cloud_draws++;
if (buffer_idx == 0) {
if (is_first_draw) {
stats.sky_draws++;
} else {
stats.sky_blends++;
}
} else {
stats.cloud_blends++;
if (is_first_draw) {
stats.cloud_draws++;
} else {
stats.cloud_blends++;
}
}
glBindTexture(GL_TEXTURE_2D, m_textures[buffer_idx]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_sizes[buffer_idx], m_sizes[buffer_idx], 0, GL_RGBA,
GL_UNSIGNED_INT_8_8_8_8_REV, m_texture_data[buffer_idx].data());
}
}
// put in pool.
if (render_state->dump_playback) {
return stats;
}
return stats;
}
void SkyBlendCPU::init_textures(TexturePool& tex_pool) {
for (int i = 0; i < 2; i++) {
// todo - these are hardcoded and rely on the vram layout.
u32 tbp = i == 0 ? 8064 : 8096;
// lookup existing, or create a new entry
TextureRecord* tex = render_state->texture_pool->lookup(tbp);
if (!tex) {
auto tsp = std::make_shared<TextureRecord>();
render_state->texture_pool->set_texture(tbp, tsp);
tex = tsp.get();
}
// update it
glBindTexture(GL_TEXTURE_2D, m_textures[i]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_sizes[i], m_sizes[i], 0, GL_RGBA,
GL_UNSIGNED_INT_8_8_8_8_REV, m_texture_data[i].data());
TextureInput in;
tex->gpu_texture = m_textures[i];
tex->on_gpu = true;
tex->only_on_gpu = true;
tex->do_gc = false;
tex->w = m_sizes[i];
tex->h = m_sizes[i];
tex->name = fmt::format("PC-SKY-{}", i);
in.gpu_texture = m_textures[i];
in.w = m_sizes[i];
in.h = m_sizes[i];
in.name = fmt::format("PC-SKY-CPU-{}", i);
tex_pool.give_texture_and_load_to_vram(in, SKY_TEXTURE_VRAM_ADDRS[i]);
}
// fmt::print("sky blend took {:.2f} ms\n", sky_timer.getMs());
return stats;
}

View File

@ -13,6 +13,7 @@ class SkyBlendCPU {
SkyBlendStats do_sky_blends(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof);
void init_textures(TexturePool& tex_pool);
private:
GLuint m_textures[2]; // sky, clouds

View File

@ -59,6 +59,17 @@ SkyBlendGPU::~SkyBlendGPU() {
glDeleteTextures(2, m_textures);
}
void SkyBlendGPU::init_textures(TexturePool& tex_pool) {
for (int i = 0; i < 2; i++) {
TextureInput in;
in.gpu_texture = m_textures[i];
in.w = m_sizes[i];
in.h = in.w;
in.name = fmt::format("PC-SKY-GPU-{}", i);
tex_pool.give_texture_and_load_to_vram(in, SKY_TEXTURE_VRAM_ADDRS[i]);
}
}
SkyBlendStats SkyBlendGPU::do_sky_blends(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof) {
@ -76,9 +87,6 @@ SkyBlendStats SkyBlendGPU::do_sky_blends(DmaFollower& dma,
while (dma.current_tag().qwc == 6) {
// assuming that the vif and gif-tag is correct
auto setup_data = dma.read_and_advance();
if (render_state->dump_playback) {
// continue;
}
// first is an adgif
AdgifHelper adgif(setup_data.data + 16);
@ -116,10 +124,6 @@ SkyBlendStats SkyBlendGPU::do_sky_blends(DmaFollower& dma,
auto tex = render_state->texture_pool->lookup(adgif.tex0().tbp0());
ASSERT(tex);
if (!tex->on_gpu) {
render_state->texture_pool->upload_to_gpu(tex);
}
// setup for rendering!
glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffers[buffer_idx]);
glViewport(0, 0, m_sizes[buffer_idx], m_sizes[buffer_idx]);
@ -162,7 +166,7 @@ SkyBlendStats SkyBlendGPU::do_sky_blends(DmaFollower& dma,
);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tex->gpu_texture);
glBindTexture(GL_TEXTURE_2D, *tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
@ -190,29 +194,6 @@ SkyBlendStats SkyBlendGPU::do_sky_blends(DmaFollower& dma,
}
}
// put in pool.
for (int i = 0; i < 2; i++) {
// todo - these are hardcoded and rely on the vram layout.
u32 tbp = i == 0 ? 8064 : 8096;
// lookup existing, or create a new entry
TextureRecord* tex = render_state->texture_pool->lookup(tbp);
if (!tex) {
auto tsp = std::make_shared<TextureRecord>();
render_state->texture_pool->set_texture(tbp, tsp);
tex = tsp.get();
}
// update it
tex->gpu_texture = m_textures[i];
tex->on_gpu = true;
tex->only_on_gpu = true;
tex->do_gc = false;
tex->w = m_sizes[i];
tex->h = m_sizes[i];
tex->name = fmt::format("PC-SKY-{}", i);
}
glViewport(old_viewport[0], old_viewport[1], old_viewport[2], old_viewport[3]);
glBindFramebuffer(GL_FRAMEBUFFER, old_framebuffer);
glBindVertexArray(0);

View File

@ -8,7 +8,7 @@ class SkyBlendGPU {
public:
SkyBlendGPU();
~SkyBlendGPU();
void init_textures(TexturePool& tex_pool);
SkyBlendStats do_sky_blends(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof);

View File

@ -470,22 +470,17 @@ void Sprite3::flush_sprites(SharedRenderState* render_state,
DrawMode mode;
mode.as_int() = bucket->key & 0xffffffff;
TextureRecord* tex = nullptr;
std::optional<u64> tex;
tex = render_state->texture_pool->lookup(tbp);
if (!tex) {
fmt::print("Failed to find texture at {}, using random\n", tbp);
tex = render_state->texture_pool->get_random_texture();
tex = render_state->texture_pool->get_placeholder_texture();
}
ASSERT(tex);
// first: do we need to load the texture?
if (!tex->on_gpu) {
render_state->texture_pool->upload_to_gpu(tex);
}
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tex->gpu_texture);
glBindTexture(GL_TEXTURE_2D, *tex);
auto settings = setup_opengl_from_draw_mode(mode, GL_TEXTURE0, false);

View File

@ -597,7 +597,7 @@ void SpriteRenderer::update_gl_prim(SharedRenderState* /*render_state*/) {
}
void SpriteRenderer::update_gl_texture(SharedRenderState* render_state, int unit) {
TextureRecord* tex = nullptr;
std::optional<u64> tex;
auto& state = m_adgif_state_stack[unit];
if (!state.used) {
// nothing used this state, don't bother binding the texture.
@ -610,22 +610,13 @@ void SpriteRenderer::update_gl_texture(SharedRenderState* render_state, int unit
}
if (!tex) {
// TODO Add back
fmt::print("Failed to find texture at {}, using random\n", state.texture_base_ptr);
tex = render_state->texture_pool->get_random_texture();
if (tex) {
// fmt::print("Successful texture lookup! {} {}\n", tex->page_name, tex->name);
}
tex = render_state->texture_pool->get_placeholder_texture();
}
ASSERT(tex);
// first: do we need to load the texture?
if (!tex->on_gpu) {
render_state->texture_pool->upload_to_gpu(tex);
}
glActiveTexture(GL_TEXTURE20 + unit);
glBindTexture(GL_TEXTURE_2D, tex->gpu_texture);
glBindTexture(GL_TEXTURE_2D, *tex);
// Note: CLAMP and CLAMP_TO_EDGE are different...
if (state.clamp_s) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);

View File

@ -10,8 +10,6 @@ TextureUploadHandler::TextureUploadHandler(const std::string& name, BucketId my_
void TextureUploadHandler::render(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& /*prof*/) {
m_stats = {};
// this is the data we get from the PC Port modification.
struct TextureUpload {
u64 page;
@ -46,65 +44,26 @@ void TextureUploadHandler::render(DmaFollower& dma,
}
}
// if we're replaying a graphics dump, don't try to read ee memory
// TODO, we might still want to grab stuff from the cache
if (render_state->dump_playback) {
return;
}
// NOTE: we don't actually copy the textures in the dma chain copying because they aren't
// reference by DMA tag. So there's the potential for race conditions if the game gets messed
// up and corrupts the texture memory.
const u8* ee_mem = (const u8*)render_state->ee_main_memory;
// The logic here is a bit confusing. It works around an issue where higher LODs are uploaded
// before their CLUT in some cases.
if (uploads.size() == 2 && uploads[0].mode == 2 && uploads[1].mode == -2 &&
uploads[0].page == uploads[1].page) {
bool has_segment[3] = {true, true, true};
if (!try_to_populate_from_cache(uploads[0].page, has_segment, render_state)) {
// couldn't find this texture in cache, need to convert it
populate_cache(render_state->texture_pool->convert_textures(
ee_mem + uploads[0].page, -2, ee_mem, render_state->offset_of_s7),
render_state);
populate_cache(render_state->texture_pool->convert_textures(
ee_mem + uploads[0].page, 2, ee_mem, render_state->offset_of_s7),
render_state);
// after conversion, we should be able to populate the texture pool.
bool ok = try_to_populate_from_cache(uploads[0].page, has_segment, render_state);
ASSERT(ok);
}
render_state->texture_pool->handle_upload_now(ee_mem + uploads[0].page, -2, ee_mem,
render_state->offset_of_s7);
render_state->texture_pool->handle_upload_now(ee_mem + uploads[0].page, 2, ee_mem,
render_state->offset_of_s7);
} else if (uploads.size() == 1 && uploads[0].mode == -1) {
// look at the texture page and determine if we have it in cache.
bool has_segment[3] = {true, true, true};
if (!try_to_populate_from_cache(uploads[0].page, has_segment, render_state)) {
populate_cache(render_state->texture_pool->convert_textures(
ee_mem + uploads[0].page, -1, ee_mem, render_state->offset_of_s7),
render_state);
bool ok = try_to_populate_from_cache(uploads[0].page, has_segment, render_state);
ASSERT(ok);
}
render_state->texture_pool->handle_upload_now(ee_mem + uploads[0].page, -1, ee_mem,
render_state->offset_of_s7);
} else if (uploads.size() == 1 && uploads[0].mode == -2) {
bool has_segment[3] = {true, true, true};
if (!try_to_populate_from_cache(uploads[0].page, has_segment, render_state)) {
populate_cache(render_state->texture_pool->convert_textures(
ee_mem + uploads[0].page, -2, ee_mem, render_state->offset_of_s7),
render_state);
bool ok = try_to_populate_from_cache(uploads[0].page, has_segment, render_state);
ASSERT(ok);
}
render_state->texture_pool->handle_upload_now(ee_mem + uploads[0].page, -2, ee_mem,
render_state->offset_of_s7);
} else if (uploads.size() == 1 && uploads[0].mode == 0) {
bool has_segment[3] = {true, true, true};
if (!try_to_populate_from_cache(uploads[0].page, has_segment, render_state)) {
populate_cache(render_state->texture_pool->convert_textures(
ee_mem + uploads[0].page, 0, ee_mem, render_state->offset_of_s7),
render_state);
bool ok = try_to_populate_from_cache(uploads[0].page, has_segment, render_state);
ASSERT(ok);
}
render_state->texture_pool->handle_upload_now(ee_mem + uploads[0].page, 0, ee_mem,
render_state->offset_of_s7);
}
else if (uploads.empty()) {
@ -118,145 +77,4 @@ void TextureUploadHandler::render(DmaFollower& dma,
}
}
void TextureUploadHandler::draw_debug_window() {
ImGui::Text("Textures this frame: %d", m_stats.textures_provided);
ImGui::Text("Textures converted: %d", m_stats.textures_converted);
ImGui::Text("Textures replaced: %d", m_stats.textures_evicted);
}
namespace {
const char* goal_string(u32 ptr, const u8* memory_base) {
if (ptr == 0) {
ASSERT(false);
}
return (const char*)(memory_base + ptr + 4);
}
} // namespace
/*!
* Try to set an entry in the texture pool from a cached texture for the given page (GOAL pointer).
*/
bool TextureUploadHandler::try_to_populate_from_cache(u64 page,
const bool with_seg[3],
SharedRenderState* render_state) {
auto old_tex_provided = m_stats.textures_provided;
const u8* ee_mem = (const u8*)render_state->ee_main_memory;
auto tpage = ee_mem + page;
GoalTexturePage texture_page;
memcpy(&texture_page, tpage, sizeof(GoalTexturePage));
// loop over all textures in the page
for (int tex_idx = 0; tex_idx < texture_page.length; tex_idx++) {
// we might have some invalid textures, for whatever reason. The PS2 side checks for this.
GoalTexture tex;
if (texture_page.try_copy_texture_description(&tex, tex_idx, ee_mem, tpage,
render_state->offset_of_s7)) {
// loop over all mip levels of this texture
for (int mip_idx = 0; mip_idx < tex.num_mips; mip_idx++) {
// only grab mip levels that we requested (we don't want to overwrite vram that the engine
// expects us to not touch)
if (with_seg[tex.segment_of_mip(mip_idx)]) {
m_stats.textures_provided++;
// lookup the texture by name!
auto it = m_tex_cache.find(goal_string(tex.name_ptr, ee_mem));
if (it == m_tex_cache.end() || !it->second.at(mip_idx)) {
// failed to find it, reject the entire page load
m_stats.textures_provided = old_tex_provided;
return false;
} else {
// found it! Set it in the pool (just setting a pointer)
render_state->texture_pool->set_texture(tex.dest[mip_idx], it->second.at(mip_idx));
}
}
}
}
}
return true;
}
/*!
* Cache the given textures and set in pool
*/
void TextureUploadHandler::populate_cache(
const std::vector<std::shared_ptr<TextureRecord>>& textures,
SharedRenderState* render_state) {
for (auto& tex : textures) {
// disable automatic GC of these textures. We need this - even if the texture becomes evicted
// from PS2 VRAM, we want to hold on to the conversion. Now this cache will be responsible for
// managing this texture.
tex->do_gc = false;
m_stats.textures_provided++;
m_stats.textures_converted++;
// put in pool too
render_state->texture_pool->set_texture(tex->dest, tex);
auto it = m_tex_cache.find(tex->name);
if (it != m_tex_cache.end()) {
if (it->second.at(tex->mip_level)) {
// replacing an existing, don't forget to kill the original.
m_stats.textures_evicted++;
render_state->texture_pool->discard(it->second.at(tex->mip_level));
}
it->second.at(tex->mip_level) = tex;
} else {
std::vector<std::shared_ptr<TextureRecord>> recs(7); // max mip
recs.at(tex->mip_level) = tex;
m_tex_cache.insert({tex->name, std::move(recs)});
}
}
}
/*!
* Unload any cached textures from GPU.
* Remove all textures from this cache.
* Set do_gc on all textures, as they may be in use in the pool and we may need them.
*
* Effectively, this will require all textures to re-converted and re-uploaded next time they are
* uploaded from the game.
*/
void TextureUploadHandler::evict_all() {
for (auto& e : m_tex_cache) {
for (auto& x : e.second) {
if (x) {
if (x->on_gpu) {
x->unload_from_gpu();
}
x->do_gc = true;
}
}
}
m_tex_cache = {};
}
void TextureUploadHandler::serialize(Serializer& ser) {
if (ser.is_saving()) {
ser.save<size_t>(m_tex_cache.size());
for (auto& entry : m_tex_cache) {
ser.save_str(&entry.first);
ser.save<size_t>(entry.second.size());
for (auto& x : entry.second) {
if (x) {
ser.save<u8>(1);
x->serialize(ser);
} else {
ser.save<u8>(0);
}
}
}
} else {
evict_all();
auto size = ser.load<size_t>();
for (size_t i = 0; i < size; i++) {
auto str = ser.load_string();
std::vector<std::shared_ptr<TextureRecord>> recs(ser.load<size_t>());
for (auto& x : recs) {
if (ser.load<u8>()) {
x = std::make_shared<TextureRecord>();
x->serialize(ser);
x->on_gpu = false;
}
}
m_tex_cache.insert({str, std::move(recs)});
}
}
}
void TextureUploadHandler::draw_debug_window() {}

View File

@ -16,21 +16,4 @@ class TextureUploadHandler : public BucketRenderer {
TextureUploadHandler(const std::string& name, BucketId my_id);
void render(DmaFollower& dma, SharedRenderState* render_state, ScopedProfilerNode& prof) override;
void draw_debug_window() override;
void serialize(Serializer& ser) override;
private:
void evict_all();
bool try_to_populate_from_cache(u64 page,
const bool with_seg[3],
SharedRenderState* render_state);
void populate_cache(const std::vector<std::shared_ptr<TextureRecord>>& textures,
SharedRenderState* render_state);
std::unordered_map<std::string, std::vector<std::shared_ptr<TextureRecord>>> m_tex_cache;
struct {
u32 textures_provided = 0;
u32 textures_converted = 0;
u32 textures_evicted = 0;
} m_stats;
};

View File

@ -96,19 +96,9 @@ void OpenGlDebugGui::draw(const DmaStats& dma_stats) {
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Gfx Dump")) {
if (ImGui::BeginMenu("Screenshot")) {
ImGui::MenuItem("Screenshot Next Frame!", nullptr, &m_want_screenshot);
ImGui::InputText("File", m_screenshot_save_name, 50);
ImGui::Separator();
ImGui::MenuItem("Dump Next Frame!", nullptr, &m_want_save);
bool old_replay = m_want_replay;
ImGui::MenuItem("Load Saved Dump", nullptr, &m_want_replay);
if (!old_replay && m_want_replay) {
m_want_dump_load = true;
}
ImGui::Separator();
ImGui::InputText("Dump", m_dump_save_name, 12);
ImGui::EndMenu();
}

View File

@ -44,10 +44,6 @@ class OpenGlDebugGui {
void draw(const DmaStats& dma_stats);
bool should_draw_render_debug() const { return m_draw_debug; }
bool should_draw_profiler() const { return m_draw_profiler; }
bool& want_save() { return m_want_save; }
bool& want_dump_replay() { return m_want_replay; }
bool& want_dump_load() { return m_want_dump_load; }
const char* dump_name() const { return m_dump_save_name; }
const char* screenshot_name() const { return m_screenshot_save_name; }
bool should_advance_frame() { return m_frame_timer.should_advance_frame(); }
@ -74,11 +70,7 @@ class OpenGlDebugGui {
bool m_draw_frame_time = false;
bool m_draw_profiler = false;
bool m_draw_debug = false;
bool m_want_save = false;
bool m_want_replay = false;
bool m_want_dump_load = false;
bool m_want_screenshot = false;
char m_dump_save_name[256] = "dump.bin";
char m_screenshot_save_name[256] = "screenshot.png";
bool m_vsync = true;
float m_target_fps_text = 60.0;

View File

@ -195,7 +195,7 @@ void TFragment::render(DmaFollower& dma,
}
if (m_hack_scrambler) {
render_state->loader.hack_scramble_textures();
render_state->loader->hack_scramble_textures();
m_hack_scrambler = false;
}
}

View File

@ -39,7 +39,8 @@ Tfrag3::~Tfrag3() {
}
bool Tfrag3::update_load(const std::vector<tfrag3::TFragmentTreeKind>& tree_kinds,
const tfrag3::Level* lev_data) {
const tfrag3::Level* lev_data,
std::string& status_out) {
switch (m_load_state.state) {
case State::DISCARD_TREE:
discard_tree_cache();
@ -122,9 +123,8 @@ bool Tfrag3::update_load(const std::vector<tfrag3::TFragmentTreeKind>& tree_kind
glGenTextures(1, &tree_cache.time_of_day_texture);
glBindTexture(GL_TEXTURE_1D, tree_cache.time_of_day_texture);
// just fill with zeros. this lets use use the faster texsubimage later
glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA, TIME_OF_DAY_COLOR_COUNT, 0, GL_RGBA,
GL_UNSIGNED_INT_8_8_8_8, m_color_result.data());
GL_UNSIGNED_INT_8_8_8_8, nullptr);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindVertexArray(0);
@ -137,37 +137,61 @@ bool Tfrag3::update_load(const std::vector<tfrag3::TFragmentTreeKind>& tree_kind
ASSERT(time_of_day_count <= TIME_OF_DAY_COLOR_COUNT);
m_load_state.state = UPLOAD_VERTS;
m_load_state.vert_geo = 0;
m_load_state.vert_tree = 0;
m_load_state.vert = 0;
m_load_state.vert_debug_bytes = 0;
} break;
case State::UPLOAD_VERTS: {
constexpr u32 MAX_VERTS = 40000;
bool remaining = false;
for (int geom = 0; geom < GEOM_MAX; ++geom) {
for (size_t tree_idx = 0; tree_idx < lev_data->tfrag_trees[geom].size(); tree_idx++) {
const auto& tree = lev_data->tfrag_trees[geom][tree_idx];
constexpr u32 MAX_VERTS = 20000; // about 1.6 MB
u32 remaining_verts = MAX_VERTS;
// loop over geos/trees, picking up where we left off last time
while (m_load_state.vert_geo < tfrag3::TFRAG_GEOS) {
while (m_load_state.vert_tree < lev_data->tfrag_trees[m_load_state.vert_geo].size()) {
const auto& tree = lev_data->tfrag_trees[m_load_state.vert_geo][m_load_state.vert_tree];
if (std::find(tree_kinds.begin(), tree_kinds.end(), tree.kind) != tree_kinds.end()) {
u32 verts = tree.unpacked.vertices.size();
u32 start_vert = (m_load_state.vert) * MAX_VERTS;
u32 end_vert = std::min(verts, (m_load_state.vert + 1) * MAX_VERTS);
if (end_vert > start_vert) {
glBindVertexArray(m_cached_trees[geom][tree_idx].vao);
glBindBuffer(GL_ARRAY_BUFFER, m_cached_trees[geom][tree_idx].vertex_buffer);
glBufferSubData(GL_ARRAY_BUFFER, start_vert * sizeof(tfrag3::PreloadedVertex),
(end_vert - start_vert) * sizeof(tfrag3::PreloadedVertex),
tree.unpacked.vertices.data() + start_vert);
if (end_vert < verts) {
remaining = true;
}
// the number of vertices we'd need to finish the tree right now
size_t num_verts_left_in_tree = tree.unpacked.vertices.size() - m_load_state.vert;
// the last vertex in the tree
u32 last_vert = tree.unpacked.vertices.size();
bool need_more = false;
if (num_verts_left_in_tree > remaining_verts) {
need_more = true;
last_vert = m_load_state.vert + remaining_verts;
} else {
remaining_verts -= num_verts_left_in_tree;
}
glBindVertexArray(m_cached_trees[m_load_state.vert_geo][m_load_state.vert_tree].vao);
glBindBuffer(
GL_ARRAY_BUFFER,
m_cached_trees[m_load_state.vert_geo][m_load_state.vert_tree].vertex_buffer);
glBufferSubData(GL_ARRAY_BUFFER, m_load_state.vert * sizeof(tfrag3::PreloadedVertex),
(last_vert - m_load_state.vert) * sizeof(tfrag3::PreloadedVertex),
tree.unpacked.vertices.data() + m_load_state.vert);
m_load_state.vert_debug_bytes +=
(last_vert - m_load_state.vert) * sizeof(tfrag3::PreloadedVertex);
m_load_state.vert = last_vert;
if (need_more) {
status_out +=
fmt::format("TFRAG vertex add: {} kB\n", m_load_state.vert_debug_bytes / 1024);
return false;
}
}
m_load_state.vert_tree++;
m_load_state.vert = 0;
}
m_load_state.vert_geo++;
m_load_state.vert_tree = 0;
}
m_load_state.vert++;
if (!remaining) {
return true;
}
return true;
} break;
default:
ASSERT(false);
@ -182,7 +206,7 @@ bool Tfrag3::setup_for_level(const std::vector<tfrag3::TFragmentTreeKind>& tree_
// first, get the level in memory
Timer tfrag3_setup_timer;
const auto* lev_data = render_state->loader.get_tfrag3_level(level);
const auto* lev_data = render_state->loader->get_tfrag3_level(level);
if (!lev_data || (m_has_level && lev_data->load_id != m_load_id)) {
m_has_level = false;
m_textures = nullptr;
@ -201,7 +225,7 @@ bool Tfrag3::setup_for_level(const std::vector<tfrag3::TFragmentTreeKind>& tree_
m_load_state.loading = true;
m_load_state.state = State::FIRST;
}
if (update_load(tree_kinds, lev_data->level.get())) {
if (update_load(tree_kinds, lev_data->level.get(), render_state->load_status_debug)) {
m_has_level = true;
m_level_name = level;
m_load_state.loading = false;

View File

@ -50,7 +50,8 @@ class Tfrag3 {
};
bool update_load(const std::vector<tfrag3::TFragmentTreeKind>& tree_kinds,
const tfrag3::Level* lev_data);
const tfrag3::Level* lev_data,
std::string& status_out);
int lod() const { return Gfx::g_global_settings.lod_tfrag; }
@ -122,6 +123,9 @@ class Tfrag3 {
bool loading = false;
State state;
u32 vert = 0;
u32 vert_geo = 0;
u32 vert_tree = 0;
u32 vert_debug_bytes = 0;
} m_load_state;
static constexpr int MAX_TEX_PER_FRAME = 4;
};

View File

@ -14,7 +14,7 @@ Tie3::~Tie3() {
discard_tree_cache();
}
bool Tie3::update_load(const tfrag3::Level* lev_data) {
bool Tie3::update_load(const tfrag3::Level* lev_data, std::string& status_out) {
switch (m_load_state.state) {
case DISCARD_TREE:
m_wind_vectors.clear();
@ -24,37 +24,49 @@ bool Tie3::update_load(const tfrag3::Level* lev_data) {
m_trees[geo].resize(lev_data->tie_trees[geo].size());
}
m_load_state.state = INIT_NEW_TREES;
m_load_state.vert = 0;
m_load_state.vert_tree = 0;
m_load_state.vert_geo = 0;
m_load_state.time_of_day_count = 0;
m_load_state.vis_temp_len = 0;
m_load_state.max_draw = 0;
m_load_state.max_idx_per_draw = 0;
m_load_state.max_wind_idx = 0;
status_out += "TIE cleanup\n";
break;
case INIT_NEW_TREES: {
size_t time_of_day_count = 0;
size_t vis_temp_len = 0;
size_t max_draw = 0;
size_t max_idx_per_draw = 0;
u16 max_wind_idx = 0;
// set up each tree for each lod
for (int geo = 0; geo < 4; ++geo) {
for (size_t tree_idx = 0; tree_idx < lev_data->tie_trees[geo].size(); tree_idx++) {
bool should_abort = false;
for (; m_load_state.vert_geo < tfrag3::TIE_GEOS; m_load_state.vert_geo++) {
for (; m_load_state.vert_tree < lev_data->tie_trees[m_load_state.vert_geo].size();
m_load_state.vert_tree++) {
if (should_abort) {
status_out += "TIE tree add\n";
return false;
}
const auto tree_idx = m_load_state.vert_tree;
size_t idx_buffer_len = 0;
size_t wind_idx_buffer_len = 0;
const auto& tree = lev_data->tie_trees[geo][tree_idx];
max_draw = std::max(tree.static_draws.size(), max_draw);
const auto& tree = lev_data->tie_trees[m_load_state.vert_geo][tree_idx];
m_load_state.max_draw = std::max(tree.static_draws.size(), m_load_state.max_draw);
for (auto& draw : tree.static_draws) {
idx_buffer_len += draw.unpacked.vertex_index_stream.size();
max_idx_per_draw = std::max(max_idx_per_draw, draw.unpacked.vertex_index_stream.size());
m_load_state.max_idx_per_draw =
std::max(m_load_state.max_idx_per_draw, draw.unpacked.vertex_index_stream.size());
}
for (auto& draw : tree.instanced_wind_draws) {
wind_idx_buffer_len += draw.vertex_index_stream.size();
max_idx_per_draw = std::max(max_idx_per_draw, draw.vertex_index_stream.size());
m_load_state.max_idx_per_draw =
std::max(m_load_state.max_idx_per_draw, draw.vertex_index_stream.size());
}
for (auto& inst : tree.wind_instance_info) {
max_wind_idx = std::max(max_wind_idx, inst.wind_idx);
m_load_state.max_wind_idx = std::max(m_load_state.max_wind_idx, inst.wind_idx);
}
time_of_day_count = std::max(tree.colors.size(), time_of_day_count);
m_load_state.time_of_day_count =
std::max(tree.colors.size(), m_load_state.time_of_day_count);
u32 verts = tree.packed_vertices.color_indices.size();
fmt::print(" tree {} has {} verts ({} kB) and {} draws\n", tree_idx, verts,
verts * sizeof(tfrag3::PreloadedVertex) / 1024.f, tree.static_draws.size());
auto& lod_tree = m_trees.at(geo);
auto& lod_tree = m_trees.at(m_load_state.vert_geo);
glGenVertexArrays(1, &lod_tree[tree_idx].vao);
glBindVertexArray(lod_tree[tree_idx].vao);
glGenBuffers(1, &lod_tree[tree_idx].vertex_buffer);
@ -64,7 +76,8 @@ bool Tie3::update_load(const tfrag3::Level* lev_data) {
lod_tree[tree_idx].vis = &tree.bvh;
lod_tree[tree_idx].instance_info = &tree.wind_instance_info;
lod_tree[tree_idx].wind_draws = &tree.instanced_wind_draws;
vis_temp_len = std::max(vis_temp_len, tree.bvh.vis_nodes.size());
m_load_state.vis_temp_len =
std::max(m_load_state.vis_temp_len, tree.bvh.vis_nodes.size());
lod_tree[tree_idx].tod_cache = swizzle_time_of_day(tree.colors);
glBindBuffer(GL_ARRAY_BUFFER, lod_tree[tree_idx].vertex_buffer);
glBufferData(GL_ARRAY_BUFFER, verts * sizeof(tfrag3::PreloadedVertex), nullptr,
@ -73,9 +86,6 @@ bool Tie3::update_load(const tfrag3::Level* lev_data) {
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
// glBufferSubData(GL_ARRAY_BUFFER, 0, verts * sizeof(tfrag3::PreloadedVertex),
// tree.vertices.data());
glVertexAttribPointer(0, // location 0 in the shader
3, // 3 values per vert
GL_FLOAT, // floats
@ -123,60 +133,100 @@ bool Tie3::update_load(const tfrag3::Level* lev_data) {
glBufferData(GL_ELEMENT_ARRAY_BUFFER, wind_idx_buffer_len * sizeof(u32), temp.data(),
GL_STATIC_DRAW);
should_abort = true;
}
glActiveTexture(GL_TEXTURE10);
glGenTextures(1, &lod_tree[tree_idx].time_of_day_texture);
glBindTexture(GL_TEXTURE_1D, lod_tree[tree_idx].time_of_day_texture);
// just fill with zeros. this lets use use the faster texsubimage later
glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA, TIME_OF_DAY_COLOR_COUNT, 0, GL_RGBA,
GL_UNSIGNED_INT_8_8_8_8, m_color_result.data());
GL_UNSIGNED_INT_8_8_8_8, nullptr);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindVertexArray(0);
}
m_load_state.vert_tree = 0;
}
fmt::print("TIE temporary vis output size: {}\n", vis_temp_len);
m_cache.vis_temp.resize(vis_temp_len);
fmt::print("TIE max draws/tree: {}\n", max_draw);
m_cache.draw_idx_temp.resize(max_draw);
fmt::print("TIE draw with the most verts: {}\n", max_idx_per_draw);
fmt::print("wind: {}\n", max_wind_idx);
m_wind_vectors.resize(4 * max_wind_idx + 4); // 4x u32's per wind.
fmt::print("level max time of day: {}\n", time_of_day_count);
ASSERT(time_of_day_count <= TIME_OF_DAY_COLOR_COUNT);
// fmt::print("TIE temporary vis output size: {}\n", vis_temp_len);
m_cache.vis_temp.resize(m_load_state.vis_temp_len);
// fmt::print("TIE max draws/tree: {}\n", max_draw);
m_cache.draw_idx_temp.resize(m_load_state.max_draw);
// fmt::print("TIE draw with the most verts: {}\n", max_idx_per_draw);
// fmt::print("wind: {}\n", max_wind_idx);
m_wind_vectors.resize(4 * m_load_state.max_wind_idx + 4); // 4x u32's per wind.
// fmt::print("level max time of day: {}\n", time_of_day_count);
ASSERT(m_load_state.time_of_day_count <= TIME_OF_DAY_COLOR_COUNT);
}
m_load_state.state = UPLOAD_VERTS;
m_load_state.vert_geo = 0;
m_load_state.vert_tree = 0;
m_load_state.vert = 0;
m_load_state.vert_debug_bytes = 0;
break;
case State::UPLOAD_VERTS: {
constexpr u32 MAX_VERTS = 40000;
bool remaining = false;
for (int geo = 0; geo < 4; ++geo) {
for (size_t tree_idx = 0; tree_idx < lev_data->tie_trees[geo].size(); tree_idx++) {
const auto& tree = lev_data->tie_trees[geo][tree_idx];
u32 verts = tree.unpacked.vertices.size();
u32 start_vert = (m_load_state.vert) * MAX_VERTS;
u32 end_vert = std::min(verts, (m_load_state.vert + 1) * MAX_VERTS);
if (end_vert > start_vert) {
glBindVertexArray(m_trees[geo][tree_idx].vao);
glBindBuffer(GL_ARRAY_BUFFER, m_trees[geo][tree_idx].vertex_buffer);
glBufferSubData(GL_ARRAY_BUFFER, start_vert * sizeof(tfrag3::PreloadedVertex),
(end_vert - start_vert) * sizeof(tfrag3::PreloadedVertex),
tree.unpacked.vertices.data() + start_vert);
if (end_vert < verts) {
remaining = true;
constexpr u32 CHUNK_SIZE = 30000;
Timer timer;
u32 uploaded_bytes = 0;
// loop over geos/trees, picking up where we left off last time
while (true) {
const auto& tree = lev_data->tie_trees[m_load_state.vert_geo][m_load_state.vert_tree];
u32 end_vert_in_tree = tree.unpacked.vertices.size();
// the number of vertices we'd need to finish the tree right now
size_t num_verts_left_in_tree = end_vert_in_tree - m_load_state.vert;
size_t start_vert_for_chunk;
size_t end_vert_for_chunk;
bool complete_tree;
if (num_verts_left_in_tree > CHUNK_SIZE) {
complete_tree = false;
// should only do partial
start_vert_for_chunk = m_load_state.vert;
end_vert_for_chunk = start_vert_for_chunk + CHUNK_SIZE;
m_load_state.vert += CHUNK_SIZE;
} else {
// should do all!
start_vert_for_chunk = m_load_state.vert;
end_vert_for_chunk = end_vert_in_tree;
complete_tree = true;
}
glBindVertexArray(m_trees[m_load_state.vert_geo][m_load_state.vert_tree].vao);
glBindBuffer(GL_ARRAY_BUFFER,
m_trees[m_load_state.vert_geo][m_load_state.vert_tree].vertex_buffer);
u32 upload_size =
(end_vert_for_chunk - start_vert_for_chunk) * sizeof(tfrag3::PreloadedVertex);
glBufferSubData(GL_ARRAY_BUFFER, start_vert_for_chunk * sizeof(tfrag3::PreloadedVertex),
upload_size, tree.unpacked.vertices.data() + start_vert_for_chunk);
uploaded_bytes += upload_size;
if (complete_tree) {
// and move on to next tree
m_load_state.vert = 0;
m_load_state.vert_tree++;
if (m_load_state.vert_tree >= lev_data->tie_trees[m_load_state.vert_geo].size()) {
m_load_state.vert_tree = 0;
m_load_state.vert_geo++;
if (m_load_state.vert_geo >= tfrag3::TIE_GEOS) {
return true;
}
}
}
if (timer.getMs() > Loader::TIE_LOAD_BUDGET || (uploaded_bytes / 1024) > 2048) {
status_out +=
fmt::format("TIE vertex {:6d} kB, {:3.2f}ms\n", uploaded_bytes / 1024, timer.getMs());
return false;
}
}
m_load_state.vert++;
if (!remaining) {
return true;
}
return true;
} break;
default:
@ -193,7 +243,7 @@ bool Tie3::setup_for_level(const std::string& level, SharedRenderState* render_s
// make sure we have the level data.
// TODO: right now this will wait to load from disk and unpack it.
Timer tfrag3_setup_timer;
auto lev_data = render_state->loader.get_tfrag3_level(level);
auto lev_data = render_state->loader->get_tfrag3_level(level);
if (!lev_data || (m_has_level && lev_data->load_id != m_load_id)) {
m_has_level = false;
m_textures = nullptr;
@ -211,7 +261,7 @@ bool Tie3::setup_for_level(const std::string& level, SharedRenderState* render_s
m_load_state.loading = true;
m_load_state.state = State::FIRST;
}
if (update_load(lev_data->level.get())) {
if (update_load(lev_data->level.get(), render_state->load_status_debug)) {
m_has_level = true;
m_level_name = level;
m_load_state.loading = false;

View File

@ -40,7 +40,7 @@ class Tie3 : public BucketRenderer {
int lod() const { return Gfx::g_global_settings.lod_tie; }
private:
bool update_load(const tfrag3::Level* lev_data);
bool update_load(const tfrag3::Level* lev_data, std::string& status_out);
void discard_tree_cache();
void render_tree_wind(int idx,
int geom,
@ -122,13 +122,25 @@ class Tie3 : public BucketRenderer {
DISCARD_TREE = 0,
INIT_NEW_TREES = 1,
UPLOAD_VERTS = 2,
INIT_TEX = 4,
UPLOAD_WIND_INDEX = 3,
};
struct {
bool loading = false;
State state;
u32 tex = 0;
u32 vert_geo = 0;
u32 vert_tree = 0;
u32 vert = 0;
u32 vert_debug_bytes = 0;
size_t time_of_day_count = 0;
size_t vis_temp_len = 0;
size_t max_draw = 0;
size_t max_idx_per_draw = 0;
u16 max_wind_idx = 0;
} m_load_state;
};

View File

@ -50,26 +50,22 @@ struct GraphicsData {
// texture pool
std::shared_ptr<TexturePool> texture_pool;
std::shared_ptr<Loader> loader;
// temporary opengl renderer
OpenGLRenderer ogl_renderer;
OpenGlDebugGui debug_gui;
Serializer loaded_dump;
FrameLimiter frame_limiter;
Timer engine_timer;
double last_engine_time = 1. / 60.;
void serialize(Serializer& ser) {
dma_copier.serialize_last_result(ser);
ogl_renderer.serialize(ser);
}
GraphicsData()
: dma_copier(EE_MAIN_MEM_SIZE),
texture_pool(std::make_shared<TexturePool>()),
ogl_renderer(texture_pool) {}
loader(std::make_shared<Loader>()),
ogl_renderer(texture_pool, loader) {}
};
std::unique_ptr<GraphicsData> g_gfx_data;
@ -220,23 +216,6 @@ std::string make_output_file_name(const std::string& file_name) {
}
} // namespace
void make_gfx_dump() {
Timer ser_timer;
Serializer ser;
// save the dma chain and renderer state
g_gfx_data->serialize(ser);
auto result = ser.get_save_result();
Timer compression_timer;
auto compressed = compression::compress_zstd(result.first, result.second);
lg::info("Serialized graphics state in {:.1f} ms, {:.3f} MB, compressed {:.3f} MB {:.1f} ms",
ser_timer.getMs(), ((double)result.second) / (1 << 20),
((double)compressed.size() / (1 << 20)), compression_timer.getMs());
file_util::write_binary_file(make_output_file_name(g_gfx_data->debug_gui.dump_name()),
compressed.data(), compressed.size());
}
void render_game_frame(int width, int height, int lbox_width, int lbox_height) {
// wait for a copied chain.
bool got_chain = false;
@ -249,12 +228,6 @@ void render_game_frame(int width, int height, int lbox_width, int lbox_height) {
}
// render that chain.
if (got_chain) {
// we want to serialize before rendering
if (g_gfx_data->debug_gui.want_save()) {
make_gfx_dump();
g_gfx_data->debug_gui.want_save() = false;
}
g_gfx_data->frame_idx_of_input_data = g_gfx_data->frame_idx;
RenderOptions options;
options.window_height_px = height;
@ -263,7 +236,6 @@ void render_game_frame(int width, int height, int lbox_width, int lbox_height) {
options.lbox_width_px = lbox_width;
options.draw_render_debug_window = g_gfx_data->debug_gui.should_draw_render_debug();
options.draw_profiler_window = g_gfx_data->debug_gui.should_draw_profiler();
options.playing_from_dump = false;
options.save_screenshot = g_gfx_data->debug_gui.get_screenshot_flag();
options.draw_small_profiler_window = g_gfx_data->debug_gui.small_profiler;
if (options.save_screenshot) {
@ -290,42 +262,6 @@ void render_game_frame(int width, int height, int lbox_width, int lbox_height) {
}
}
void render_dump_frame(int width, int height, int lbox_width, int lbox_height) {
Timer deser_timer;
if (g_gfx_data->debug_gui.want_dump_load()) {
auto data =
file_util::read_binary_file(make_output_file_name(g_gfx_data->debug_gui.dump_name()));
auto decompressed = compression::decompress_zstd(data.data(), data.size());
g_gfx_data->loaded_dump = Serializer(decompressed.data(), decompressed.size());
}
g_gfx_data->loaded_dump.reset_load();
g_gfx_data->serialize(g_gfx_data->loaded_dump);
if (g_gfx_data->debug_gui.want_dump_load()) {
lg::info("Loaded and deserialized graphics state in {:.1f} ms, {:.3f} MB", deser_timer.getMs(),
((double)g_gfx_data->loaded_dump.data_size()) / (1 << 20));
}
g_gfx_data->debug_gui.want_dump_load() = false;
auto& chain = g_gfx_data->dma_copier.get_last_result();
RenderOptions options;
options.window_height_px = height;
options.window_width_px = width;
options.lbox_height_px = lbox_height;
options.lbox_width_px = lbox_width;
options.draw_render_debug_window = g_gfx_data->debug_gui.should_draw_render_debug();
options.draw_profiler_window = g_gfx_data->debug_gui.should_draw_profiler();
options.playing_from_dump = true;
options.save_screenshot = g_gfx_data->debug_gui.get_screenshot_flag();
if (options.save_screenshot) {
options.screenshot_path = make_output_file_name(g_gfx_data->debug_gui.screenshot_name());
}
g_gfx_data->ogl_renderer.render(DmaFollower(chain.data.data(), chain.start_offset), options);
}
static void gl_display_position(GfxDisplay* display, int* x, int* y) {
glfwGetWindowPos(display->window_glfw, x, y);
}
@ -419,12 +355,7 @@ static void gl_render_display(GfxDisplay* display) {
#endif
// render game!
if (g_gfx_data->debug_gui.want_dump_replay()) {
// hack: no letterbox for dump frames, for now.
render_dump_frame(fbuf_w, fbuf_h, 0, 0);
} else if (g_gfx_data->debug_gui.should_advance_frame()) {
render_game_frame(width, height, lbox_w, lbox_h);
}
render_game_frame(width, height, lbox_w, lbox_h);
if (g_gfx_data->debug_gui.should_gl_finish()) {
glFinish();
@ -457,7 +388,7 @@ static void gl_render_display(GfxDisplay* display) {
}
// toggle even odd and wake up engine waiting on vsync.
if (!g_gfx_data->debug_gui.want_dump_replay()) {
{
std::unique_lock<std::mutex> lock(g_gfx_data->sync_mutex);
g_gfx_data->frame_idx++;
g_gfx_data->sync_cv.notify_all();
@ -532,7 +463,7 @@ void gl_send_chain(const void* data, u32 offset) {
void gl_texture_upload_now(const u8* tpage, int mode, u32 s7_ptr) {
// block
if (g_gfx_data && !g_gfx_data->debug_gui.want_dump_replay()) {
if (g_gfx_data) {
// just pass it to the texture pool.
// the texture pool will take care of locking.
// we don't want to lock here for the entire duration of the conversion.
@ -541,7 +472,7 @@ void gl_texture_upload_now(const u8* tpage, int mode, u32 s7_ptr) {
}
void gl_texture_relocate(u32 destination, u32 source, u32 format) {
if (g_gfx_data && !g_gfx_data->debug_gui.want_dump_replay()) {
if (g_gfx_data) {
g_gfx_data->texture_pool->relocate(destination, source, format);
}
}
@ -550,6 +481,10 @@ void gl_poll_events() {
glfwPollEvents();
}
void gl_set_levels(const std::vector<std::string>& levels) {
g_gfx_data->loader->set_want_levels(levels);
}
const GfxRendererModule moduleOpenGL = {
gl_init, // init
gl_make_main_display, // make_main_display
@ -567,6 +502,7 @@ const GfxRendererModule moduleOpenGL = {
gl_texture_upload_now, // texture_upload_now
gl_texture_relocate, // texture_relocate
gl_poll_events, // poll_events
gl_set_levels, // set_levels
GfxPipeline::OpenGL, // pipeline
"OpenGL 4.3" // name
};

View File

@ -1,23 +1,15 @@
#include <regex>
#include <algorithm>
#include "TexturePool.h"
#include "third-party/fmt/core.h"
#include "third-party/imgui/imgui.h"
#include "common/util/FileUtil.h"
#include "common/util/Timer.h"
#include "common/log/log.h"
#include "game/graphics/pipelines/opengl.h"
#include "common/util/Assert.h"
////////////////////////////////
// Extraction of textures
// Note: this is intended to be temporary until we have a better system.
// this simply converts the PS2 format textures loaded by the game, then puts them into the PC
// port texture pool.
// constexpr bool dump_textures_to_file = false;
namespace {
const char empty_string[] = "";
const char* goal_string(u32 ptr, const u8* memory_base) {
@ -35,232 +27,142 @@ std::string GoalTexturePage::print() const {
segment[2].size, segment[2].dest);
}
void TextureRecord::serialize(Serializer& ser) {
if (only_on_gpu) {
ASSERT(on_gpu);
if (ser.is_saving()) {
// we should download the texture and save it.
data.resize(w * h * 4);
glBindTexture(GL_TEXTURE_2D, gpu_texture);
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, data.data());
}
}
ser.from_str(&page_name);
ser.from_str(&name);
ser.from_ptr(&mip_level);
ser.from_ptr(&psm);
ser.from_ptr(&cpsm);
ser.from_ptr(&w);
ser.from_ptr(&h);
ser.from_ptr(&data_segment);
ser.from_ptr(&on_gpu);
ser.from_ptr(&do_gc);
ser.from_ptr(&only_on_gpu);
ser.from_ptr(&gpu_texture);
ser.from_ptr(&dest);
ser.from_pod_vector(&data);
if (ser.is_loading()) {
gpu_texture = -1;
}
}
void TextureData::serialize(Serializer& ser) {
if (ser.is_saving()) {
if (normal_texture) {
ser.save<u8>(1); // has it.
normal_texture->serialize(ser);
} else {
ser.save<u8>(0);
}
if (mt4hh_texture) {
ser.save<u8>(1); // has it.
mt4hh_texture->serialize(ser);
} else {
ser.save<u8>(0);
}
} else {
u8 has_normal = ser.load<u8>();
if (has_normal) {
normal_texture = std::make_shared<TextureRecord>();
normal_texture->serialize(ser);
normal_texture->on_gpu = false;
normal_texture->do_gc = true;
} else {
normal_texture.reset();
}
u8 has_mt4 = ser.load<u8>();
if (has_mt4) {
mt4hh_texture = std::make_shared<TextureRecord>();
mt4hh_texture->serialize(ser);
mt4hh_texture->on_gpu = false;
mt4hh_texture->do_gc = true;
} else {
mt4hh_texture.reset();
}
}
}
void TexturePool::serialize(Serializer& ser) {
m_tex_converter.serialize(ser);
if (ser.is_loading()) {
remove_garbage_textures();
unload_all_textures();
}
for (auto& tex : m_textures) {
tex.serialize(ser);
}
}
void TexturePool::unload_all_textures() {
for (auto& tex : m_textures) {
if (tex.normal_texture && tex.normal_texture->on_gpu) {
tex.normal_texture->unload_from_gpu();
}
if (tex.mt4hh_texture && tex.mt4hh_texture->on_gpu) {
tex.mt4hh_texture->unload_from_gpu();
}
}
}
void TextureRecord::unload_from_gpu() {
ASSERT(on_gpu);
GLuint tex_id = gpu_texture;
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);
glDeleteTextures(1, &tex_id);
on_gpu = false;
gpu_texture = -1;
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;
}
std::vector<std::shared_ptr<TextureRecord>> TexturePool::convert_textures(const u8* tpage,
int mode,
const u8* memory_base,
u32 s7_ptr) {
Timer timer;
std::vector<std::shared_ptr<TextureRecord>> result;
GpuTexture* TexturePool::give_texture(const TextureInput& in) {
const auto& it = m_loaded_textures.find(in.name);
if (it == m_loaded_textures.end()) {
// nothing references this texture yet.
GpuTexture gtex;
gtex.page_name = in.page_name;
gtex.name = in.name;
gtex.w = in.w;
gtex.h = in.h;
gtex.is_common = in.common;
gtex.gpu_textures = {{in.gpu_texture, in.src_data}};
gtex.combo_id = in.combo_id;
gtex.is_placeholder = false;
bool dump_textures_to_file = false;
// 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};
u32 sizes[3] = {texture_page.segment[0].size, texture_page.segment[1].size,
texture_page.segment[2].size};
if (mode == -1) {
// I don't really understand what's going on here with the size.
// the sizes given aren't the actual sizes in memory, so if you just use that, you get the
// wrong answer. I solved this in the decompiler by using the size of the actual data, but we
// don't really have that here.
u32 size = ((sizes[0] + sizes[1] + sizes[2] + 4096) / 256) * 256;
m_tex_converter.upload(memory_base + texture_page.segment[0].block_data_ptr,
texture_page.segment[0].dest, size);
} else if (mode == 2) {
// dump_textures_to_file = true;
has_segment[0] = false;
has_segment[1] = false;
u32 size = ((sizes[2] + 255) / 256) * 256;
// dest is in 4-byte vram words
m_tex_converter.upload(memory_base + texture_page.segment[2].block_data_ptr,
texture_page.segment[2].dest, size);
} else if (mode == -2) {
has_segment[2] = false;
// I don't really understand what's going on here with the size.
// the selector texture the hud page will be missing the clut unless I make this bigger.
u32 size = ((sizes[0] + sizes[1] + 2047) / 256) * 256;
m_tex_converter.upload(memory_base + texture_page.segment[0].block_data_ptr,
texture_page.segment[0].dest, size);
} else if (mode == 0) {
has_segment[1] = false;
has_segment[2] = false;
u32 size = ((sizes[0] + 255) / 256) * 256;
// dest is in 4-byte vram words
m_tex_converter.upload(memory_base + texture_page.segment[0].block_data_ptr,
texture_page.segment[0].dest, size);
return &m_loaded_textures.insert({in.name, gtex}).first->second;
} else {
// no reason to skip this, other than
lg::error("TexturePool skipping upload now with mode {}.", mode);
return {};
if (!it->second.is_placeholder) {
fmt::print(
"[tex2] loader providing {}, but we already have an entry for it {} common? {} mine "
"{}x{} 0x{:x} new {}x{} 0x{:x}.\n",
in.name, it->second.name, it->second.is_common, in.w, in.h, in.combo_id, it->second.w,
it->second.h, it->second.combo_id);
ASSERT(!it->second.gpu_textures.empty());
} else {
ASSERT(it->second.gpu_textures.empty());
}
it->second.is_placeholder = false;
it->second.page_name = in.page_name;
it->second.name = in.name;
it->second.w = in.w;
it->second.h = in.h;
it->second.gpu_textures.push_back({in.gpu_texture, in.src_data});
it->second.is_common = in.common;
it->second.combo_id = in.combo_id;
refresh_links(it->second);
return &it->second;
}
}
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::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;
}
// 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)) {
// 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)]) {
u32 ww = tex.w >> mip_idx;
u32 hh = tex.h >> mip_idx;
u32 size_bytes = ww * hh * 4;
auto texture_record = std::make_shared<TextureRecord>();
texture_record->page_name = goal_string(texture_page.name_ptr, memory_base);
texture_record->name = goal_string(tex.name_ptr, memory_base);
texture_record->mip_level = mip_idx;
texture_record->w = ww;
texture_record->h = hh;
texture_record->data_segment = tex.segment_of_mip(mip_idx);
texture_record->data.resize(size_bytes);
texture_record->psm = tex.psm;
texture_record->cpsm = tex.clutpsm;
texture_record->dest = tex.dest[mip_idx];
m_tex_converter.download_rgba8888(texture_record->data.data(), tex.dest[mip_idx],
tex.width[mip_idx], ww, hh, tex.psm, tex.clutpsm,
tex.clut_dest, size_bytes);
u8 max_a_zero = 0;
u8 min_a_zero = 255;
u8 max_a_nonzero = 0;
u8 min_a_nonzero = 255;
for (u32 i = 0; i < ww * hh; i++) {
u8 r = texture_record->data[i * 4 + 0];
u8 g = texture_record->data[i * 4 + 1];
u8 b = texture_record->data[i * 4 + 2];
u8 a = texture_record->data[i * 4 + 3];
if (r || g || b) {
max_a_nonzero = std::max(max_a_nonzero, a);
min_a_nonzero = std::min(min_a_nonzero, a);
} else {
max_a_zero = std::max(max_a_zero, a);
min_a_zero = std::min(min_a_zero, a);
}
}
// Debug output.
if (dump_textures_to_file) {
const char* tpage_name = goal_string(texture_page.name_ptr, memory_base);
const char* tex_name = goal_string(tex.name_ptr, memory_base);
file_util::create_dir_if_needed(
file_util::get_file_path({"debug_out", "textures", tpage_name}));
file_util::write_rgba_png(
fmt::format(
file_util::get_file_path({"debug_out", "textures", tpage_name, "{}-{}-{}.png"}),
tex_idx, tex_name, mip_idx),
texture_record->data.data(), ww, hh);
}
result.push_back(std::move(texture_record));
}
for (auto slot : texture.mt4hh_slots) {
for (auto& tex : m_mt4hh_textures) {
if (tex.slot == slot) {
tex.ref.gpu_texture = tex_to_use;
}
} else {
// texture was #f, skip it.
}
}
}
fmt::print("upload now took {:.2f} ms\n", timer.getMs());
return result;
void TexturePool::unload_texture(const std::string& name, u64 id) {
auto& tex = m_loaded_textures.at(name);
if (tex.is_common) {
ASSERT(false);
return;
}
if (tex.is_placeholder) {
fmt::print("trying to unload something that was already placholdered: {} {}\n", name,
tex.gpu_textures.size());
}
ASSERT(!tex.is_placeholder);
auto it = std::find_if(tex.gpu_textures.begin(), tex.gpu_textures.end(),
[&](const auto& a) { return a.gl == 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);
}
/*!
@ -277,175 +179,164 @@ std::vector<std::shared_ptr<TextureRecord>> TexturePool::convert_textures(const
* multiple frames.
*/
void TexturePool::handle_upload_now(const u8* tpage, int mode, const u8* memory_base, u32 s7_ptr) {
auto textures = convert_textures(tpage, mode, memory_base, s7_ptr);
for (auto& tex : textures) {
set_texture(tex->dest, tex);
}
}
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));
/*!
* Store a texture in the pool. Location is specified like TBP.
*/
void TexturePool::set_texture(u32 location, std::shared_ptr<TextureRecord> record) {
if (record->psm == 44) {
if (m_textures.at(location).mt4hh_texture) {
if (record->do_gc && m_textures.at(location).mt4hh_texture != record) {
m_garbage_textures.push_back(std::move(m_textures[location].mt4hh_texture));
}
}
m_textures[location].mt4hh_texture = std::move(record);
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 {
if (m_textures.at(location).normal_texture) {
if (record->do_gc && m_textures.at(location).normal_texture != record) {
m_garbage_textures.push_back(std::move(m_textures[location].normal_texture));
// fmt::print("replace add to garbage list {}\n", m_garbage_textures.back()->name);
// 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)) {
// 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)]) {
auto name = std::string(goal_string(texture_page.name_ptr, memory_base)) +
goal_string(tex.name_ptr, memory_base);
auto& slot = m_textures[tex.dest[mip_idx]];
if (slot.source) {
if (slot.source->name == name) {
// 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(name, tex.dest[mip_idx]);
ASSERT(slot.gpu_texture != (u64)-1);
}
} else {
slot.source = get_gpu_texture_for_slot(name, tex.dest[mip_idx]);
ASSERT(slot.gpu_texture != (u64)-1);
}
}
}
} else {
// texture was #f, skip it.
}
m_textures[location].normal_texture = std::move(record);
}
}
/*!
* Move a texture.
*/
void TexturePool::relocate(u32 destination, u32 source, u32 format) {
auto& src = m_textures.at(source).normal_texture;
std::unique_lock<std::mutex> lk(m_mutex);
GpuTexture* src = lookup_gpu_texture(source);
ASSERT(src);
if (format == 44) {
m_textures.at(destination).mt4hh_texture = std::move(src);
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 {
m_textures.at(destination).normal_texture = std::move(src);
move_existing_to_vram(src, destination);
}
}
GpuTexture* TexturePool::get_gpu_texture_for_slot(const std::string& name, u32 slot) {
auto it = m_loaded_textures.find(name);
if (it == m_loaded_textures.end()) {
GpuTexture placeholder;
placeholder.name = name;
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 &r.first->second;
} else {
auto result = &it->second;
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 {};
}
TexturePool::TexturePool() {
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::Text("GC %d on GPU %d", m_most_recent_gc_count, m_most_recent_gc_count_gpu);
ImGui::InputText("texture search", m_regex_input, sizeof(m_regex_input));
std::regex regex(m_regex_input[0] ? m_regex_input : ".*");
for (auto& record : m_textures) {
if (record.normal_texture) {
total_textures++;
auto& tex = *record.normal_texture;
if (std::regex_search(tex.name, regex)) {
for (size_t i = 0; i < m_textures.size(); i++) {
auto& record = m_textures[i];
total_textures++;
if (record.source) {
if (std::regex_search(record.source->name, regex)) {
ImGui::PushID(id++);
draw_debug_for_tex(tex.name, tex);
draw_debug_for_tex(record.source->name, record.source, i);
ImGui::PopID();
total_displayed_textures++;
}
if (tex.on_gpu) {
total_vram_bytes += tex.w * tex.h * 4; // todo, if we support other formats
total_uploaded_textures++;
}
}
if (record.mt4hh_texture) {
total_textures++;
auto& tex = *record.mt4hh_texture;
if (std::regex_search(tex.name, regex)) {
ImGui::PushID(id++);
draw_debug_for_tex(tex.name, tex);
ImGui::PopID();
total_displayed_textures++;
}
if (tex.on_gpu) {
total_vram_bytes += tex.w * tex.h * 4; // todo, if we support other formats
total_uploaded_textures++;
}
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, TextureRecord& tex) {
if (tex.on_gpu) {
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->slots.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(name.c_str())) {
ImGui::Text("P: %s sz: %d x %d mip %d GPU? %d psm %d cpsm %d dest %d", tex.page_name.c_str(),
tex.w, tex.h, tex.mip_level, tex.on_gpu, tex.psm, tex.cpsm, tex.dest);
if (tex.on_gpu) {
ImGui::Image((void*)tex.gpu_texture, ImVec2(tex.w, tex.h));
if (ImGui::TreeNode(fmt::format("{} {}", name, slot).c_str())) {
ImGui::Text("P: %s sz: %d x %d", tex->page_name.c_str(), tex->w, tex->h);
if (!tex->is_placeholder) {
ImGui::Image((void*)tex->gpu_textures.at(0).gl, ImVec2(tex->w, tex->h));
} else {
if (ImGui::Button("Upload to GPU!")) {
upload_to_gpu(&tex);
}
ImGui::Text("PLACEHOLDER");
}
ImGui::TreePop();
ImGui::Separator();
}
if (tex.on_gpu) {
ImGui::PopStyleColor();
}
}
void TexturePool::upload_to_gpu(TextureRecord* tex) {
ASSERT(!tex->on_gpu);
GLuint tex_id;
glGenTextures(1, &tex_id);
tex->gpu_texture = 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, tex->w, tex->h, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV,
tex->data.data());
glBindTexture(GL_TEXTURE_2D, 0);
// we have to set these, imgui won't do it automatically
glBindTexture(GL_TEXTURE_2D, tex->gpu_texture);
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);
tex->on_gpu = true;
}
void TexturePool::remove_garbage_textures() {
m_most_recent_gc_count = m_garbage_textures.size();
m_most_recent_gc_count_gpu = 0;
for (auto& t : m_garbage_textures) {
if (t->on_gpu) {
m_most_recent_gc_count_gpu++;
fmt::print("GC {}\n", t->name);
t->unload_from_gpu();
}
}
m_garbage_textures.clear();
}
void TexturePool::discard(std::shared_ptr<TextureRecord> tex) {
ASSERT(!tex->do_gc);
fmt::print("discard {}\n", tex->name);
m_garbage_textures.push_back(tex);
}
TextureRecord* TexturePool::get_random_texture() {
u32 idx = 8;
for (u32 i = 0; i < m_textures.size(); i++) {
if (m_textures.at((i + idx) % m_textures.size()).normal_texture) {
return m_textures.at((i + idx) % m_textures.size()).normal_texture.get();
}
}
return nullptr;
}
ImGui::PopStyleColor();
}

View File

@ -3,72 +3,152 @@
#include <array>
#include <memory>
#include <string>
#include <mutex>
#include <optional>
#include <unordered_map>
#include "common/common_types.h"
#include "game/graphics/texture/TextureConverter.h"
#include "common/util/Serializer.h"
// Converting textures happens when textures are uploaded by the game.
// Uploading textures to the gpu and creating samplers is done lazily, as needed.
// verify all texture lookups.
// will make texture lookups slower and likely caused dropped frames when loading
constexpr bool EXTRA_TEX_DEBUG = false;
// Each sampler
struct TextureSampler {
u64 sampler_object = -1; // the opengl sampler
u64 handle = -1; // the handle used for bindless textures.
bool created = false; // lazily created as needed, by default we don't make them
};
// sky, cloud textures
constexpr int SKY_TEXTURE_VRAM_ADDRS[2] = {8064, 8096};
// Each texture in our pool has a record:
struct TextureRecord {
std::string page_name; // the page we belong to (from game info)
std::string name; // our name (from game info)
u8 mip_level; // which mip we are
u8 psm = -1; // format in the game
u8 cpsm = -1; // clut format in the game
u16 w, h; // our dimensions
u8 data_segment; // which segment we came from in the texture page
bool on_gpu = false; // if we are uploaded to the GPU
// garbage collection settings.
// by default, do_gc is set, and the pool will take care of freeing textures.
// when a texture is uploaded on top of a texture (in PS2 VRAM), the texture will be unloaded from
// the GPU. Unless somebody has another instance of the shared_ptr to this texture, this structure
// (including converted texture data) will be discarded.
// In some cases, this is not desirable because the game may toggle between two different textures
// in VRAM, and we don't want to reconvert every time. The TextureUploadHandler will implement its
// own caching to help with this. To manage textures yourself, you should:
// - keep around a shared_ptr to the TextureRecord (so it doesn't get deleted when it's out of PS2
// VRAM).
// - set do_gc to false (to keep texture in GPU memory when replaced).
// In this case, you must use the discard function to remove the texture from the GPU, if you
// really want to get rid of it.
bool do_gc = true;
// The texture data. In some cases, we keep textures only on the GPU (for example, the result of a
// render to texture). In these, data is not populated, but you must set only_on_gpu = true. When
// saving graphics state, the texture will be dumped from the GPU and saved to the file so it is
// possible to restore.
bool only_on_gpu = false;
std::vector<u8> data;
// if we're on the gpu, our OpenGL texture
u64 gpu_texture = 0;
// our VRAM address.
u32 dest = -1;
void unload_from_gpu();
void serialize(Serializer& ser);
};
/*!
* PC Port Texture System
*
* The main goal of this texture system is to support fast lookup textures by VRAM address
* (sometimes called texture base pointer or TBP). The lookup ends up being a single read from
* an array - no pointer chasing required.
*
* The TIE/TFRAG background renderers use their own more efficient system for this.
* This is only used for renderers that interpret GIF data (sky, eyes, generic, merc, direct,
* sprite).
*
* Some additional challenges:
* - Some textures are generated by rendering to a texture (eye, sky)
* - The game may try to render things before their textures have been loaded. This is a "bug" in
* the original game, but can't be seen most of the time because the objects are often hidden.
* - We preconvert PS2-format textures and store them in the FR3 level asset files. But the game may
* try to use the textures before the PC port has finished loading them.
* - The game may copy textures from one location in VRAM to another
* - The game may store two texture on top of each other in some formats (only the font). The PS2's
* texture formats allow you to do this if you use the right pair formats.
* - The same texture may appear in multiple levels, both of which can be loaded at the same time.
* The two levels can unload in either order, and the remaining level should be able to use the
* texture.
* - Some renderers need to access the actual texture data on the CPU.
* - We don't want to load all the textures into VRAM at the same time.
*
* But, we have a few assumptions we make to simplify things:
* - Two textures with the same "combined name" are always identical data. (This is verified by the
* decompiler). So we can use the name as an ID for the texture.
* - The game will remove all references to textures that belong to an unloaded level, so once the
* level is gone, we can forget its textures.
* - The number of times a texture is duplicated (both in VRAM, and in loaded levels) is small
*
* Unlike the first version of the texture system, our approach is to load all the textures to
* the GPU during loading.
*
* This approach has three layers:
* - A VRAM entry (TextureReference), which refers to a GpuTexture
* - A GpuTexture, which represents an in-game texture, and refers to all loaded instances of it
* - Actual texture data
*
* Note that the VRAM entries store the GLuint for the actual texture reference inline, so texture
* lookups during drawing are very fast. The time to set up and maintain all these links only
* happens during loading, and it's insignificant compared to reading from the hard drive or
* unpacking/uploading meshes.
*
* The loader will inform us when things are added/removed.
* The game will inform us when it uploads to VRAM
*/
/*!
* The lowest level reference to texture data.
*/
struct TextureData {
std::shared_ptr<TextureRecord> normal_texture;
std::shared_ptr<TextureRecord> mt4hh_texture;
void serialize(Serializer& ser);
u64 gl = -1; // the OpenGL texture ID
const u8* data = nullptr; // pointer to texture data (owned by the loader)
};
/*!
* This represents a unique in-game texture, including any instances of it that are loaded.
* It's possible for there to be 0 instances of the texture loaded yet.
*/
struct GpuTexture {
std::string page_name;
std::string name;
// all the currently loaded copies of this texture
std::vector<TextureData> gpu_textures;
// the vram addresses that contain this texture
std::vector<u32> slots;
// the vram address that contain this texture, stored in mt4hh format
std::vector<u32> mt4hh_slots;
// our "combo id", containing the tpage and texture ID
u32 combo_id = -1;
// texture dimensions
u16 w, h;
// set to true if we have no copies of the texture, and we should use a placeholder
bool is_placeholder = false;
// set to true if we are part of the textures in GAME.CGO that are always loaded.
// for these textures, the pool can assume that we are never a placeholder.
bool is_common = false;
// the size of our data, in bytes
u32 data_size() const { return 4 * w * h; }
// get a pointer to our data, or nullptr if we are a placeholder.
const u8* get_data_ptr() const {
if (is_placeholder) {
return nullptr;
} else {
return gpu_textures.at(0).data;
}
}
// add or remove a VRAM reference to this texture
void remove_slot(u32 slot);
void add_slot(u32 slot);
};
/*!
* A VRAM slot.
* If the source is nullptr, the game has not loaded anything to this address.
* If the game has loaded something, but the loader hasn't loaded the converted texture, the
* source will be non-null and the gpu_texture will be a placeholder that is safe to use.
*/
struct TextureVRAMReference {
u64 gpu_texture = -1; // the OpenGL texture to use when rendering.
GpuTexture* source = nullptr;
};
/*!
* A texture provided by the loader.
*/
struct TextureInput {
std::string page_name;
std::string name;
u64 gpu_texture = -1;
bool common = false;
u32 combo_id = -1;
const u8* src_data;
u16 w, h;
};
/*!
* The in-game texture type.
*/
struct GoalTexture {
s16 w;
s16 h;
@ -98,6 +178,9 @@ static_assert(sizeof(GoalTexture) == 60, "GoalTexture size");
static_assert(offsetof(GoalTexture, clutpsm) == 8);
static_assert(offsetof(GoalTexture, clut_dest) == 24);
/*!
* The in-game texture page type.
*/
struct GoalTexturePage {
struct Seg {
u32 block_data_ptr;
@ -131,58 +214,93 @@ struct GoalTexturePage {
}
};
/*!
* The main texture pool.
* Moving textures around should be done with locking. (the game EE thread and the loader run
* simultaneously)
*
* Lookups can be done without locking.
* It is safe for renderers to use textures without worrying about locking - OpenGL textures
* themselves are only removed from the rendering thread.
*
* There could be races with the game doing texture uploads and doing texture lookups, but these
* races are harmless. If there's an actual in-game race condition, the exact texture you get may be
* unknown, but you will get a valid texture.
*
* (note that the above property is only true because we never make a VRAM slot invalid after
* it has been loaded once)
*/
class TexturePool {
public:
TexturePool();
void handle_upload_now(const u8* tpage, int mode, const u8* memory_base, u32 s7_ptr);
GpuTexture* give_texture(const TextureInput& in);
GpuTexture* give_texture_and_load_to_vram(const TextureInput& in, u32 vram_slot);
void unload_texture(const std::string& name, u64 id);
std::vector<std::shared_ptr<TextureRecord>> convert_textures(const u8* tpage,
int mode,
const u8* memory_base,
u32 s7_ptr);
/*!
* Look up an OpenGL texture by vram address. Return std::nullopt if the game hasn't loaded
* anything to this address.
*/
std::optional<u64> lookup(u32 location) {
auto& t = m_textures[location];
if (t.source) {
if constexpr (EXTRA_TEX_DEBUG) {
if (t.source->is_placeholder) {
ASSERT(t.gpu_texture == m_placeholder_texture_id);
} else {
bool fnd = false;
for (auto& tt : t.source->gpu_textures) {
if (tt.gl == t.gpu_texture) {
fnd = true;
break;
}
}
ASSERT(fnd);
}
}
return t.gpu_texture;
} else {
return {};
}
}
void set_texture(u32 location, std::shared_ptr<TextureRecord> record);
/*!
* Look up a game texture by VRAM address. Will be nullptr if the game hasn't loaded anything to
* this address.
*
* You should probably not use this to lookup textures that could be uploaded with
* handle_upload_now.
*/
GpuTexture* lookup_gpu_texture(u32 location) { return m_textures[location].source; }
std::optional<u64> lookup_mt4hh(u32 location);
u64 get_placeholder_texture() { return m_placeholder_texture_id; }
void draw_debug_window();
TextureRecord* lookup(u32 location) {
if (m_textures.at(location).normal_texture) {
return m_textures[location].normal_texture.get();
} else {
return nullptr;
}
}
TextureRecord* lookup_mt4hh(u32 location) {
if (m_textures.at(location).mt4hh_texture) {
return m_textures[location].mt4hh_texture.get();
} else {
return nullptr;
}
}
TextureRecord* get_random_texture();
void upload_to_gpu(TextureRecord* rec);
void relocate(u32 destination, u32 source, u32 format);
void draw_debug_for_tex(const std::string& name, GpuTexture* tex, u32 slot);
const std::array<TextureVRAMReference, 1024 * 1024 * 4 / 256> all_textures() const {
return m_textures;
}
void move_existing_to_vram(GpuTexture* tex, u32 slot_addr);
void remove_garbage_textures();
void discard(std::shared_ptr<TextureRecord> tex);
void serialize(Serializer& ser);
std::mutex& mutex() { return m_mutex; }
private:
void unload_all_textures();
void draw_debug_for_tex(const std::string& name, TextureRecord& tex);
TextureConverter m_tex_converter;
// uses tex.dest[mip] indexing. (bytes / 256). Currently only sets the base of a texture.
std::array<TextureData, 1024 * 1024 * 4 / 256> m_textures;
// textures that the game overwrote, but may be still allocated on the GPU.
// TODO: free these periodically.
std::vector<std::shared_ptr<TextureRecord>> m_garbage_textures;
void refresh_links(GpuTexture& texture);
GpuTexture* get_gpu_texture_for_slot(const std::string& name, u32 slot);
char m_regex_input[256] = "";
std::array<TextureVRAMReference, 1024 * 1024 * 4 / 256> m_textures;
struct Mt4hhTexture {
TextureVRAMReference ref;
u32 slot;
};
std::vector<Mt4hhTexture> m_mt4hh_textures;
int m_most_recent_gc_count = 0;
int m_most_recent_gc_count_gpu = 0;
};
std::vector<u32> m_placeholder_data;
u64 m_placeholder_texture_id = 0;
std::unordered_map<std::string, GpuTexture> m_loaded_textures;
std::mutex m_mutex;
};

View File

@ -559,10 +559,25 @@ void pc_texture_relocate(u32 dst, u32 src, u32 format) {
u64 pc_get_mips2c(u32 name) {
const char* n = Ptr<String>(name).c()->data();
fmt::print("Getting mips: {}\n", n);
return Mips2C::gLinkedFunctionTable.get(n);
}
void pc_set_levels(u32 l0, u32 l1) {
std::string l0s = Ptr<String>(l0).c()->data();
std::string l1s = Ptr<String>(l1).c()->data();
std::vector<std::string> levels;
if (l0s != "none" && l0s != "#f") {
levels.push_back(l0s);
}
if (l1s != "none" && l1s != "#f") {
levels.push_back(l1s);
}
Gfx::set_levels(levels);
}
/*!
* Open a file-stream. Name is a GOAL string. Mode is a GOAL symbol. Use 'read for readonly
* and anything else for write only.
@ -809,6 +824,7 @@ void InitMachine_PCPort() {
make_function_symbol_from_c("__pc-texture-upload-now", (void*)pc_texture_upload_now);
make_function_symbol_from_c("__pc-texture-relocate", (void*)pc_texture_relocate);
make_function_symbol_from_c("__pc-get-mips2c", (void*)pc_get_mips2c);
make_function_symbol_from_c("__pc-set-levels", (void*)pc_set_levels);
// pad stuff
make_function_symbol_from_c("pc-pad-get-mapped-button", (void*)Gfx::get_mapped_button);

View File

@ -1743,7 +1743,6 @@ void vcallms_311_case_386(ExecutionContext* c, u16* vis) {
c->vfs[vf14].vf.mul_xyzw(c->vf_src(vf13).vf, c->Q);
// sqi.xyzw vf04, vi08 | mulaw.xyzw ACC, vf20, vf08
c->acc.vf.mula_xyzw(c->vf_src(vf20).vf, c->vf_src(vf08).vf.w()); sq_xyzw(c->vf_src(vf04).vf, vis[vi08]++);
JUMP_386:
// rsqrt Q, vf00.w, vf16.x | maddaw.xyzw ACC, vf21, vf09
c->acc.vf.madda_xyzw(c->vfs[vf21].vf, c->vfs[vf09].vf.w()); c->Q = c->vf_src(vf00).vf.w() / std::sqrt(c->vf_src(vf16).vf.x());
// mtir vi12, vf01.y | maddaw.xyzw ACC, vf22, vf10

View File

@ -163,7 +163,7 @@ void FreeBuffer(IsoBufferHeader* buffer) {
void DisplayQueue() {
for (int pri = 0; pri < N_PRIORITIES; pri++) {
for (int cmd = 0; cmd < (int)gPriStack[pri].n; cmd++) {
printf(" PRI %d elt %d %s\n", pri, cmd, gPriStack[pri].names[cmd].c_str());
lg::debug(" PRI {} elt {} {}\n", pri, cmd, gPriStack[pri].names[cmd]);
}
}
}

View File

@ -78,7 +78,6 @@ void* RPC_STR(unsigned int fno, void* _cmd, int y) {
(void)fno;
(void)y;
auto* cmd = (RPC_Str_Cmd*)_cmd;
printf("RPC STR runs!\n");
if (cmd->chunk_id < 0) {
// it's _not_ a stream file. So we just treat it like a normal load.
@ -149,7 +148,6 @@ void* RPC_STR(unsigned int fno, void* _cmd, int y) {
}
}
}
printf("Command result %d\n", cmd->result);
return cmd;
}

View File

@ -5,10 +5,6 @@
;; name in dgo: sky-tng
;; dgos: GAME, ENGINE
(rlet ((st :reg r14 :type uint))
(format 0 "-------> ~A~%" (+ st #x4c58))
)
(define *sky-work*
(new 'static 'sky-work
;; usual 5x a+d

View File

@ -1727,18 +1727,9 @@
;; send now!
;; we send this to the GIF, which will basically ignore the
;; VIF tags (hopefully).
(#cond
(PC_PORT
(format 0 "Skipping upload-now! for ~A sz #x~X/#x~X bytes~%"
obj
(* 4 (-> obj segment 2 size))
(* 4 NEAR_PRIVATE_WORDS)
)
)
(else
(dma-buffer-send-chain (the-as dma-bank-source #x1000a000) gp-0)
)
)
(#unless PC_PORT
(dma-buffer-send-chain (the-as dma-bank-source #x1000a000) gp-0)
)
)
;; sync, wait for upload to complete
(dma-sync (the-as pointer #x1000a000) 0 0)
@ -2020,14 +2011,9 @@
0)
;; send and sync! VIF tags are probably ignored.
(#cond
(PC_PORT
(format 0 "Skipping dma-buffer-send-chain to relocate font texture~%")
)
(else
(dma-buffer-send-chain (the-as dma-bank-source #x10009000) dma-buff)
)
)
(#unless PC_PORT
(dma-buffer-send-chain (the-as dma-bank-source #x10009000) dma-buff)
)
)
(dma-sync (the-as pointer #x10009000) 0 0)

View File

@ -2142,6 +2142,13 @@
)
)
)
;; tell PC port about our levels
(__pc-set-levels
(if (= (-> obj level0 status) 'inactive) "none" (symbol->string (-> obj level0 nickname)))
(if (= (-> obj level1 status) 'inactive) "none" (symbol->string (-> obj level1 nickname)))
)
0
)

View File

@ -318,6 +318,7 @@
(define-extern __pc-texture-upload-now (function object object none))
(define-extern __pc-texture-relocate (function object object object none))
(define-extern __pc-get-mips2c (function string function))
(define-extern __pc-set-levels (function string string none))
(define-extern pc-pad-input-mode-set (function symbol none))
(define-extern pc-pad-input-pad-set (function int none))
(define-extern pc-pad-input-mode-get (function int))