@@ -89,12 +89,19 @@ namespace {
8989 // =========================================================================
9090
9191 HMODULE gmodDll = nullptr ;
92+ // True only when this module called LoadLibrary on gmodDll, i.e. we own the
93+ // reference and must FreeLibrary it on teardown. False when gMod was already
94+ // present (e.g. injected as the d3d9.dll proxy) - that copy belongs to the game.
95+ bool gmodLoadedByUs = false ;
9296
9397 std::vector<TexturePackEntry> packs;
9498
9599 bool gmodReady = false ;
96100 std::string statusMessage;
97101 bool gmodLoadAttempted = false ;
102+ // Set when gMod is no longer needed (last pack removed); the unload is performed
103+ // from Update() rather than the draw pass.
104+ bool gmodUnloadRequested = false ;
98105
99106 constexpr const char * INI_SECTION = " TexmodModule" ;
100107 constexpr const char * INI_PACK_COUNT = " PackCount" ;
@@ -324,6 +331,7 @@ namespace {
324331 HMODULE h = GetModuleHandleW (name);
325332 if (!h) continue ;
326333 gmodDll = h;
334+ gmodLoadedByUs = false ; // already in the process; we don't own this reference
327335 if (!ResolveTextureClientFunctions ()) {
328336 gmodDll = nullptr ;
329337 continue ;
@@ -335,6 +343,12 @@ namespace {
335343 return true ;
336344 }
337345
346+ // Only load our own copy of gMod when there is a pack to serve. With no packs
347+ // there is nothing to mod, so we leave gMod unloaded rather than have it hook
348+ // the device for nothing. (A gMod already injected as the d3d9 proxy was
349+ // adopted above regardless of this.)
350+ if (packs.empty ()) return false ;
351+
338352 if (gmodLoadAttempted) {
339353 // statusMessage = "Error: gMod initialization already attempted and failed. Please fix the issue and click Retry.";
340354 return false ;
@@ -352,11 +366,13 @@ namespace {
352366 statusMessage = " Error: Could not load gMod.dll. Make sure it is next to GWToolbox.dll." ;
353367 return false ;
354368 }
369+ gmodLoadedByUs = true ; // we own this reference and must FreeLibrary it on teardown
355370
356371 // 3. Resolve TextureClient shim exports.
357372 if (!ResolveTextureClientFunctions ()) {
358373 FreeLibrary (gmodDll);
359374 gmodDll = nullptr ;
375+ gmodLoadedByUs = false ;
360376 return false ;
361377 }
362378 pfnSetDevice (GuildWars_IDirect3DDevice9_Instance);
@@ -479,13 +495,11 @@ namespace {
479495
480496 void ShutdownGMod ()
481497 {
482- if (!gmodReady) return ;
483-
484- // Unload synchronously: on teardown the worker threads are stopping, so a
485- // queued ApplyLoadOrder might never run. Holding apply_mutex waits out any
486- // in-flight worker reconcile; bumping the generation makes queued/running
487- // ones skip; then we remove everything here, last, so nothing reloads after.
488- {
498+ if (gmodReady) {
499+ // Unload synchronously: on teardown the worker threads are stopping, so a
500+ // queued ApplyLoadOrder might never run. Holding apply_mutex waits out any
501+ // in-flight worker reconcile; bumping the generation makes queued/running
502+ // ones skip; then we remove everything here, last, so nothing reloads after.
489503 std::lock_guard lk (apply_mutex);
490504 ++apply_generation;
491505 for (auto & pack : packs)
@@ -497,6 +511,21 @@ namespace {
497511 }
498512
499513 gmodReady = false ;
514+ pfnAddFile = nullptr ;
515+ pfnRemoveFile = nullptr ;
516+ pfnGetFiles = nullptr ;
517+ pfnSetDevice = nullptr ;
518+
519+ // Free the dll only if we loaded it. gMod's DllMain(DLL_PROCESS_DETACH) reverts
520+ // all its D3D9 vtable hooks (RemoveAllD3D9Hooks), so this is its clean shutdown
521+ // path. A gMod injected as the d3d9 proxy is owned by the game - never free it.
522+ if (gmodLoadedByUs && gmodDll) {
523+ FreeLibrary (gmodDll);
524+ }
525+ gmodDll = nullptr ;
526+ gmodLoadedByUs = false ;
527+ gmodLoadAttempted = false ; // allow a fresh load later (e.g. when a pack is added)
528+ gmodLocalVersionChecked = false ;
500529 }
501530
502531 // =========================================================================
@@ -856,6 +885,8 @@ namespace {
856885 if (ImGui::ConfirmButton (ICON_FA_TRASH , &del_bool)) {
857886 if (pack.loaded ) UnloadTexturePack (pack.path );
858887 packs.erase (packs.begin () + i);
888+ // No packs left to serve: drop gMod so it stops hooking the device.
889+ if (packs.empty ()) gmodUnloadRequested = true ;
859890 ImGui::PopID ();
860891 break ;
861892 }
@@ -1142,6 +1173,14 @@ void TexmodModule::Update(float)
11421173 // Keep the D3D9 CreateTexture capture hook matched to the recording toggle.
11431174 SetTextureCapture (recording);
11441175
1176+ // Unload gMod once nothing needs it (requested when the last pack was removed).
1177+ if (gmodUnloadRequested) {
1178+ gmodUnloadRequested = false ;
1179+ // Only unload what we own; an injected proxy is left to the game. ShutdownGMod
1180+ // already no-ops the FreeLibrary in that case, but skip the pack teardown too.
1181+ if (gmodLoadedByUs) ShutdownGMod ();
1182+ }
1183+
11451184 if (gmodReady) return ;
11461185 InitGMod ();
11471186}
@@ -1154,7 +1193,7 @@ void TexmodModule::Draw(IDirect3DDevice9*)
11541193void TexmodModule::Terminate ()
11551194{
11561195 TeardownTextureCapture ();
1157- ShutdownGMod (); // synchronously unloads every pack from gMod
1196+ ShutdownGMod (); // unloads every pack and frees gMod.dll if we loaded it
11581197 ToolboxModule::Terminate ();
11591198}
11601199
0 commit comments