diff --git a/modules/TextureClient.ixx b/modules/TextureClient.ixx index 3e8af03..99449da 100644 --- a/modules/TextureClient.ixx +++ b/modules/TextureClient.ixx @@ -5,12 +5,21 @@ module; #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. @@ -28,6 +37,13 @@ public: 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; @@ -51,14 +67,22 @@ private: 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); - void LoadModsFromModlist(std::pair modfile_tuple); - std::filesystem::path dll_path; // path to gmod dll + 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) @@ -70,11 +94,19 @@ TextureClient::TextureClient(IDirect3DDevice9* device) 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); } @@ -86,8 +118,12 @@ TextureClient::~TextureClient() int TextureClient::MergeUpdate() { - if (!should_update) return RETURN_OK; - should_update = false; + 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; @@ -95,6 +131,8 @@ int TextureClient::MergeUpdate() Message("MergeUpdate(): %p\n", this); + ProcessPendingOps(); + for (const auto pTexture : OriginalTextures) { if (pTexture->CrossRef_D3Dtex == nullptr) { LookUpToMod(pTexture); @@ -111,9 +149,69 @@ int TextureClient::MergeUpdate() } } + 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) @@ -154,7 +252,7 @@ int TextureClient::UnlockMutex() return RETURN_OK; } -gsl::owner AddFile(TexEntry& entry, const bool compress) +gsl::owner MakeTextureFileStruct(TexEntry& entry, const bool compress) { const auto texture_file_struct = new TextureFileStruct(); texture_file_struct->crc_hash = entry.crc_hash; @@ -173,6 +271,7 @@ std::vector> ProcessModfile(const std::filesystem auto entries = file_loader.GetContents(); if (entries.empty()) { Message("No entries found.\n"); + CoUninitialize(); return {}; } Message("%zu textures... ", entries.size()); @@ -180,9 +279,9 @@ std::vector> ProcessModfile(const std::filesystem texture_file_structs.reserve(entries.size()); unsigned file_bytes_loaded = 0; for (auto& tpf_entry : entries) { - const auto tex_file_struct = AddFile(tpf_entry, compress); - texture_file_structs.push_back(tex_file_struct); - file_bytes_loaded += texture_file_structs.back()->data.size(); + 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); @@ -190,65 +289,162 @@ std::vector> ProcessModfile(const std::filesystem return texture_file_structs; } -void TextureClient::LoadModsFromModlist(std::pair modfile_tuple) +std::vector TextureClient::GetFiles() { - static std::vector loaded_modfiles{}; + 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::istringstream file(modfile_tuple.second); - std::string line; std::vector modfiles; - while (std::getline(file, line)) { - if (line.starts_with("//") || line.starts_with("#") || line.empty()) { + 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; } - // Remove newline character - line.erase(std::ranges::remove(line, '\r').begin(), line.end()); - line.erase(std::ranges::remove(line, '\n').begin(), line.end()); - const auto wline = utils::utf8_to_wstring(line); - const auto fsline = std::filesystem::path(wline); - - if (!std::ranges::contains(loaded_modfiles, fsline)) { - modfiles.push_back(fsline); - loaded_modfiles.push_back(fsline); - } - } - auto files_size = 0ull; - for (const auto modfile : modfiles) { - if (std::filesystem::exists(modfile)) { - files_size += std::filesystem::file_size(modfile); - } - } - std::vector>>> futures; - for (const auto modfile : modfiles) { - futures.emplace_back(std::async(std::launch::async, ProcessModfile, modfile, files_size > 400'000'000)); - } - auto loaded_size = 0u; - for (auto& future : futures) { - const auto texture_file_structs = future.get(); - for (const auto texture_file_struct : texture_file_structs) { - if (!texture_file_struct->crc_hash) continue; - if (!modded_textures.contains(texture_file_struct->crc_hash)) { - modded_textures.emplace(texture_file_struct->crc_hash, texture_file_struct); + 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)); + } } - else { - delete texture_file_struct; - } + delete texture_file_struct; + } + + if (!hashes.empty()) { + loaded_files.emplace(modfiles[i], std::move(hashes)); } - should_update = true; } - Message("Finished loading mods from %s: Loaded %u bytes (%u mb)\n", modfile_tuple.first.c_str(), loaded_size, loaded_size / 1024 / 1024); + 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"); - for (const auto& modlists_content : modlists_contents) { - LoadModsFromModlist(modlists_content); + + // 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); @@ -333,18 +529,18 @@ int TextureClient::LookUpToMod(uModTexturePtr auto pTexture) const auto textureFileStruct = found->second; - decltype(pTexture) fake_Texture; - ret = LoadTexture(textureFileStruct, &fake_Texture); + decltype(pTexture) fake_texture; + ret = LoadTexture(textureFileStruct, &fake_texture); if (ret != RETURN_OK) return ret; - ret = SwitchTextures(fake_Texture, pTexture); + ret = SwitchTextures(fake_texture, pTexture); if (ret != RETURN_OK) { Message("TextureClient::LookUpToMod(): textures not switched %#lX\n", textureFileStruct->crc_hash); - fake_Texture->Release(); + fake_texture->Release(); return ret; } - fake_Texture->Reference = textureFileStruct; + fake_texture->Reference = textureFileStruct; return ret; } diff --git a/source/dll_main.cpp b/source/dll_main.cpp index 5de38a2..602cd80 100644 --- a/source/dll_main.cpp +++ b/source/dll_main.cpp @@ -252,6 +252,54 @@ void InitInstance(HINSTANCE hModule) } } +extern "C" __declspec(dllexport) int __cdecl AddFile(const wchar_t* path) +{ + if (!path) return RETURN_BAD_ARGUMENT; + try { + return TextureClient::AddFile(std::filesystem::path(path)); + } + catch (...) { + return RETURN_FATAL_ERROR; + } +} + +extern "C" __declspec(dllexport) int __cdecl RemoveFile(const wchar_t* path) +{ + if (!path) return RETURN_BAD_ARGUMENT; + try { + return TextureClient::RemoveFile(std::filesystem::path(path)); + } + catch (...) { + return RETURN_FATAL_ERROR; + } +} + +extern "C" __declspec(dllexport) int __cdecl GetFiles(wchar_t* buffer, int buffer_size_chars) +{ + try { + const auto files = TextureClient::GetFiles(); + size_t required = 1; + for (const auto& path : files) { + required += path.native().size() + 1; + } + + if (buffer && buffer_size_chars > 0 && static_cast(buffer_size_chars) >= required) { + wchar_t* out = buffer; + for (const auto& path : files) { + const auto& native = path.native(); + std::ranges::copy(native, out); + out += native.size(); + *out++ = L'\0'; + } + *out = L'\0'; + } + return static_cast(required); + } + catch (...) { + return RETURN_FATAL_ERROR; + } +} + void ExitInstance() { DISABLE_HOOK(GetProcAddress_fn);