module; #include "Main.h" #include "Error.h" #include "uMod_IDirect3DTexture9.h" #include #include #include export module TextureClient; import TextureFunction; import ModfileLoader; export std::vector> modlists_contents; struct PendingOp { enum class Kind { Add, Remove }; Kind kind; HashType hash; std::vector data; }; /* * An object of this class is owned by each d3d9 device. * All other functions are called from the render thread instance of the game itself. */ export class TextureClient { public: TextureClient(IDirect3DDevice9* device); ~TextureClient(); int AddTexture(uModTexturePtr auto pTexture); int RemoveTexture(uModTexturePtr auto pTexture); //called from uMod_IDirect3DTexture9::Release() int LookUpToMod(uModTexturePtr auto pTexture); // called at the end AddTexture(...) and from Device->UpdateTexture(...) int MergeUpdate(); //called from uMod_IDirect3DDevice9::BeginScene() void Initialize(); void EnqueueAdd(HashType hash, std::vector data); void EnqueueRemove(HashType hash); static int AddFile(const std::filesystem::path& path); static int RemoveFile(const std::filesystem::path& path); static std::vector GetFiles(); std::vector OriginalTextures; // stores the pointer to the uMod_IDirect3DTexture9 objects created by the game std::vector OriginalVolumeTextures; // stores the pointer to the uMod_IDirect3DVolumeTexture9 objects created by the game std::vector OriginalCubeTextures; // stores the pointer to the uMod_IDirect3DCubeTexture9 objects created by the game private: IDirect3DDevice9* D3D9Device; // Cached info about whether this id a dx9ex device or not; used for proxy functions bool isDirectXExDevice = false; // DX9 proxy functions void SetLastCreatedTexture(uMod_IDirect3DTexture9*); void SetLastCreatedVolumeTexture(uMod_IDirect3DVolumeTexture9*); void SetLastCreatedCubeTexture(uMod_IDirect3DCubeTexture9*); bool should_update = false; int LockMutex(); int UnlockMutex(); HANDLE hMutex; std::mutex pending_mutex; std::vector pending_ops; void ProcessPendingOps(); void RemoveModdedTexture(HashType hash); std::unordered_map> modded_textures; // array which stores the file in memory and the hash of each texture to be modded // called if a target texture is found int LoadTexture(TextureFileStruct* file_in_memory, uModTexturePtrPtr auto ppTexture); static void LoadStartupModlists(); static inline std::mutex global_mutex; static inline TextureClient* current_client = nullptr; static inline std::map> loaded_files; }; TextureClient::TextureClient(IDirect3DDevice9* device) { Message("TextureClient::TextureClient(): %p\n", this); D3D9Device = device; void* cpy; isDirectXExDevice = D3D9Device->QueryInterface(IID_IDirect3DTexture9, &cpy) == 0x01000001L; hMutex = CreateMutex(nullptr, false, nullptr); std::lock_guard lk(global_mutex); ASSERT(current_client == nullptr); // gMod assumes a single d3d9 device per process current_client = this; } TextureClient::~TextureClient() { Message("TextureClient::~TextureClient(): %p\n", this); { std::lock_guard lk(global_mutex); if (current_client == this) current_client = nullptr; } if (hMutex != nullptr) { CloseHandle(hMutex); } for (const auto texture_file_struct : modded_textures | std::views::values) { delete texture_file_struct; } modded_textures.clear(); } int TextureClient::MergeUpdate() { bool has_pending; { std::lock_guard lk(pending_mutex); has_pending = !pending_ops.empty(); } if (!should_update && !has_pending) return RETURN_OK; if (const int ret = LockMutex()) { gl_ErrorState |= uMod_ERROR_TEXTURE; return ret; } Message("MergeUpdate(): %p\n", this); ProcessPendingOps(); for (const auto pTexture : OriginalTextures) { if (pTexture->CrossRef_D3Dtex == nullptr) { LookUpToMod(pTexture); } } for (const auto pTexture : OriginalVolumeTextures) { if (pTexture->CrossRef_D3Dtex == nullptr) { LookUpToMod(pTexture); } } for (const auto pTexture : OriginalCubeTextures) { if (pTexture->CrossRef_D3Dtex == nullptr) { LookUpToMod(pTexture); } } should_update = false; return UnlockMutex(); } void TextureClient::EnqueueAdd(HashType hash, std::vector data) { if (!hash) return; std::lock_guard lk(pending_mutex); pending_ops.push_back(PendingOp{PendingOp::Kind::Add, hash, std::move(data)}); } void TextureClient::EnqueueRemove(HashType hash) { if (!hash) return; std::lock_guard lk(pending_mutex); pending_ops.push_back(PendingOp{PendingOp::Kind::Remove, hash, {}}); } void TextureClient::ProcessPendingOps() { std::vector ops; { std::lock_guard lk(pending_mutex); ops.swap(pending_ops); } for (auto& op : ops) { if (op.kind == PendingOp::Kind::Add) { if (modded_textures.contains(op.hash)) continue; const auto texture_file_struct = new TextureFileStruct(); texture_file_struct->crc_hash = op.hash; texture_file_struct->data = std::move(op.data); modded_textures.emplace(op.hash, texture_file_struct); should_update = true; } else { RemoveModdedTexture(op.hash); } } } void TextureClient::RemoveModdedTexture(HashType hash) { const auto it = modded_textures.find(hash); if (it == modded_textures.end()) return; const auto texture_file_struct = it->second; auto unswitch_in = [texture_file_struct](auto& vec) { for (auto* original_texture : vec) { auto* fake_texture = original_texture->CrossRef_D3Dtex; if (fake_texture && fake_texture->Reference == texture_file_struct) { UnswitchTextures(original_texture); fake_texture->Release(); } } }; unswitch_in(OriginalTextures); unswitch_in(OriginalVolumeTextures); unswitch_in(OriginalCubeTextures); modded_textures.erase(it); delete texture_file_struct; } void TextureClient::SetLastCreatedTexture(uMod_IDirect3DTexture9* texture) { if (isDirectXExDevice) return static_cast(D3D9Device)->SetLastCreatedTexture(texture); return static_cast(D3D9Device)->SetLastCreatedTexture(texture); } void TextureClient::SetLastCreatedVolumeTexture(uMod_IDirect3DVolumeTexture9* texture) { if (isDirectXExDevice) return static_cast(D3D9Device)->SetLastCreatedVolumeTexture(texture); return static_cast(D3D9Device)->SetLastCreatedVolumeTexture(texture); } void TextureClient::SetLastCreatedCubeTexture(uMod_IDirect3DCubeTexture9* texture) { if (isDirectXExDevice) return static_cast(D3D9Device)->SetLastCreatedCubeTexture(texture); return static_cast(D3D9Device)->SetLastCreatedCubeTexture(texture); } int TextureClient::LockMutex() { if ((gl_ErrorState & (uMod_ERROR_FATAL | uMod_ERROR_MUTEX))) { return RETURN_NO_MUTEX; } if (WAIT_OBJECT_0 != WaitForSingleObject(hMutex, 100)) { return RETURN_MUTEX_LOCK; //waiting 100ms, to wait infinite pass INFINITE } return RETURN_OK; } int TextureClient::UnlockMutex() { if (ReleaseMutex(hMutex) == 0) { return RETURN_MUTEX_UNLOCK; } return RETURN_OK; } gsl::owner MakeTextureFileStruct(TexEntry& entry, const bool compress) { const auto texture_file_struct = new TextureFileStruct(); texture_file_struct->crc_hash = entry.crc_hash; const auto dds_blob = TextureFunction::ConvertToCompressedDDS(entry, compress); texture_file_struct->data.assign(static_cast(dds_blob.GetBufferPointer()), static_cast(dds_blob.GetBufferPointer()) + dds_blob.GetBufferSize()); return texture_file_struct; } std::vector> ProcessModfile(const std::filesystem::path& modfile, const bool compress) { const auto hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); if (FAILED(hr)) return {}; const auto modfile_str = modfile.string(); Message("Initialize: loading file %s... ", modfile_str.c_str()); auto file_loader = ModfileLoader(modfile); auto entries = file_loader.GetContents(); if (entries.empty()) { Message("No entries found.\n"); CoUninitialize(); return {}; } Message("%zu textures... ", entries.size()); std::vector> texture_file_structs; texture_file_structs.reserve(entries.size()); unsigned file_bytes_loaded = 0; for (auto& tpf_entry : entries) { const auto texture_file_struct = MakeTextureFileStruct(tpf_entry, compress); texture_file_structs.push_back(texture_file_struct); file_bytes_loaded += static_cast(texture_file_structs.back()->data.size()); } entries.clear(); Message("%d bytes loaded.\n", file_bytes_loaded); CoUninitialize(); return texture_file_structs; } std::vector TextureClient::GetFiles() { std::lock_guard lk(global_mutex); std::vector result; result.reserve(loaded_files.size()); for (const auto& key : loaded_files | std::views::keys) { result.push_back(key); } return result; } int TextureClient::RemoveFile(const std::filesystem::path& path) { const auto absolute_path = std::filesystem::absolute(path); std::lock_guard lk(global_mutex); const auto it = loaded_files.find(absolute_path); if (it == loaded_files.end()) return RETURN_FILE_NOT_LOADED; if (current_client) { for (const auto hash : it->second) { current_client->EnqueueRemove(hash); } } loaded_files.erase(it); return RETURN_OK; } int TextureClient::AddFile(const std::filesystem::path& path) { const auto absolute_path = std::filesystem::absolute(path); { std::lock_guard lk(global_mutex); if (loaded_files.contains(absolute_path)) return RETURN_EXISTS; } if (!std::filesystem::exists(absolute_path)) return RETURN_FILE_NOT_LOADED; const auto file_size = std::filesystem::file_size(absolute_path); const auto texture_file_structs = ProcessModfile(absolute_path, file_size > 400'000'000); if (texture_file_structs.empty()) return RETURN_FILE_NOT_LOADED; std::lock_guard lk(global_mutex); // Re-check under lock; another thread may have loaded the same path concurrently. if (loaded_files.contains(absolute_path)) { for (auto* texture_file_struct : texture_file_structs) delete texture_file_struct; return RETURN_EXISTS; } std::vector hashes; for (auto* texture_file_struct : texture_file_structs) { const auto hash = texture_file_struct->crc_hash; if (hash && std::ranges::find(hashes, hash) == hashes.end()) { hashes.push_back(hash); if (current_client) { current_client->EnqueueAdd(hash, std::move(texture_file_struct->data)); } } delete texture_file_struct; } if (hashes.empty()) return RETURN_TEXTURE_NOT_LOADED; loaded_files.emplace(absolute_path, std::move(hashes)); return RETURN_OK; } void TextureClient::LoadStartupModlists() { std::locale::global(std::locale("")); std::vector modfiles; for (const auto& content : modlists_contents | std::views::values) { std::istringstream iss(content); std::string line; while (std::getline(iss, line)) { if (line.starts_with("//") || line.starts_with("#") || line.empty()) { continue; } // Remove newline character line.erase(std::ranges::remove(line, '\r').begin(), line.end()); line.erase(std::ranges::remove(line, '\n').begin(), line.end()); if (line.empty()) continue; auto modfile = std::filesystem::absolute(std::filesystem::path(utils::utf8_to_wstring(line))); if (!std::ranges::contains(modfiles, modfile)) { modfiles.push_back(std::move(modfile)); } } } auto total_size = 0ull; for (const auto& modfile : modfiles) { if (std::filesystem::exists(modfile)) total_size += std::filesystem::file_size(modfile); } const bool compress = total_size > 400'000'000; std::vector>>> futures; futures.reserve(modfiles.size()); for (const auto& modfile : modfiles) { futures.emplace_back(std::async(std::launch::async, ProcessModfile, modfile, compress)); } auto loaded_size = 0ull; for (size_t i = 0; i < modfiles.size(); ++i) { auto texture_file_structs = futures[i].get(); std::lock_guard lk(global_mutex); if (loaded_files.contains(modfiles[i])) { for (const auto* texture_file_struct : texture_file_structs) delete texture_file_struct; continue; } std::vector hashes; for (auto* texture_file_struct : texture_file_structs) { const auto hash = texture_file_struct->crc_hash; if (hash && std::ranges::find(hashes, hash) == hashes.end()) { hashes.push_back(hash); loaded_size += texture_file_struct->data.size(); if (current_client) { current_client->EnqueueAdd(hash, std::move(texture_file_struct->data)); } } delete texture_file_struct; } if (!hashes.empty()) { loaded_files.emplace(modfiles[i], std::move(hashes)); } } Info("LoadStartupModlists: %llu bytes (%llu MB)\n", loaded_size, loaded_size / 1024 / 1024); } void TextureClient::Initialize() { const auto t1 = std::chrono::high_resolution_clock::now(); Info("Initialize: begin\n"); // now load files that were added by AddFile() before the device existed std::vector pre_existing_paths; { std::lock_guard lk(global_mutex); pre_existing_paths.reserve(loaded_files.size()); for (const auto& key : loaded_files | std::views::keys) pre_existing_paths.push_back(key); } for (const auto& path : pre_existing_paths) { if (!std::filesystem::exists(path)) continue; const auto file_size = std::filesystem::file_size(path); auto texture_file_structs = ProcessModfile(path, file_size > 400'000'000); std::vector seen; for (auto* texture_file_struct : texture_file_structs) { const auto hash = texture_file_struct->crc_hash; if (hash && std::ranges::find(seen, hash) == seen.end()) { seen.push_back(hash); EnqueueAdd(hash, std::move(texture_file_struct->data)); } delete texture_file_struct; } } LoadStartupModlists(); const auto t2 = std::chrono::high_resolution_clock::now(); const auto ms = duration_cast(t2 - t1); Info("Initialize: end, took %d ms\n", ms); } int TextureClient::AddTexture(uModTexturePtr auto pTexture) { if constexpr (std::same_as) { SetLastCreatedTexture(nullptr); } else if constexpr (std::same_as) { SetLastCreatedVolumeTexture(nullptr); } else if constexpr (std::same_as) { SetLastCreatedCubeTexture(nullptr); } if (pTexture->FAKE) { return RETURN_OK; // this is a fake texture } if (gl_ErrorState & uMod_ERROR_FATAL) { return RETURN_FATAL_ERROR; } Message("TextureClient::AddTexture( Cube: %p): %p (thread: %u)\n", pTexture, this, GetCurrentThreadId()); pTexture->Hash = pTexture->GetHash(); if (!pTexture->Hash) { return RETURN_FATAL_ERROR; } if constexpr (std::same_as) { OriginalTextures.push_back(pTexture); } else if constexpr (std::same_as) { OriginalVolumeTextures.push_back(pTexture); } else if constexpr (std::same_as) { OriginalCubeTextures.push_back(pTexture); } return LookUpToMod(pTexture); // check if this texture should be modded } int TextureClient::RemoveTexture(uModTexturePtr auto pTexture) { Message("TextureClient::RemoveTexture( %p, %#lX): %p\n", pTexture, pTexture->Hash, this); if (gl_ErrorState & uMod_ERROR_FATAL) { return RETURN_FATAL_ERROR; } if (!pTexture->FAKE) { if constexpr (std::same_as) { utils::erase_first(OriginalTextures, pTexture); } else if constexpr (std::same_as) { utils::erase_first(OriginalVolumeTextures, pTexture); } else if constexpr (std::same_as) { utils::erase_first(OriginalCubeTextures, pTexture); } } if (!pTexture->Reference) return RETURN_OK; // Should this ever happen? return RETURN_OK; } int TextureClient::LookUpToMod(uModTexturePtr auto pTexture) { Message("TextureClient::LookUpToMod( %p): hash: %#lX\n", pTexture, pTexture->Hash); int ret = RETURN_OK; if (pTexture->CrossRef_D3Dtex != nullptr) return ret; // bug, this texture is already switched auto found = modded_textures.find(pTexture->Hash.crc32); if (found == modded_textures.end()) if (found = modded_textures.find(pTexture->Hash.crc64), !pTexture->Hash.crc64 || found == modded_textures.end()) return ret; const auto textureFileStruct = found->second; decltype(pTexture) fake_texture; ret = LoadTexture(textureFileStruct, &fake_texture); if (ret != RETURN_OK) return ret; ret = SwitchTextures(fake_texture, pTexture); if (ret != RETURN_OK) { Message("TextureClient::LookUpToMod(): textures not switched %#lX\n", textureFileStruct->crc_hash); fake_texture->Release(); return ret; } fake_texture->Reference = textureFileStruct; return ret; } int TextureClient::LoadTexture(TextureFileStruct* file_in_memory, uModTexturePtrPtr auto ppTexture) { Message("LoadTexture( %p, %p, %#lX): %p\n", file_in_memory, ppTexture, file_in_memory->crc_hash, this); if (const auto ret = DirectX::CreateDDSTextureFromMemoryEx( D3D9Device, file_in_memory->data.data(), file_in_memory->data.size(), 0, D3DPOOL_MANAGED, false, reinterpret_cast(ppTexture)); ret != D3D_OK) { *ppTexture = nullptr; Warning("LoadDDSTexture (%p, %#lX): FAILED ret: \n", file_in_memory->data.data(), file_in_memory->crc_hash, ret); return RETURN_TEXTURE_NOT_LOADED; } if constexpr (std::same_as) { SetLastCreatedTexture(nullptr); } else if constexpr (std::same_as) { SetLastCreatedVolumeTexture(nullptr); } else if constexpr (std::same_as) { SetLastCreatedCubeTexture(nullptr); } (*ppTexture)->FAKE = true; Message("LoadTexture (%p, %#lX): DONE\n", *ppTexture, file_in_memory->crc_hash); return RETURN_OK; }