From ce3a21bb73e4cf872cb4307176ac435ad51d71d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 12 May 2026 21:08:44 +0000 Subject: [PATCH 1/2] Refill mod when later draw uses a layout with extra semantics Some games (e.g. "2015g1") first draw a modded mesh with a vertex layout that lacks NORMAL (likely a depth/shadow pass). The mod VB gets filled from that layout, leaving zeros where the normal should be. A later draw with a layout that does include NORMAL then reuses the bad VB and the mod renders with garbage normals. Detect that mismatch and refill: - Store a u128 bitmask of (semantic, semantic_index) pairs declared by the fill layout on ModD3DData11, populated in load_d3d_data11. - On each modded draw, recompute the current layout's mask and bail out to the reload path if it has any bits the fill mask doesn't. - Hot-path check is two map lookups + a bitwise op; no locks, no global. The closure passed to check_and_render_mod gets only an immutable mod ref, so the refill request bubbles up via an override variable and the existing NotRenderedButLoadRequested handler does the work, via a new mod_load::reset_for_reload helper that releases the d3d data and transitions back to Unloaded. https://claude.ai/code/session_018KSB4LpgEY7ZyheoHr1A4Y --- Native/hook_core/src/hook_render_d3d11.rs | 53 +++++++++++++++- Native/mod_load/src/mod_load.rs | 18 ++++++ Native/shared_dx/src/dx11rs.rs | 77 ++++++++++++++++++++++- Native/types/src/d3ddata.rs | 10 +++ 4 files changed, 154 insertions(+), 4 deletions(-) diff --git a/Native/hook_core/src/hook_render_d3d11.rs b/Native/hook_core/src/hook_render_d3d11.rs index da6cddc9..9b1be270 100644 --- a/Native/hook_core/src/hook_render_d3d11.rs +++ b/Native/hook_core/src/hook_render_d3d11.rs @@ -6,7 +6,7 @@ use std::time::{SystemTime, Duration}; use global_state::{GLOBAL_STATE, LOADED_MODS, METRICS_TRACK_MOD_PRIMS, HWND}; use mod_stats::mod_stats; -use shared_dx::dx11rs::{DX11RenderState}; +use shared_dx::dx11rs::{DX11RenderState, VertexFormat}; use shared_dx::types::{HookDeviceState, DevicePointer, DX11Metrics, D3D11Tex}; use shared_dx::types_dx11::{HookDirect3D11Context}; use shared_dx::util::{write_log_file, ReleaseOnDrop}; @@ -386,6 +386,33 @@ fn compute_prim_vert_count(index_count: UINT, rs:&DX11RenderState) -> Option<(u3 Some((prim_count,vert_count)) } +/// True if `current_layout_ptr` declares any semantic that wasn't present in +/// the layout used to fill `d3d11d.vb`. When that happens, the existing fill +/// is missing data (e.g. NORMAL bytes are zero because the fill layout was a +/// depth-only pass), and the mod needs to be released and refilled. +/// +/// Returns false if the mask wasn't recorded at fill time (mask == 0), if the +/// current layout pointer is null, or if the layout isn't in the context's +/// layout map. All of those preserve pre-refill behavior. +unsafe fn needs_refill_for_layout( + d3d11d: &ModD3DData11, + current_layout_ptr: *mut ID3D11InputLayout, + layouts: &FnvHashMap, +) -> bool { + if current_layout_ptr.is_null() { + return false; + } + let old_mask = d3d11d.vlayout_semantic_mask; + if old_mask == 0 { + return false; + } + let new_mask = match layouts.get(&(current_layout_ptr as usize)) { + Some(vf) => vf.semantic_mask(), + None => return false, + }; + VertexFormat::has_extra_semantics(old_mask, new_mask) +} + fn update_drawn_recently(metrics:&mut DX11Metrics, prim_count:u32, vert_count: u32, checkres:&CheckRenderModResult) { if METRICS_TRACK_MOD_PRIMS { use shared_dx::types::MetricsDrawStatus::*; @@ -661,11 +688,26 @@ pub unsafe extern "system" fn hook_draw_indexed( } else { profile_end!(hdi, mod_precheck); profile_start!(hdi, mod_check); + // If a draw needs the mod refilled (current layout has + // semantics the original fill didn't see), the closure + // can't mutate the mod directly — it has only an immutable + // ref. Stash the request here and let the post-check + // block handle release + reload. + let mut override_mod_status = None; let mod_status = check_and_render_mod(prim_count, vert_count, |d3dd,nmod| { profile_start!(hdi, mod_render); let res = if let ModD3DData::D3D11(d3d11d) = d3dd { - render_mod_d3d11(THIS, hook_context, d3d11d, nmod, override_texture, sel_stage, (prim_count,vert_count)) + if needs_refill_for_layout(d3d11d, + state.rs.current_input_layout, + &state.rs.context_input_layouts_by_ptr) { + override_mod_status = Some( + CheckRenderModResult::NotRenderedButLoadRequested( + nmod.name.clone())); + false + } else { + render_mod_d3d11(THIS, hook_context, d3d11d, nmod, override_texture, sel_stage, (prim_count,vert_count)) + } } else { false }; @@ -673,7 +715,7 @@ pub unsafe extern "system" fn hook_draw_indexed( res }); profile_end!(hdi, mod_check); - mod_status + override_mod_status.unwrap_or(mod_status) }; profile_start!(hdi, post_mod_check); @@ -689,6 +731,11 @@ pub unsafe extern "system" fn hook_draw_indexed( Ok(mut loaded_mods_guard) => { let nmod = mod_load::get_mod_by_name(name, &mut *loaded_mods_guard); if let Some(nmod) = nmod { + // If the mod is already loaded but we need + // a refill (semantics-mismatch path), drop + // the stale d3d data so the Unloaded path + // below can prime a fresh load. + mod_load::reset_for_reload(nmod); // need to store current input layout in the d3d data if let ModD3DState::Unloaded = nmod.d3d_data { let il = state.rs.current_input_layout; diff --git a/Native/mod_load/src/mod_load.rs b/Native/mod_load/src/mod_load.rs index efb120f8..eba8d0cb 100644 --- a/Native/mod_load/src/mod_load.rs +++ b/Native/mod_load/src/mod_load.rs @@ -52,6 +52,20 @@ fn clear_d3d_data(nmd:&mut NativeModData) { nmd.d3d_data = native_mod::ModD3DState::Unloaded; } +/// If the mod is currently `Loaded`, release its d3d resources and transition +/// to `Unloaded`. The standard "render an unloaded mod" path then primes the +/// reload using the current input layout on the next frame. +/// +/// Used when something about the current draw makes the existing fill stale +/// (e.g. the layout in use exposes semantics the original fill didn't see). +pub unsafe fn reset_for_reload(nmd: &mut NativeModData) { + if let native_mod::ModD3DState::Loaded(ref mut d3dd) = nmd.d3d_data { + write_log_file(&format!("resetting mod {} for reload", nmd.name)); + d3dd.release(); + nmd.d3d_data = native_mod::ModD3DState::Unloaded; + } +} + pub unsafe fn clear_loaded_mods(device: DevicePointer) { let lock = GLOBAL_STATE_LOCK.lock(); if let Err(_e) = lock { @@ -411,6 +425,10 @@ pub unsafe fn load_d3d_data11(device: *mut ID3D11Device, callbacks: interop::Man d3d_data.vb = vertex_buffer; d3d_data.vert_size = vert_size as u32; d3d_data.vert_count = vert_count as u32; + // Remember which semantics the fill layout exposed, so we can detect later + // draws that use a richer layout (e.g. depth-only pass filled the VB, then + // a color pass with NORMAL shows up) and trigger a refill. + d3d_data.vlayout_semantic_mask = vlayout.semantic_mask(); // load textures, if any let dp = DevicePointer::D3D11(device); diff --git a/Native/shared_dx/src/dx11rs.rs b/Native/shared_dx/src/dx11rs.rs index 4814ef03..0c57a641 100644 --- a/Native/shared_dx/src/dx11rs.rs +++ b/Native/shared_dx/src/dx11rs.rs @@ -18,6 +18,58 @@ pub struct VertexFormat { pub size: u32, } +/// Packed bitmask of (semantic, semantic_index) pairs declared by a vertex +/// layout, restricted to the semantics ModelMod knows how to fill. +/// +/// Two distinct layouts that declare the same set of supported semantics +/// produce equal masks. The hot-path refill check is `(new & !old) != 0`. +pub type SemanticMask = u128; + +/// Subset of D3D semantic names ModelMod fills. Anything not listed here +/// (custom engine semantics, etc.) is silently dropped from the mask, which +/// matches the existing fill behavior. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum Semantic { + Position = 0, + Normal = 1, + TexCoord = 2, + Binormal = 3, + Bitangent = 4, + Color = 5, + Tangent = 6, + BlendIndices = 7, + BlendWeight = 8, +} + +/// Number of distinct semantic indices per semantic that fit in the mask. +/// 9 semantics * 14 indices = 126 bits, fits in u128 with 2 spare. Indices +/// at or above this value get clamped to the top slot (extremely rare in +/// practice; ModelMod would not be filling them anyway). +const SEM_INDEX_SLOTS: u32 = 14; + +impl Semantic { + /// D3D treats semantic names as case-insensitive; match that here. + fn from_name_bytes(name: &[u8]) -> Option { + if name.eq_ignore_ascii_case(b"POSITION") { Some(Semantic::Position) } + else if name.eq_ignore_ascii_case(b"NORMAL") { Some(Semantic::Normal) } + else if name.eq_ignore_ascii_case(b"TEXCOORD") { Some(Semantic::TexCoord) } + else if name.eq_ignore_ascii_case(b"BINORMAL") { Some(Semantic::Binormal) } + else if name.eq_ignore_ascii_case(b"BITANGENT") { Some(Semantic::Bitangent) } + else if name.eq_ignore_ascii_case(b"COLOR") { Some(Semantic::Color) } + else if name.eq_ignore_ascii_case(b"TANGENT") { Some(Semantic::Tangent) } + else if name.eq_ignore_ascii_case(b"BLENDINDICES") { Some(Semantic::BlendIndices) } + else if name.eq_ignore_ascii_case(b"BLENDWEIGHT") { Some(Semantic::BlendWeight) } + else { None } + } + + #[inline] + fn mask_bit(self, index: u32) -> SemanticMask { + let idx = index.min(SEM_INDEX_SLOTS - 1); + 1u128 << ((self as u32) * SEM_INDEX_SLOTS + idx) + } +} + impl VertexFormat { /// Create a shallow copy of the vertex format. This will copy the layout vector, but the /// pointers in the vector elements will still point to the same strings as the original. @@ -27,6 +79,29 @@ impl VertexFormat { size: self.size, } } + + /// Compute a bitmask of the (semantic, semantic_index) pairs declared by + /// this layout, restricted to semantics ModelMod fills. Cheap enough to + /// recompute on each modded draw (CStr scan over typically <=16 elements). + pub fn semantic_mask(&self) -> SemanticMask { + let mut mask: SemanticMask = 0; + for elem in &self.layout { + if elem.SemanticName.is_null() { + continue; + } + let name_bytes = unsafe { CStr::from_ptr(elem.SemanticName) }.to_bytes(); + if let Some(sem) = Semantic::from_name_bytes(name_bytes) { + mask |= sem.mask_bit(elem.SemanticIndex); + } + } + mask + } + + /// True if `new` declares any (semantic, index) pair not present in `old`. + #[inline] + pub fn has_extra_semantics(old: SemanticMask, new: SemanticMask) -> bool { + (new & !old) != 0 + } } impl Display for VertexFormat { @@ -35,7 +110,7 @@ impl Display for VertexFormat { for i in 0..self.layout.len() { let bytename = unsafe { CStr::from_ptr(self.layout[i].SemanticName) }.to_str(); - write!(f, "{:?}", bytename)?; + write!(f, "{:?}/{}", bytename, self.layout[i].SemanticIndex)?; if i < self.layout.len() - 1 { write!(f, ", ")?; } diff --git a/Native/types/src/d3ddata.rs b/Native/types/src/d3ddata.rs index 042dfa29..0d99c865 100644 --- a/Native/types/src/d3ddata.rs +++ b/Native/types/src/d3ddata.rs @@ -1,6 +1,7 @@ use winapi::shared::d3d9::*; use winapi::um::d3d11::{ID3D11Buffer, ID3D11InputLayout, ID3D11Texture2D, ID3D11Resource, ID3D11ShaderResourceView}; +use shared_dx::dx11rs::SemanticMask; pub struct ModD3DData9 { pub vb: *mut IDirect3DVertexBuffer9, @@ -73,6 +74,11 @@ impl Drop for ModD3DData9 { pub struct ModD3DData11 { pub vb: *mut ID3D11Buffer, pub vlayout: *mut ID3D11InputLayout, + /// Semantic-name/index bitmask of the layout that was used to fill `vb`. + /// Zero means "not tracked" — the refill check treats that as "don't + /// refill" so any code path that forgets to set this remains compatible + /// with the pre-refill behavior. + pub vlayout_semantic_mask: SemanticMask, pub textures: [*mut ID3D11Texture2D; 4], pub has_textures: bool, pub srvs: [*mut ID3D11ShaderResourceView; 4], @@ -106,6 +112,7 @@ impl Clone for ModD3DData11 { Self { vb: self.vb, vlayout: self.vlayout, + vlayout_semantic_mask: self.vlayout_semantic_mask, textures: self.textures, has_textures: self.has_textures, srvs: self.srvs, @@ -122,6 +129,7 @@ impl ModD3DData11 { Self { vb: null_mut(), vlayout: null_mut(), + vlayout_semantic_mask: 0, textures: [null_mut(); 4], has_textures: false, srvs: [null_mut(); 4], @@ -136,6 +144,7 @@ impl ModD3DData11 { Self { vb: null_mut(), vlayout: layout, + vlayout_semantic_mask: 0, textures: [null_mut(); 4], has_textures: false, srvs: [null_mut(); 4], @@ -156,6 +165,7 @@ impl ModD3DData11 { //if rc == 0 { util::write_log_file("releasing vlayout on d3d11 data");} self.vlayout = std::ptr::null_mut(); } + self.vlayout_semantic_mask = 0; for srv in self.srvs.iter_mut() { if !srv.is_null() { let bsrv = *srv as *mut ID3D11Resource; From 90c45b1848edbb1266ffd6b9ae4ff600fc3ac92f Mon Sep 17 00:00:00 2001 From: John Quigley Date: Tue, 12 May 2026 18:53:49 -0400 Subject: [PATCH 2/2] Tweak comment --- Native/types/src/d3ddata.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Native/types/src/d3ddata.rs b/Native/types/src/d3ddata.rs index 0d99c865..321c9388 100644 --- a/Native/types/src/d3ddata.rs +++ b/Native/types/src/d3ddata.rs @@ -76,8 +76,7 @@ pub struct ModD3DData11 { pub vlayout: *mut ID3D11InputLayout, /// Semantic-name/index bitmask of the layout that was used to fill `vb`. /// Zero means "not tracked" — the refill check treats that as "don't - /// refill" so any code path that forgets to set this remains compatible - /// with the pre-refill behavior. + /// refill". pub vlayout_semantic_mask: SemanticMask, pub textures: [*mut ID3D11Texture2D; 4], pub has_textures: bool,