diff --git a/docs/cvars.md b/docs/cvars.md
index beeca5d8..63c9e43d 100644
--- a/docs/cvars.md
+++ b/docs/cvars.md
@@ -173,6 +173,7 @@
|sar_demo_clean_start|0|Attempts to minimize visual interpolation of some elements (like post-processing or lighting) when demo playback begins.|
|sar_demo_clean_start_tonemap|0|Overrides initial tonemap scalar value used in auto-exposure.
Setting it to 0 will attempt to skip over to target value for several ticks.|
|sar_demo_clean_start_tonemap_sample|cmd|sar_demo_clean_start_tonemap_sample [tick] - samples tonemap scale from current demo at given tick and stores it in "sar_demo_clean_start_tonemap" variable. If no tick is given, sampling will happen when `__END__` is seen in demo playback.|
+|sar_demo_modelcache_clear_protected_flags|0|Fix demo model-cache growth by clearing stale CModelLoader protected flags on demo stop.|
|sar_demo_overwrite_bak|0|Rename demos to (name)_bak if they would be overwritten by recording|
|sar_demo_portal_interp_fix|1|Fix eye interpolation through portals in demo playback.|
|sar_demo_remove_broken|1|Whether to remove broken frames from demo playback|
diff --git a/src/Features/Demo/ModelCacheTools.cpp b/src/Features/Demo/ModelCacheTools.cpp
new file mode 100644
index 00000000..2b90a45d
--- /dev/null
+++ b/src/Features/Demo/ModelCacheTools.cpp
@@ -0,0 +1,84 @@
+#include "Event.hpp"
+#include "Modules/Console.hpp"
+#include "Modules/Server.hpp"
+#include "Offsets.hpp"
+#include "Utils.hpp"
+#include "Utils/Memory.hpp"
+#include "Variable.hpp"
+
+namespace {
+constexpr unsigned int MODEL_FLAG_PROTECTED_MASK = 0x7E;
+constexpr unsigned int MODEL_FLAG_SKIP_PROTECTED_CLEAR = 0x40;
+constexpr unsigned int MAX_REASONABLE_MODEL_COUNT = 16384;
+
+DECL_CVAR_CALLBACK(sar_demo_modelcache_clear_protected_flags);
+
+Variable sar_demo_modelcache_clear_protected_flags(
+ "sar_demo_modelcache_clear_protected_flags",
+ "0",
+ "Fix demo model-cache growth by clearing stale CModelLoader protected flags on demo stop. Requires sv_cheats 1 to enable.\n",
+ FCVAR_NONE,
+ sar_demo_modelcache_clear_protected_flags_callback);
+
+DECL_CVAR_CALLBACK(sar_demo_modelcache_clear_protected_flags) {
+ if (sar_demo_modelcache_clear_protected_flags.GetBool() && flOldValue == 0.0f && !sv_cheats.GetBool()) {
+ console->Print("sar_demo_modelcache_clear_protected_flags requires sv_cheats 1.\n");
+ sar_demo_modelcache_clear_protected_flags.SetValue(pOldValue ? pOldValue : "0");
+ }
+}
+
+void *GetModelLoader() {
+ static uintptr_t global = 0;
+ if (!global) {
+ auto site = Memory::Scan(MODULE("engine"), Offsets::CModelLoaderModelPrecache);
+ if (!site) return nullptr;
+
+ global = Memory::Deref(site + Offsets::CModelLoaderModelPrecacheGlobal);
+ }
+
+ return Memory::Deref(global);
+}
+
+void ClearProtectedModelFlags() {
+ auto modelLoader = reinterpret_cast(GetModelLoader());
+ if (!modelLoader) {
+ static bool warned = false;
+ if (!warned) {
+ warned = true;
+ console->Warning("SAR: Failed to find CModelLoader for repeated-demo model-cache cleanup.\n");
+ }
+ return;
+ }
+
+ auto entries = *reinterpret_cast(modelLoader + Offsets::CModelLoaderEntryArray);
+ auto count = *reinterpret_cast(modelLoader + Offsets::CModelLoaderEntryCount);
+ if (!entries || count > MAX_REASONABLE_MODEL_COUNT) {
+ static bool warned = false;
+ if (!warned) {
+ warned = true;
+ console->Warning(
+ "SAR: Invalid CModelLoader state (loader=%p entries=%p count=%u).\n",
+ reinterpret_cast(modelLoader),
+ reinterpret_cast(entries),
+ count);
+ }
+ return;
+ }
+
+ for (unsigned int i = 0; i < count; ++i) {
+ auto model = *reinterpret_cast(entries + i * Offsets::CModelLoaderEntryStride + Offsets::CModelLoaderEntryModel);
+ if (!model) continue;
+
+ auto flags = reinterpret_cast(model + Offsets::CModelLoaderModelFlags);
+ if ((*flags & MODEL_FLAG_SKIP_PROTECTED_CLEAR) != 0) continue;
+
+ *flags &= ~MODEL_FLAG_PROTECTED_MASK;
+ }
+}
+} // namespace
+
+ON_EVENT(DEMO_STOP) {
+ if (sar_demo_modelcache_clear_protected_flags.GetBool()) {
+ ClearProtectedModelFlags();
+ }
+}
diff --git a/src/Offsets/Portal 2 5723.hpp b/src/Offsets/Portal 2 5723.hpp
index bbab9f47..746730b3 100644
--- a/src/Offsets/Portal 2 5723.hpp
+++ b/src/Offsets/Portal 2 5723.hpp
@@ -30,3 +30,12 @@ OFFSET_LINUX(DrawPortalSpBranchOff, 0x15)
SIGSCAN_LINUX(DrawPortalGhost, "55 89 E5 57 56 53 83 EC 5C A1 ? ? ? ? 8B 40")
SIGSCAN_LINUX(DrawPortalGhostSpBranch, "0F 84 ? ? ? ? FF 90 ? ? ? ? 80 BB ? ? ? ? 01")
SIGSCAN_LINUX(GetChapterProgress, "55 89 E5 57 56 53 83 EC 2C 8B 7D 08 E8 ? ? ? ? 8B 10 C7")
+SIGSCAN_LINUX(DispatchParticleEffect,"")
+SIGSCAN_LINUX(PrecacheParticleSystem, "")
+SIGSCAN_LINUX(GetCurrentTonemappingSystem, "")
+SIGSCAN_LINUX(ResetToneMapping, "")
+SIGSCAN_LINUX(LoadingProgress__SetupControlStatesInstruction, "")
+
+// Server
+SIGSCAN_LINUX(FloorReportalBranch,"")
+SIGSCAN_LINUX(CPortal_Player__PollForUseEntity_CheckMP, "")
\ No newline at end of file
diff --git a/src/Offsets/Portal 2 8151.hpp b/src/Offsets/Portal 2 8151.hpp
index f79461e8..796e189e 100644
--- a/src/Offsets/Portal 2 8151.hpp
+++ b/src/Offsets/Portal 2 8151.hpp
@@ -68,6 +68,11 @@ SIGSCAN_LINUX(AddShadowToReceiver, "55 89 E5 57 56 53 83 EC ? 8B 45 ? 8B 4D ? 8B
SIGSCAN_LINUX(UTIL_Portal_Color, "55 89 E5 56 53 83 EC 10 8B 75 ? 8B 5D ? 85 F6 0F 84")
SIGSCAN_LINUX(UTIL_Portal_Color_Particles, "55 89 E5 53 83 EC 14 A1 ? ? ? ? 8B 5D ? 8B 10 89 04 24 FF 92 ? ? ? ? 84 C0 75 ? 83 7D ? 01")
SIGSCAN_LINUX(GetChapterProgress, "55 89 E5 57 56 53 83 EC 2C 8B 5D 08 E8 ? ? ? ? 8B 10")
+SIGSCAN_LINUX(DispatchParticleEffect, "")
+SIGSCAN_LINUX(PrecacheParticleSystem, "")
+SIGSCAN_LINUX(GetCurrentTonemappingSystem, "")
+SIGSCAN_LINUX(ResetToneMapping, "")
+SIGSCAN_LINUX(LoadingProgress__SetupControlStatesInstruction, "")
// Engine
SIGSCAN_LINUX(Host_AccumulateTime, "55 89 E5 83 EC 28 F3 0F 10 05 ? ? ? ? A1 ? ? ? ? F3 0F 58 45 08 F3 0F 11 05 ? ? ? ? 8B 10 89 04 24 FF 52 24")
@@ -82,6 +87,15 @@ SIGSCAN_LINUX(InsertCommand, "55 89 E5 57 56 53 83 EC 1C 8B 75 ? 8B 5D ? 81 FE F
// EngineDemoPlayer
SIGSCAN_LINUX(InterpolateDemoCommand, "55 31 C9 89 E5 57 56 53 83 EC 3C 89 4D F0 8B 45 08 8B 4D 14 8B 80 B0 05 00 00 89 45 B8 8B 45 14 83 C0 04 89 45 D0")
+// CModelLoader
+SIGSCAN_LINUX(CModelLoaderModelPrecache, "A1 ? ? ? ? 8B 8D ? ? ? ? 8B 10 C7 44 24 08 04 00 00 00 89 4C 24 04 89 04 24 FF 52 1C")
+OFFSET_LINUX(CModelLoaderModelPrecacheGlobal, 1)
+OFFSET_LINUX(CModelLoaderEntryArray, 0x8)
+OFFSET_LINUX(CModelLoaderEntryCount, 0x16)
+OFFSET_LINUX(CModelLoaderEntryStride, 0x10)
+OFFSET_LINUX(CModelLoaderEntryModel, 0xC)
+OFFSET_LINUX(CModelLoaderModelFlags, 0x108)
+
// MaterialSystem
SIGSCAN_LINUX(KeyValues_SetString, "55 89 E5 53 83 EC ? 8B 45 ? C7 44 24 ? ? ? ? ? 8B 5D ? 89 44 24 ? 8B 45 ? 89 04 24 E8 ? ? ? ? 85 C0 74 ? 89 5D")
@@ -98,5 +112,8 @@ SIGSCAN_LINUX(UTIL_GetCommandClientIndex, "A1 ? ? ? ? 55 89 E5 5D 83 C0 01 C3")
SIGSCAN_LINUX(CheckStuck_FloatTime, "E8 ? ? ? ? 8B 43 04 DD 9D ? ? ? ? F2 0F 10 B5 ? ? ? ? 8B 50 24 66 0F 14 F6 66 0F 5A CE 85 D2")
SIGSCAN_DEFAULT(aircontrol_fling_speedSig, "0F 2F 25 ? ? ? ? 0F 28 F0",
"0F 2E 05 ? ? ? ? 0F 86 ? ? ? ? 0F 2E 25")
+SIGSCAN_DEFAULT(Portal2PromoFlagsSig, "", "")
+SIGSCAN_LINUX(FloorReportalBranch, "")
+SIGSCAN_LINUX(CPortal_Player__PollForUseEntity_CheckMP, "")
// clang-format on
diff --git a/src/Offsets/Portal 2 9568.hpp b/src/Offsets/Portal 2 9568.hpp
index 820e0826..be38ef68 100644
--- a/src/Offsets/Portal 2 9568.hpp
+++ b/src/Offsets/Portal 2 9568.hpp
@@ -481,7 +481,21 @@ OFFSET_DEFAULT(StartupDemoFile_HeaderName, 212, 184)
// EngineDemoPlayer
SIGSCAN_DEFAULT(InterpolateDemoCommand, "55 8B EC 83 EC 10 56 8B F1 8B 4D 10 57 8B BE B4 05 00 00 83 C1 04 89 75 F4 89 7D F0 E8 ? ? ? ? 8B 4D 14 83 C1 04",
- "55 57 56 53 83 EC 10 8B 44 24 24 8B 5C 24 2C 8B 88 B0 05 00 00 8B 44 24 30 8D 70 04 8D 90 9C 00 00 00 89 F0 F3 0F 10 40 04")
+ "55 57 56 53 83 EC 10 8B 44 24 24 8B 5C 24 2C 8B 88 B0 05 00 00 8B 44 24 30 8D 70 04 8D 90 9C 00 00 00 89 F0 F3 0F 10 40 04")
+
+
+// CModelLoader
+// win: "modelprecache" xref -> client string-table update callback -> CModelLoader vtable +0x1C call with flag 4; global immediate is g_pModelLoader
+// linux: "CClientState::ConsistencyCheck" xref -> model consistency type 3 block -> CModelLoader vtable +0x1C call with flag 4; global immediate is g_pModelLoader
+SIGSCAN_DEFAULT(CModelLoaderModelPrecache,
+ "8B 0D ? ? ? ? 8B 11 6A 04 50 8B 42 1C FF D0 50 EB 02 6A 00",
+ "A1 ? ? ? ? 83 EC 04 8B 10 6A 04 FF B5 ? ? ? ? 50 FF 52 1C 89 85 ? ? ? ? 83 C4 10 85 C0")
+OFFSET_DEFAULT(CModelLoaderModelPrecacheGlobal, 2, 1)
+OFFSET_DEFAULT(CModelLoaderEntryArray, 0x8, 0x8) // "CModelLoader::FindModel: NULL name" xref -> successful lookup path reads [this+8] + index*0x10 + 0xC
+OFFSET_DEFAULT(CModelLoaderEntryCount, 0x16, 0x16) // same CModelLoader::FindModel tree/list state; active count is this+0x16
+OFFSET_DEFAULT(CModelLoaderEntryStride, 0x10, 0x10) // same CModelLoader::FindModel lookup path; entry nodes are 0x10 bytes
+OFFSET_DEFAULT(CModelLoaderEntryModel, 0xC, 0xC) // same CModelLoader::FindModel lookup path; entry+0xC is model_t *
+OFFSET_DEFAULT(CModelLoaderModelFlags, 0x108, 0x108) // CModelLoader vtable +0x1C target ORs caller flags into model_t+0x108
// Matchmaking