summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-06 16:17:02 +0100
committersalmonik <salmonik@tuta.io>2026-11-06 16:17:02 +0100
commit8a5efe20e6e077e10592aa427d704836d2a08dd1 (patch)
tree1df8a4cacb3f0637682d569a9f9469d52546c8a0
parentFix audit findings: crash safety, correctness, rendering, performance (diff)
downloadhl2h-8a5efe20e6e077e10592aa427d704836d2a08dd1.tar.gz
hl2h-8a5efe20e6e077e10592aa427d704836d2a08dd1.zip
Chams: leave engine-forced material passes alone
Shadow-depth and reflection passes call DrawModelExecute with a material already forced via IVModelRender::ForcedMaterialOverride. Chams could not tell that apart from a free draw, substituted its own material (putting chams into the shadow map) and then cleared the override to null, which broke the shadow material for every model drawn afterwards. Hook slot 1 as a tracker so the current forced material is known, and skip the chams substitution whenever one is active.
-rw-r--r--FIX_PLAN.md13
-rw-r--r--src/studio.cpp24
2 files changed, 33 insertions, 4 deletions
diff --git a/FIX_PLAN.md b/FIX_PLAN.md
index e4e149e..2c84122 100644
--- a/FIX_PLAN.md
+++ b/FIX_PLAN.md
@@ -778,6 +778,16 @@ Caches are cleared by `esp_reset_session()` on map change/respawn. The ESP scan
778itself is unchanged: projection only happens for the handful of entities that 778itself is unchanged: projection only happens for the handful of entities that
779survive the class/life/friendly filters, not for every entity. 779survive the class/life/friendly filters, not for every entity.
780 780
781### Batch 5 — chams shadow passes
782
783`IVModelRender::ForcedMaterialOverride` (slot 1) is now hooked as well, purely
784to track the engine's current forced material. `DrawModelExecute` skips chams
785when an override is already active, so shadow-depth and reflection passes draw
786with their own material instead of being substituted with chams and then having
787the engine's override cleared for every model drawn afterwards. This is the fix
788for the corruption described in P2-6; it needs an in-game check because it
789relies on the engine always going through the vtable for that call.
790
781### Known remaining work 791### Known remaining work
782 792
783Deliberately deferred, with reasons: 793Deliberately deferred, with reasons:
@@ -787,9 +797,6 @@ Deliberately deferred, with reasons:
787 slot plus a way to get the networkable from a raw entity pointer, or a 797 slot plus a way to get the networkable from a raw entity pointer, or a
788 cached pointer→index map. Not worth the risk for a hot path that fires a few 798 cached pointer→index map. Not worth the risk for a hot path that fires a few
789 times per second. 799 times per second.
790- **P2-6 shadow-depth `ForcedMaterialOverride`** — requires reading the
791 engine's current override, which VEngineModel016 does not expose. Needs an
792 alternative detection (draw flags in `DrawModelState_t`) validated in-game.
793- **P3-2 declarative settings table** — large rewrite of `config_save`, 800- **P3-2 declarative settings table** — large rewrite of `config_save`,
794 `config_load`, `config_reset_defaults` and `build_cards`. It can regress 801 `config_load`, `config_reset_defaults` and `build_cards`. It can regress
795 config persistence, so it should follow an in-game pass. 802 config persistence, so it should follow an in-game pass.
diff --git a/src/studio.cpp b/src/studio.cpp
index 69a2e07..8b0a710 100644
--- a/src/studio.cpp
+++ b/src/studio.cpp
@@ -212,6 +212,20 @@ ModelExecFn g_orig_model_exec = nullptr;
212ForceOverrideFn g_force_override = nullptr; 212ForceOverrideFn g_force_override = nullptr;
213void *g_style_mats[8] = {}; 213void *g_style_mats[8] = {};
214 214
215// The engine forces a material for special passes (shadow depth, reflections)
216// and clears it again. Without tracking that, the chams pass could not tell
217// "no override" from "engine override in progress": it hijacked the shadow pass
218// with chams materials and then cleared the engine's override for every model
219// drawn afterwards.
220static ForceOverrideFn g_real_force_override = nullptr;
221static void *g_current_override = nullptr;
222
223static void hkdForceOverride(void *self, void *mat, int type) {
224 g_current_override = mat;
225 if (g_real_force_override)
226 g_real_force_override(self, mat, type);
227}
228
215const char *chams_material_name(int idx) { 229const char *chams_material_name(int idx) {
216 switch (idx) { 230 switch (idx) {
217 case 1: return "debug/debugwireframe"; 231 case 1: return "debug/debugwireframe";
@@ -330,6 +344,12 @@ void hkdModelExecute(void *self, const void *state, const void *info,
330 g_orig_model_exec(self, state, info, bone); 344 g_orig_model_exec(self, state, info, bone);
331 return; 345 return;
332 } 346 }
347 // Shadow-depth/reflection passes arrive with a material already forced. Let
348 // them draw normally instead of substituting chams.
349 if (g_current_override) {
350 g_orig_model_exec(self, state, info, bone);
351 return;
352 }
333 if (feature_active(FEAT_CHAMS) && info && g_force_override) { 353 if (feature_active(FEAT_CHAMS) && info && g_force_override) {
334 // ModelRenderInfo_t::entity_index is at 0x30 on 32-bit. 354 // ModelRenderInfo_t::entity_index is at 0x30 on 32-bit.
335 int idx = *(const int *)((const char *)info + 0x30); 355 int idx = *(const int *)((const char *)info + 0x30);
@@ -568,7 +588,8 @@ void chams_init() {
568 logf("chams: model render has no vtable"); 588 logf("chams: model render has no vtable");
569 return; 589 return;
570 } 590 }
571 g_force_override = (ForceOverrideFn)vt[1]; 591 g_real_force_override = (ForceOverrideFn)vt[1];
592 g_force_override = g_real_force_override;
572 g_orig_model_exec = (ModelExecFn)vt[19]; 593 g_orig_model_exec = (ModelExecFn)vt[19];
573 if (!mod_engine().contains((uintptr_t)g_force_override) || 594 if (!mod_engine().contains((uintptr_t)g_force_override) ||
574 !mod_engine().contains((uintptr_t)g_orig_model_exec)) { 595 !mod_engine().contains((uintptr_t)g_orig_model_exec)) {
@@ -581,6 +602,7 @@ void chams_init() {
581 logf("chams: vtable clone failed"); 602 logf("chams: vtable clone failed");
582 return; 603 return;
583 } 604 }
605 nvt[1] = (void *)&hkdForceOverride; // track engine-forced passes
584 nvt[19] = (void *)&hkdModelExecute; 606 nvt[19] = (void *)&hkdModelExecute;
585 g_modelrender = g_iface.modelrender; 607 g_modelrender = g_iface.modelrender;
586 *(void ***)g_modelrender = nvt; 608 *(void ***)g_modelrender = nvt;