#pragma once #include #include #include #include #include #include #include "common/common_types.h" #include "common/util/Serializer.h" #include "common/util/SmallVector.h" #include "common/versions/versions.h" #include "game/graphics/texture/TextureConverter.h" #include "game/graphics/texture/TextureID.h" #include "third-party/glad/include/glad/glad.h" // verify all texture lookups. // will make texture lookups slower and likely caused dropped frames when loading constexpr bool EXTRA_TEX_DEBUG = false; // sky, cloud textures constexpr int SKY_TEXTURE_VRAM_ADDRS[2] = {8064, 8096}; /*! * 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 */ template class TextureMap { public: TextureMap(const std::vector& tpage_dir) { u32 off = 0; for (auto& x : tpage_dir) { m_dir.push_back(off); off += x; } m_data.resize(off); } T* lookup_existing(PcTextureId id) { auto& elt = m_data[m_dir[id.page] + id.tex]; if (elt.present) { return &elt.val; } else { return nullptr; } } T& at(PcTextureId id) { auto& elt = m_data[m_dir[id.page] + id.tex]; if (elt.present) { return elt.val; } ASSERT(false); } std::pair lookup_or_insert(PcTextureId id) { auto& elt = m_data[m_dir[id.page] + id.tex]; if (elt.present) { return std::make_pair(&elt.val, true); } else { elt.present = true; return std::make_pair(&elt.val, false); } } void erase(PcTextureId id) { auto& elt = m_data[m_dir[id.page] + id.tex]; elt.present = false; } private: std::vector m_dir; struct Element { T val; bool present = false; }; std::vector m_data; }; /*! * The lowest level reference to texture data. */ struct TextureData { GLuint 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 { GpuTexture(PcTextureId id) : tex_id(id) {} GpuTexture() = default; PcTextureId tex_id; // all the currently loaded copies of this texture std::vector gpu_textures; // the vram addresses that contain this texture std::vector slots; // the vram address that contain this texture, stored in mt4hh format std::vector mt4hh_slots; // 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 { GLuint gpu_texture = -1; // the OpenGL texture to use when rendering. GpuTexture* source = nullptr; }; /*! * A texture provided by the loader. */ struct TextureInput { std::string debug_page_name; std::string debug_name; PcTextureId id; GLuint gpu_texture = -1; bool common = false; const u8* src_data; u16 w, h; }; /*! * The in-game texture type. */ struct GoalTexture { s16 w; s16 h; u8 num_mips; u8 tex1_control; u8 psm; u8 mip_shift; u16 clutpsm; u16 dest[7]; u16 clut_dest; u8 width[7]; u32 name_ptr; u32 size; float uv_dist; u32 masks[3]; s32 segment_of_mip(s32 mip) const { if (2 >= num_mips) { return num_mips - mip - 1; } else { return std::max(0, 2 - mip); } } }; 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; u32 size; u32 dest; }; u32 file_info_ptr; u32 name_ptr; u32 id; s32 length; // texture count u32 mip0_size; u32 size; Seg segment[3]; u32 pad[16]; // start of array. std::string print() const; bool try_copy_texture_description(GoalTexture* dest, int idx, const u8* memory_base, const u8* tpage, u32 s7_ptr) { u32 ptr; memcpy(&ptr, tpage + sizeof(GoalTexturePage) + 4 * idx, 4); if (ptr == s7_ptr) { return false; } memcpy(dest, memory_base + ptr, sizeof(GoalTexture)); return true; } }; /*! * 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(GameVersion version); void handle_upload_now(const u8* tpage, int mode, const u8* memory_base, u32 s7_ptr, bool debug); GpuTexture* give_texture(const TextureInput& in); GpuTexture* give_texture_and_load_to_vram(const TextureInput& in, u32 vram_slot); void unload_texture(PcTextureId tex_id, u64 gpu_id); void update_gl_texture(GpuTexture* texture, u32 new_w, u32 new_h, GLuint new_gl_texture); /*! * Look up an OpenGL texture by vram address. Return std::nullopt if the game hasn't loaded * anything to this address. */ std::optional 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 {}; } } /*! * 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 lookup_mt4hh(u32 location); u64 get_placeholder_texture() { return m_placeholder_texture_id; } void draw_debug_window(); void relocate(u32 destination, u32 source, u32 format); void draw_debug_for_tex(const std::string& name, GpuTexture* tex, u32 slot); const std::array& all_textures() const { return m_textures; } void move_existing_to_vram(GpuTexture* tex, u32 slot_addr); std::mutex& mutex() { return m_mutex; } PcTextureId allocate_pc_port_texture(GameVersion version); std::string get_debug_texture_name(PcTextureId id); std::string get_debug_texture_name_from_tbp(u32 tbp); private: void refresh_links(GpuTexture& texture); GpuTexture* get_gpu_texture_for_slot(PcTextureId id, u32 slot); char m_regex_input[256] = ""; std::array m_textures; struct Mt4hhTexture { TextureVRAMReference ref; u32 slot; }; std::vector m_mt4hh_textures; std::vector m_placeholder_data; u64 m_placeholder_texture_id = 0; TextureMap m_loaded_textures; // we maintain a mapping of all textures/ids we've seen so far. // this is only used for debug. TextureMap m_id_to_name; std::unordered_map m_name_to_id; u32 m_next_pc_texture_to_allocate = 0; u32 m_tpage_dir_size = 0; std::mutex m_mutex; };