diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-06 16:17:02 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-06 16:17:02 +0100 |
| commit | 8a5efe20e6e077e10592aa427d704836d2a08dd1 (patch) | |
| tree | 1df8a4cacb3f0637682d569a9f9469d52546c8a0 | |
| parent | Fix audit findings: crash safety, correctness, rendering, performance (diff) | |
| download | hl2h-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.md | 13 | ||||
| -rw-r--r-- | src/studio.cpp | 24 |
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 | |||
| 778 | itself is unchanged: projection only happens for the handful of entities that | 778 | itself is unchanged: projection only happens for the handful of entities that |
| 779 | survive the class/life/friendly filters, not for every entity. | 779 | survive 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 | ||
| 784 | to track the engine's current forced material. `DrawModelExecute` skips chams | ||
| 785 | when an override is already active, so shadow-depth and reflection passes draw | ||
| 786 | with their own material instead of being substituted with chams and then having | ||
| 787 | the engine's override cleared for every model drawn afterwards. This is the fix | ||
| 788 | for the corruption described in P2-6; it needs an in-game check because it | ||
| 789 | relies on the engine always going through the vtable for that call. | ||
| 790 | |||
| 781 | ### Known remaining work | 791 | ### Known remaining work |
| 782 | 792 | ||
| 783 | Deliberately deferred, with reasons: | 793 | Deliberately 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; | |||
| 212 | ForceOverrideFn g_force_override = nullptr; | 212 | ForceOverrideFn g_force_override = nullptr; |
| 213 | void *g_style_mats[8] = {}; | 213 | void *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. | ||
| 220 | static ForceOverrideFn g_real_force_override = nullptr; | ||
| 221 | static void *g_current_override = nullptr; | ||
| 222 | |||
| 223 | static 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 | |||
| 215 | const char *chams_material_name(int idx) { | 229 | const 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; |
