@@ -88,12 +88,19 @@ namespace {
8888 // =========================================================================
8989
9090 HMODULE gmodDll = nullptr ;
91+ // True only when this module called LoadLibrary on gmodDll, i.e. we own the
92+ // reference and must FreeLibrary it on teardown. False when gMod was already
93+ // present (e.g. injected as the d3d9.dll proxy) - that copy belongs to the game.
94+ bool gmodLoadedByUs = false ;
9195
9296 std::vector<TexturePackEntry> packs;
9397
9498 bool gmodReady = false ;
9599 std::string statusMessage;
96100 bool gmodLoadAttempted = false ;
101+ // Set when gMod is no longer needed (last pack removed); the unload is performed
102+ // from Update() rather than the draw pass.
103+ bool gmodUnloadRequested = false ;
97104
98105 constexpr const char * INI_SECTION = " TexmodModule" ;
99106 constexpr const char * INI_PACK_COUNT = " PackCount" ;
@@ -323,6 +330,7 @@ namespace {
323330 HMODULE h = GetModuleHandleW (name);
324331 if (!h) continue ;
325332 gmodDll = h;
333+ gmodLoadedByUs = false ; // already in the process; we don't own this reference
326334 if (!ResolveTextureClientFunctions ()) {
327335 gmodDll = nullptr ;
328336 continue ;
@@ -334,6 +342,12 @@ namespace {
334342 return true ;
335343 }
336344
345+ // Only load our own copy of gMod when there is a pack to serve. With no packs
346+ // there is nothing to mod, so we leave gMod unloaded rather than have it hook
347+ // the device for nothing. (A gMod already injected as the d3d9 proxy was
348+ // adopted above regardless of this.)
349+ if (packs.empty ()) return false ;
350+
337351 if (gmodLoadAttempted) {
338352 // statusMessage = "Error: gMod initialization already attempted and failed. Please fix the issue and click Retry.";
339353 return false ;
@@ -351,11 +365,13 @@ namespace {
351365 statusMessage = " Error: Could not load gMod.dll. Make sure it is next to GWToolbox.dll." ;
352366 return false ;
353367 }
368+ gmodLoadedByUs = true ; // we own this reference and must FreeLibrary it on teardown
354369
355370 // 3. Resolve TextureClient shim exports.
356371 if (!ResolveTextureClientFunctions ()) {
357372 FreeLibrary (gmodDll);
358373 gmodDll = nullptr ;
374+ gmodLoadedByUs = false ;
359375 return false ;
360376 }
361377 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) pack.loaded = false ;
@@ -495,6 +509,21 @@ namespace {
495509 }
496510
497511 gmodReady = false ;
512+ pfnAddFile = nullptr ;
513+ pfnRemoveFile = nullptr ;
514+ pfnGetFiles = nullptr ;
515+ pfnSetDevice = nullptr ;
516+
517+ // Free the dll only if we loaded it. gMod's DllMain(DLL_PROCESS_DETACH) reverts
518+ // all its D3D9 vtable hooks (RemoveAllD3D9Hooks), so this is its clean shutdown
519+ // path. A gMod injected as the d3d9 proxy is owned by the game - never free it.
520+ if (gmodLoadedByUs && gmodDll) {
521+ FreeLibrary (gmodDll);
522+ }
523+ gmodDll = nullptr ;
524+ gmodLoadedByUs = false ;
525+ gmodLoadAttempted = false ; // allow a fresh load later (e.g. when a pack is added)
526+ gmodLocalVersionChecked = false ;
498527 }
499528
500529 // =========================================================================
@@ -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