From f5409c96c635c3cbfda30e984c55974b5482e8de Mon Sep 17 00:00:00 2001 From: PortalRex <35444194+PortalRex@users.noreply.github.com> Date: Thu, 22 Oct 2026 23:47:43 -0500 Subject: [PATCH 1/2] fix: crash when recording right after loading mp_coop_wall_5 CTextureManager::ForceAllTexturesIntoHardware queues a draw of every listed texture without referencing it. After a full update (e.g. `record` right after a load) textures whose last reference was dropped that frame are freed by the render thread before it reaches the preload draw, which then crashes. mp_coop_wall_5's movie screens hit this every time with sar_record_at 0. Run CacheUsedMaterials with threading off, as the engine does around screenshots. --- src/Modules/MaterialSystem.cpp | 20 ++++++++++++++++++++ src/Modules/MaterialSystem.hpp | 3 +++ src/Offsets/Portal 2 10090.hpp | 2 ++ src/Offsets/Portal 2 8151.hpp | 2 ++ 4 files changed, 27 insertions(+) diff --git a/src/Modules/MaterialSystem.cpp b/src/Modules/MaterialSystem.cpp index daf7275cf..d105ceffc 100644 --- a/src/Modules/MaterialSystem.cpp +++ b/src/Modules/MaterialSystem.cpp @@ -45,6 +45,7 @@ class MemTexRegen : public ITextureRegenerator { }; REDECL(MaterialSystem::UncacheUnusedMaterials); +REDECL(MaterialSystem::CacheUsedMaterials); REDECL(MaterialSystem::CreateMaterial); DETOUR(MaterialSystem::UncacheUnusedMaterials, bool bRecomputeStateSnapshots) { @@ -56,6 +57,22 @@ DETOUR(MaterialSystem::UncacheUnusedMaterials, bool bRecomputeStateSnapshots) { return result; } +// CacheUsedMaterials ends in CTextureManager::ForceAllTexturesIntoHardware, +// which queues a draw of every listed texture without referencing it. When an +// entity released its last reference to a texture earlier in the frame (the +// client runs this after every full update, which recreates all entities), the +// render thread frees that texture before it gets to the preload draw, which +// then crashes. `record` right after a load hits this on maps with a playing +// movie screen (mp_coop_wall_5). Turning threading off for the call runs the +// pending queue first, like the engine does around screenshots. +DETOUR(MaterialSystem::CacheUsedMaterials) { + auto AllowThreading = materialSystem->materials->Original<_AllowThreading>(Offsets::AllowThreading); + bool allowed = AllowThreading(thisptr, false, 0); + auto result = MaterialSystem::CacheUsedMaterials(thisptr); + AllowThreading(thisptr, allowed, 0); + return result; +} + DETOUR_T(IMaterial *, MaterialSystem::CreateMaterial, const char *pMaterialName, void *pVMTKeyValues) { if (sar.game->Is(SourceGame_Portal2 | SourceGame_Portal2_2011 | SourceGame_PortalReloaded | SourceGame_PortalStoriesMel)) { // Patched by Valve, no need for us to do it again @@ -88,6 +105,9 @@ bool MaterialSystem::Init() { if (this->materials) { this->materials->Hook(MaterialSystem::UncacheUnusedMaterials_Hook, MaterialSystem::UncacheUnusedMaterials, Offsets::UncacheUnusedMaterials); this->materials->Hook(MaterialSystem::CreateMaterial_Hook, MaterialSystem::CreateMaterial, Offsets::CreateMaterial); + if (Offsets::CacheUsedMaterials) { + this->materials->Hook(MaterialSystem::CacheUsedMaterials_Hook, MaterialSystem::CacheUsedMaterials, Offsets::CacheUsedMaterials); + } this->RemoveMaterial = this->materials->Original<_RemoveMaterial>(Offsets::RemoveMaterial); diff --git a/src/Modules/MaterialSystem.hpp b/src/Modules/MaterialSystem.hpp index 7075e810e..baea3d99a 100644 --- a/src/Modules/MaterialSystem.hpp +++ b/src/Modules/MaterialSystem.hpp @@ -26,8 +26,11 @@ class MaterialSystem : public Module { using _KeyValues_SetString = void(__rescall*)(void* thisptr, const char *key, const char *val); _KeyValues_SetString KeyValues_SetString = nullptr; + using _AllowThreading = bool(__rescall *)(void *thisptr, bool bAllow, int nServiceThread); + public: DECL_DETOUR(UncacheUnusedMaterials, bool bRecomputeStateSnapshots); + DECL_DETOUR(CacheUsedMaterials); DECL_DETOUR_T(IMaterial *, CreateMaterial, const char *pMaterialName, void *pVMTKeyValues); bool Init() override; diff --git a/src/Offsets/Portal 2 10090.hpp b/src/Offsets/Portal 2 10090.hpp index 4f99a7ed8..a6928a812 100644 --- a/src/Offsets/Portal 2 10090.hpp +++ b/src/Offsets/Portal 2 10090.hpp @@ -344,6 +344,8 @@ OFFSET_DEFAULT(snd_p, 98, 116) OFFSET_DEFAULT(snd_vol, 108, 122) OFFSET_DEFAULT(StartTouch, 102, 103) OFFSET_DEFAULT(UncacheUnusedMaterials, 77, 77) +OFFSET_DEFAULT(CacheUsedMaterials, 78, 0) +OFFSET_DEFAULT(AllowThreading, 135, 0) OFFSET_DEFAULT(CreateMaterial, 81, 81) OFFSET_DEFAULT(FindMaterial, 82, 82) OFFSET_DEFAULT(CreateProceduralTexture, 91, 91) diff --git a/src/Offsets/Portal 2 8151.hpp b/src/Offsets/Portal 2 8151.hpp index a7d5c605e..5ec5c2f2e 100644 --- a/src/Offsets/Portal 2 8151.hpp +++ b/src/Offsets/Portal 2 8151.hpp @@ -1,6 +1,8 @@ #include "Offsets/Portal 2 10090.hpp" // Portal Stories: Mel, Reloaded +OFFSET_WINDOWS(CacheUsedMaterials, 0) // texture preload fix only verified on Portal 2 10090 + OFFSET_LINUX(tickcount, 74) OFFSET_LINUX(interval_per_tick, 82) OFFSET_LINUX(demoplayer, 92) From 23a07936b2e900f0b4ef426e36101886e6a1dfa5 Mon Sep 17 00:00:00 2001 From: AMJ <69196954+ThisAMJ@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:04:06 +1100 Subject: [PATCH 2/2] feat: linux support for wall_5 fix --- src/Offsets/Portal 2 10090.hpp | 4 ++-- src/Offsets/Portal 2 8151.hpp | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Offsets/Portal 2 10090.hpp b/src/Offsets/Portal 2 10090.hpp index a6928a812..da96dc59d 100644 --- a/src/Offsets/Portal 2 10090.hpp +++ b/src/Offsets/Portal 2 10090.hpp @@ -344,8 +344,8 @@ OFFSET_DEFAULT(snd_p, 98, 116) OFFSET_DEFAULT(snd_vol, 108, 122) OFFSET_DEFAULT(StartTouch, 102, 103) OFFSET_DEFAULT(UncacheUnusedMaterials, 77, 77) -OFFSET_DEFAULT(CacheUsedMaterials, 78, 0) -OFFSET_DEFAULT(AllowThreading, 135, 0) +OFFSET_DEFAULT(CacheUsedMaterials, 78, 78) // client -> "LevelInitPreEntity - CacheUsedMaterials - Start" xref -> CPrecacheRegister::LevelInitPreEntity() -> following vtable call -> CMaterialSystem::CacheUsedMaterials +OFFSET_DEFAULT(AllowThreading, 135, 135) // materialsystem -> "Queued Material System: ENABLED!\n" xref -> CMaterialSystem::AllowThreading -> vtable index OFFSET_DEFAULT(CreateMaterial, 81, 81) OFFSET_DEFAULT(FindMaterial, 82, 82) OFFSET_DEFAULT(CreateProceduralTexture, 91, 91) diff --git a/src/Offsets/Portal 2 8151.hpp b/src/Offsets/Portal 2 8151.hpp index 5ec5c2f2e..75b955973 100644 --- a/src/Offsets/Portal 2 8151.hpp +++ b/src/Offsets/Portal 2 8151.hpp @@ -1,7 +1,8 @@ #include "Offsets/Portal 2 10090.hpp" // Portal Stories: Mel, Reloaded -OFFSET_WINDOWS(CacheUsedMaterials, 0) // texture preload fix only verified on Portal 2 10090 +OFFSET_DEFAULT(CacheUsedMaterials, 0, 0) // texture preload fix only verified on Portal 2 10090 +OFFSET_DEFAULT(AllowThreading, 0, 0) OFFSET_LINUX(tickcount, 74) OFFSET_LINUX(interval_per_tick, 82)