Skip to main content

siglus_cfx_decompiler/
effect.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt::Write as _;
3use std::path::Path;
4
5use crate::cfx::ShaderBlob;
6use crate::disasm::ShaderKind;
7
8const FX_MAGIC_LE: u32 = 0xfeff_0901;
9const FX_MAGIC_BYTESWAPPED: u32 = 0x0109_fffe;
10const D3DXPC_SCALAR: u32 = 0;
11const D3DXPC_VECTOR: u32 = 1;
12const D3DXPC_MATRIX_ROWS: u32 = 2;
13const D3DXPC_MATRIX_COLUMNS: u32 = 3;
14const D3DXPC_OBJECT: u32 = 4;
15const D3DXPC_STRUCT: u32 = 5;
16
17const D3DXPT_STRING: u32 = 4;
18const D3DXPT_TEXTURE: u32 = 5;
19const D3DXPT_TEXTURE1D: u32 = 6;
20const D3DXPT_TEXTURE2D: u32 = 7;
21const D3DXPT_TEXTURE3D: u32 = 8;
22const D3DXPT_TEXTURECUBE: u32 = 9;
23const D3DXPT_SAMPLER: u32 = 10;
24const D3DXPT_SAMPLER1D: u32 = 11;
25const D3DXPT_SAMPLER2D: u32 = 12;
26const D3DXPT_SAMPLER3D: u32 = 13;
27const D3DXPT_SAMPLERCUBE: u32 = 14;
28const D3DXPT_PIXELSHADER: u32 = 15;
29const D3DXPT_VERTEXSHADER: u32 = 16;
30
31#[derive(Debug, Clone)]
32pub struct EffectFile {
33    pub tag: u32,
34    pub table_base: usize,
35    pub start_offset: usize,
36    pub parameter_count: u32,
37    pub technique_count: u32,
38    pub object_count: u32,
39    pub string_count: u32,
40    pub resource_count: u32,
41    pub parameters: Vec<EffectParameter>,
42    pub techniques: Vec<Technique>,
43    pub objects: Vec<EffectObject>,
44    pub notes: Vec<String>,
45}
46
47#[derive(Debug, Clone)]
48pub struct EffectParameter {
49    pub index: usize,
50    pub name: String,
51    pub semantic: String,
52    pub value_type: u32,
53    pub class: u32,
54    pub rows: u32,
55    pub columns: u32,
56    pub elements: u32,
57    pub bytes: u32,
58    pub flags: u32,
59    pub object_id: Option<u32>,
60}
61
62#[derive(Debug, Clone)]
63pub struct Technique {
64    pub index: usize,
65    pub name: String,
66    pub annotation_count: u32,
67    pub passes: Vec<Pass>,
68}
69
70#[derive(Debug, Clone)]
71pub struct Pass {
72    pub index: usize,
73    pub name: String,
74    pub annotation_count: u32,
75    pub states: Vec<State>,
76    pub vertex_shader: Option<ShaderRef>,
77    pub pixel_shader: Option<ShaderRef>,
78}
79
80#[derive(Debug, Clone)]
81pub struct State {
82    pub index: usize,
83    pub operation: u32,
84    pub operation_name: String,
85    pub class_name: String,
86    pub state_index: u32,
87    pub typedef_offset: u32,
88    pub value_offset: u32,
89    pub parameter: EffectParameter,
90    pub value: StateValue,
91    pub resource_usage: Option<u32>,
92}
93
94#[derive(Debug, Clone)]
95pub enum StateValue {
96    Empty,
97    ObjectId(u32),
98    Int(Vec<i32>),
99    Float(Vec<f32>),
100    Bool(Vec<bool>),
101    StringObject { object_id: u32, text: Option<String> },
102    Raw { offset: u32, bytes: usize },
103}
104
105#[derive(Debug, Clone)]
106pub struct ShaderRef {
107    pub kind: ShaderKind,
108    pub object_id: Option<u32>,
109    pub object_data_offset: Option<usize>,
110    pub object_size: usize,
111    pub shader_index: Option<usize>,
112    pub shader_offset: Option<usize>,
113    pub unresolved_reason: Option<String>,
114}
115
116#[derive(Debug, Clone, Default)]
117pub struct EffectObject {
118    pub id: usize,
119    pub data_offset: Option<usize>,
120    pub size: usize,
121    pub owner_name: Option<String>,
122    pub owner_type: Option<u32>,
123}
124
125struct Reader<'a> {
126    data: &'a [u8],
127    pos: usize,
128}
129
130impl<'a> Reader<'a> {
131    fn new(data: &'a [u8], pos: usize) -> Self {
132        Self { data, pos }
133    }
134
135    fn read_u32(&mut self) -> Result<u32, String> {
136        let off = self.pos;
137        let b = self
138            .data
139            .get(off..off + 4)
140            .ok_or_else(|| format!("truncated dword at 0x{off:x}"))?;
141        self.pos += 4;
142        Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
143    }
144
145    fn skip_dwords(&mut self, count: u32) -> Result<(), String> {
146        let bytes = (count as usize)
147            .checked_mul(4)
148            .ok_or_else(|| "dword skip overflow".to_string())?;
149        let new_pos = self
150            .pos
151            .checked_add(bytes)
152            .ok_or_else(|| "reader overflow".to_string())?;
153        if new_pos > self.data.len() {
154            return Err(format!("skip beyond EOF: 0x{:x} > 0x{:x}", new_pos, self.data.len()));
155        }
156        self.pos = new_pos;
157        Ok(())
158    }
159}
160
161#[derive(Debug, Clone)]
162struct TypeDef {
163    name: String,
164    semantic: String,
165    value_type: u32,
166    class: u32,
167    rows: u32,
168    columns: u32,
169    elements: u32,
170    member_count: u32,
171    bytes: u32,
172}
173
174pub fn parse_effect(data: &[u8], shaders: &[ShaderBlob]) -> Result<EffectFile, String> {
175    if data.len() < 8 {
176        return Err("file is too short for a D3DX effect header".to_string());
177    }
178
179    let tag = read_u32_at(data, 0)?;
180    if tag != FX_MAGIC_LE && tag != FX_MAGIC_BYTESWAPPED {
181        return Err(format!("not a D3DX9 compiled effect tag: 0x{tag:08x}"));
182    }
183
184    let table_base = 8usize;
185    let start_rel = read_u32_at(data, 4)? as usize;
186    let start_offset = table_base
187        .checked_add(start_rel)
188        .ok_or_else(|| "effect start offset overflow".to_string())?;
189    if start_offset + 16 > data.len() {
190        return Err(format!("effect start offset outside file: 0x{start_offset:x}"));
191    }
192
193    let mut notes = Vec::new();
194    let mut objects = Vec::new();
195    let mut r = Reader::new(data, start_offset);
196    let parameter_count = r.read_u32()?;
197    let technique_count = r.read_u32()?;
198    let unknown = r.read_u32()?;
199    let object_count = r.read_u32()?;
200    if unknown != 0 {
201        notes.push(format!("effect header unknown dword is 0x{unknown:08x}"));
202    }
203    for id in 0..object_count as usize {
204        objects.push(EffectObject { id, ..EffectObject::default() });
205    }
206
207    let mut parameters = Vec::new();
208    for index in 0..parameter_count as usize {
209        let parameter = parse_top_level_parameter(data, table_base, &mut r, index, &mut objects)?;
210        parameters.push(parameter);
211    }
212
213    let mut techniques = Vec::new();
214    for index in 0..technique_count as usize {
215        let technique = parse_technique(data, table_base, &mut r, index, &mut objects)?;
216        techniques.push(technique);
217    }
218
219    // D3DX9 compiled-effect order is: string_count, resource_count,
220    // string object payloads, then resource payloads.  Reading string
221    // payloads before resource_count shifts the stream and makes pass
222    // shader objects impossible to resolve.
223    let string_count = if r.pos + 4 <= data.len() { r.read_u32()? } else { 0 };
224    let resource_count = if r.pos + 4 <= data.len() { r.read_u32()? } else { 0 };
225
226    for _ in 0..string_count {
227        let object_id = r.read_u32()? as usize;
228        copy_object_data(data, &mut r, object_id, &mut objects, &mut notes)?;
229    }
230
231    for resource_index in 0..resource_count {
232        match parse_resource_like(data, &mut r, &mut techniques, &mut parameters, &mut objects, &mut notes) {
233            Ok(()) => {}
234            Err(e) => {
235                notes.push(format!("resource {resource_index} parse stopped: {e}"));
236                break;
237            }
238        }
239    }
240
241    resolve_shader_refs(&mut techniques, &objects, shaders);
242
243    Ok(EffectFile {
244        tag,
245        table_base,
246        start_offset,
247        parameter_count,
248        technique_count,
249        object_count,
250        string_count,
251        resource_count,
252        parameters,
253        techniques,
254        objects,
255        notes,
256    })
257}
258
259fn parse_top_level_parameter(
260    data: &[u8],
261    base: usize,
262    r: &mut Reader<'_>,
263    index: usize,
264    objects: &mut [EffectObject],
265) -> Result<EffectParameter, String> {
266    let typedef_offset = r.read_u32()?;
267    let value_offset = r.read_u32()?;
268    let flags = r.read_u32()?;
269    let annotation_count = r.read_u32()?;
270    let td = parse_typedef_at(data, base, typedef_offset)?;
271    let value = parse_value_at(data, base, value_offset, &td, objects)?;
272    let mut param = parameter_from_typedef(index, td, flags, value.object_id());
273    if let Some(id) = param.object_id {
274        if let Some(obj) = objects.get_mut(id as usize) {
275            obj.owner_name = Some(param.name.clone());
276            obj.owner_type = Some(param.value_type);
277        }
278    }
279    r.skip_dwords(annotation_count.saturating_mul(2))?;
280    Ok(param)
281}
282
283fn parse_technique(
284    data: &[u8],
285    base: usize,
286    r: &mut Reader<'_>,
287    index: usize,
288    objects: &mut [EffectObject],
289) -> Result<Technique, String> {
290    let name_offset = r.read_u32()?;
291    let name = read_name_at(data, base, name_offset).unwrap_or_else(|_| format!("technique_{index}"));
292    let annotation_count = r.read_u32()?;
293    let pass_count = r.read_u32()?;
294    r.skip_dwords(annotation_count.saturating_mul(2))?;
295
296    let mut passes = Vec::new();
297    for pass_index in 0..pass_count as usize {
298        passes.push(parse_pass(data, base, r, pass_index, objects)?);
299    }
300
301    Ok(Technique { index, name, annotation_count, passes })
302}
303
304fn parse_pass(
305    data: &[u8],
306    base: usize,
307    r: &mut Reader<'_>,
308    index: usize,
309    objects: &mut [EffectObject],
310) -> Result<Pass, String> {
311    let name_offset = r.read_u32()?;
312    let name = read_name_at(data, base, name_offset).unwrap_or_else(|_| format!("pass{index}"));
313    let annotation_count = r.read_u32()?;
314    let state_count = r.read_u32()?;
315    r.skip_dwords(annotation_count.saturating_mul(2))?;
316
317    let mut states = Vec::new();
318    let mut vertex_shader = None;
319    let mut pixel_shader = None;
320    for state_index in 0..state_count as usize {
321        let state = parse_state(data, base, r, state_index, objects)?;
322        if is_vertex_shader_state(&state) {
323            vertex_shader = Some(ShaderRef {
324                kind: ShaderKind::Vertex,
325                object_id: state.value.object_id(),
326                object_data_offset: None,
327                object_size: 0,
328                shader_index: None,
329                shader_offset: None,
330                unresolved_reason: None,
331            });
332        } else if is_pixel_shader_state(&state) {
333            pixel_shader = Some(ShaderRef {
334                kind: ShaderKind::Pixel,
335                object_id: state.value.object_id(),
336                object_data_offset: None,
337                object_size: 0,
338                shader_index: None,
339                shader_offset: None,
340                unresolved_reason: None,
341            });
342        }
343        states.push(state);
344    }
345
346    Ok(Pass { index, name, annotation_count, states, vertex_shader, pixel_shader })
347}
348
349fn parse_state(
350    data: &[u8],
351    base: usize,
352    r: &mut Reader<'_>,
353    index: usize,
354    objects: &mut [EffectObject],
355) -> Result<State, String> {
356    let operation = r.read_u32()?;
357    let state_index = r.read_u32()?;
358    let typedef_offset = r.read_u32()?;
359    let td = parse_typedef_at(data, base, typedef_offset)?;
360    let value_offset = r.read_u32()?;
361    let parsed = parse_value_at(data, base, value_offset, &td, objects)?;
362    let parameter = parameter_from_typedef(index, td, 0, parsed.object_id());
363    let (operation_name, class_name) = operation_info(operation);
364    Ok(State {
365        index,
366        operation,
367        operation_name: operation_name.to_string(),
368        class_name: class_name.to_string(),
369        state_index,
370        typedef_offset,
371        value_offset,
372        parameter,
373        value: parsed.value,
374        resource_usage: None,
375    })
376}
377
378#[derive(Debug, Clone)]
379struct ParsedValue {
380    value: StateValue,
381}
382
383impl ParsedValue {
384    fn object_id(&self) -> Option<u32> {
385        self.value.object_id()
386    }
387}
388
389impl StateValue {
390    pub fn object_id(&self) -> Option<u32> {
391        match self {
392            StateValue::ObjectId(id) => Some(*id),
393            StateValue::StringObject { object_id, .. } => Some(*object_id),
394            _ => None,
395        }
396    }
397}
398
399fn parse_value_at(
400    data: &[u8],
401    base: usize,
402    value_offset: u32,
403    td: &TypeDef,
404    objects: &mut [EffectObject],
405) -> Result<ParsedValue, String> {
406    let off = checked_base_offset(base, value_offset)?;
407    if off > data.len() {
408        return Err(format!("value offset outside file: 0x{off:x}"));
409    }
410
411    let value = if td.class == D3DXPC_OBJECT || is_object_type(td.value_type) {
412        if off + 4 > data.len() {
413            StateValue::Empty
414        } else {
415            let object_id = read_u32_at(data, off)?;
416            if let Some(obj) = objects.get_mut(object_id as usize) {
417                if !td.name.is_empty() {
418                    obj.owner_name = Some(td.name.clone());
419                }
420                obj.owner_type = Some(td.value_type);
421            }
422            if td.value_type == D3DXPT_STRING {
423                let text = objects.get(object_id as usize)
424                    .and_then(|o| o.data_offset)
425                    .and_then(|pos| read_nul_string(data, pos).ok());
426                StateValue::StringObject { object_id, text }
427            } else {
428                StateValue::ObjectId(object_id)
429            }
430        }
431    } else if td.value_type == 3 {
432        let count = scalar_count(td).min(256);
433        let mut values = Vec::new();
434        for i in 0..count {
435            let p = off + (i as usize) * 4;
436            if p + 4 <= data.len() {
437                values.push(f32::from_le_bytes([data[p], data[p + 1], data[p + 2], data[p + 3]]));
438            }
439        }
440        StateValue::Float(values)
441    } else if td.value_type == 2 {
442        let count = scalar_count(td).min(256);
443        let mut values = Vec::new();
444        for i in 0..count {
445            let p = off + (i as usize) * 4;
446            if p + 4 <= data.len() {
447                values.push(i32::from_le_bytes([data[p], data[p + 1], data[p + 2], data[p + 3]]));
448            }
449        }
450        StateValue::Int(values)
451    } else if td.value_type == 1 {
452        let count = scalar_count(td).min(256);
453        let mut values = Vec::new();
454        for i in 0..count {
455            let p = off + (i as usize) * 4;
456            if p + 4 <= data.len() {
457                values.push(read_u32_at(data, p)? != 0);
458            }
459        }
460        StateValue::Bool(values)
461    } else {
462        StateValue::Raw { offset: value_offset, bytes: td.bytes as usize }
463    };
464
465    Ok(ParsedValue { value })
466}
467
468fn scalar_count(td: &TypeDef) -> u32 {
469    let elems = if td.elements == 0 { 1 } else { td.elements };
470    (td.rows.max(1)).saturating_mul(td.columns.max(1)).saturating_mul(elems)
471}
472
473fn parse_typedef_at(data: &[u8], base: usize, offset: u32) -> Result<TypeDef, String> {
474    let pos = checked_base_offset(base, offset)?;
475    let mut r = Reader::new(data, pos);
476    let value_type = r.read_u32()?;
477    let class = r.read_u32()?;
478    let name_offset = r.read_u32()?;
479    let name = read_name_at(data, base, name_offset).unwrap_or_default();
480    let semantic_offset = r.read_u32()?;
481    let semantic = read_name_at(data, base, semantic_offset).unwrap_or_default();
482    let elements = r.read_u32()?;
483
484    // Consume the same typedef fields as Wine/ReactOS d3dx_parse_effect_typedef().
485    // Scalar/vector/matrix typedefs all carry row/column words.  The previous build
486    // under-consumed scalar/vector typedefs, which shifted subsequent state parsing.
487    let (rows, columns, member_count, object_size) = match class {
488        D3DXPC_VECTOR => {
489            let columns = r.read_u32()?;
490            let rows = r.read_u32()?;
491            (rows, columns, 0, rows.saturating_mul(columns).saturating_mul(4))
492        }
493        D3DXPC_SCALAR | D3DXPC_MATRIX_ROWS | D3DXPC_MATRIX_COLUMNS => {
494            let rows = r.read_u32()?;
495            let columns = r.read_u32()?;
496            (rows, columns, 0, rows.saturating_mul(columns).saturating_mul(4))
497        }
498        D3DXPC_STRUCT => {
499            let members = r.read_u32()?;
500            (0, 0, members, 0)
501        }
502        D3DXPC_OBJECT => (0, 0, 0, 4),
503        _ => (0, 0, 0, 0),
504    };
505
506    let elem_count = if elements == 0 { 1 } else { elements };
507    let bytes = if class == D3DXPC_STRUCT {
508        0
509    } else if class == D3DXPC_OBJECT {
510        if matches!(value_type, D3DXPT_SAMPLER | D3DXPT_SAMPLER1D | D3DXPT_SAMPLER2D | D3DXPT_SAMPLER3D | D3DXPT_SAMPLERCUBE) {
511            0
512        } else {
513            object_size.saturating_mul(elem_count)
514        }
515    } else {
516        object_size.saturating_mul(elem_count)
517    };
518
519    Ok(TypeDef { name, semantic, value_type, class, rows, columns, elements, member_count, bytes })
520}
521
522fn parameter_from_typedef(index: usize, td: TypeDef, flags: u32, object_id: Option<u32>) -> EffectParameter {
523    EffectParameter {
524        index,
525        name: td.name,
526        semantic: td.semantic,
527        value_type: td.value_type,
528        class: td.class,
529        rows: td.rows,
530        columns: td.columns,
531        elements: td.elements,
532        bytes: td.bytes,
533        flags,
534        object_id,
535    }
536}
537
538fn copy_object_data(
539    data: &[u8],
540    r: &mut Reader<'_>,
541    object_id: usize,
542    objects: &mut [EffectObject],
543    notes: &mut Vec<String>,
544) -> Result<(), String> {
545    let size = r.read_u32()? as usize;
546    let start = r.pos;
547    let end = start
548        .checked_add(size)
549        .ok_or_else(|| "object data size overflow".to_string())?;
550    if end > data.len() {
551        return Err(format!("object {object_id} data outside file: 0x{end:x}"));
552    }
553    if let Some(obj) = objects.get_mut(object_id) {
554        obj.data_offset = Some(start);
555        obj.size = size;
556    } else {
557        notes.push(format!("object data references out-of-range object id {object_id}"));
558    }
559    r.pos = align4(end);
560    if r.pos > data.len() {
561        return Err(format!("object {object_id} aligned data outside file"));
562    }
563    Ok(())
564}
565
566fn parse_resource_like(
567    data: &[u8],
568    r: &mut Reader<'_>,
569    techniques: &mut [Technique],
570    parameters: &mut [EffectParameter],
571    objects: &mut [EffectObject],
572    notes: &mut Vec<String>,
573) -> Result<(), String> {
574    // Wine d3dx_parse_resource() reads five dwords identifying the target state or
575    // top-level sampler state, then d3dx9_copy_data() consumes a size-prefixed object
576    // payload for that state's parameter.object_id.
577    let rec_start = r.pos;
578    let technique_index = r.read_u32()?;
579    let index = r.read_u32()?;
580    let element_index = r.read_u32()?;
581    let state_index = r.read_u32()?;
582    let usage = r.read_u32()?;
583
584    let object_id = if technique_index == 0xffff_ffff {
585        let param = parameters
586            .get(index as usize)
587            .ok_or_else(|| format!("resource parameter index {index} outside parameter table"))?;
588        if element_index != 0xffff_ffff {
589            notes.push(format!(
590                "resource at 0x{rec_start:x} references parameter element {element_index}; using parent object id"
591            ));
592        }
593        param.object_id.ok_or_else(|| {
594            format!("resource at 0x{rec_start:x} top-level parameter {index} has no object id")
595        })?
596    } else {
597        let tech = techniques
598            .get_mut(technique_index as usize)
599            .ok_or_else(|| format!("resource technique index {technique_index} outside technique table"))?;
600        let pass = tech
601            .passes
602            .get_mut(index as usize)
603            .ok_or_else(|| format!("resource pass index {index} outside technique {technique_index}"))?;
604        let state = pass
605            .states
606            .get_mut(state_index as usize)
607            .ok_or_else(|| format!("resource state index {state_index} outside technique {technique_index} pass {index}"))?;
608        state.resource_usage = Some(usage);
609        state.parameter.object_id.ok_or_else(|| {
610            format!(
611                "resource at 0x{rec_start:x} technique {technique_index} pass {index} state {state_index} has no object id"
612            )
613        })?
614    };
615
616    copy_object_data(data, r, object_id as usize, objects, notes)?;
617    Ok(())
618}
619
620fn resolve_shader_refs(techniques: &mut [Technique], objects: &[EffectObject], shaders: &[ShaderBlob]) {
621    for tech in techniques {
622        for pass in &mut tech.passes {
623            if let Some(sr) = &mut pass.vertex_shader {
624                resolve_one_shader_ref(sr, objects, shaders);
625            }
626            if let Some(sr) = &mut pass.pixel_shader {
627                resolve_one_shader_ref(sr, objects, shaders);
628            }
629        }
630    }
631}
632
633fn resolve_one_shader_ref(sr: &mut ShaderRef, objects: &[EffectObject], shaders: &[ShaderBlob]) {
634    let Some(id) = sr.object_id else {
635        sr.unresolved_reason = Some("state did not contain an object id".to_string());
636        return;
637    };
638    let Some(obj) = objects.get(id as usize) else {
639        sr.unresolved_reason = Some(format!("object id {id} is outside object table"));
640        return;
641    };
642    sr.object_data_offset = obj.data_offset;
643    sr.object_size = obj.size;
644    let Some(data_off) = obj.data_offset else {
645        sr.unresolved_reason = Some(format!("object id {id} has no copied data payload"));
646        return;
647    };
648
649    let by_offset = shaders
650        .iter()
651        .find(|s| s.kind == sr.kind && s.offset == data_off);
652    let by_range = shaders.iter().find(|s| {
653        s.kind == sr.kind
654            && data_off >= s.offset
655            && data_off < s.end_offset
656            && s.offset >= data_off.saturating_sub(16)
657    });
658    let by_size = shaders.iter().find(|s| {
659        s.kind == sr.kind
660            && obj.size == s.bytes.len()
661            && obj.data_offset == Some(s.offset)
662    });
663    let matched = by_offset.or(by_size).or(by_range);
664    if let Some(shader) = matched {
665        sr.shader_index = Some(shader.index);
666        sr.shader_offset = Some(shader.offset);
667        sr.unresolved_reason = None;
668    } else {
669        sr.unresolved_reason = Some(format!(
670            "object id {id} payload offset=0x{data_off:x} size={} did not match any scanned {:?} shader",
671            obj.size, sr.kind
672        ));
673    }
674}
675
676fn is_object_type(t: u32) -> bool {
677    matches!(
678        t,
679        D3DXPT_STRING
680            | D3DXPT_TEXTURE
681            | D3DXPT_TEXTURE1D
682            | D3DXPT_TEXTURE2D
683            | D3DXPT_TEXTURE3D
684            | D3DXPT_TEXTURECUBE
685            | D3DXPT_SAMPLER
686            | D3DXPT_SAMPLER1D
687            | D3DXPT_SAMPLER2D
688            | D3DXPT_SAMPLER3D
689            | D3DXPT_SAMPLERCUBE
690            | D3DXPT_PIXELSHADER
691            | D3DXPT_VERTEXSHADER
692    )
693}
694
695fn is_vertex_shader_state(state: &State) -> bool {
696    state.operation == 146 || state.parameter.value_type == D3DXPT_VERTEXSHADER
697}
698
699fn is_pixel_shader_state(state: &State) -> bool {
700    state.operation == 147 || state.parameter.value_type == D3DXPT_PIXELSHADER
701}
702
703pub fn operation_info(op: u32) -> (&'static str, &'static str) {
704    match op {
705        0 => ("ZEnable", "RenderState"),
706        1 => ("FillMode", "RenderState"),
707        2 => ("ShadeMode", "RenderState"),
708        3 => ("ZWriteEnable", "RenderState"),
709        4 => ("AlphaTestEnable", "RenderState"),
710        5 => ("LastPixel", "RenderState"),
711        6 => ("SrcBlend", "RenderState"),
712        7 => ("DestBlend", "RenderState"),
713        8 => ("CullMode", "RenderState"),
714        9 => ("ZFunc", "RenderState"),
715        10 => ("AlphaRef", "RenderState"),
716        11 => ("AlphaFunc", "RenderState"),
717        12 => ("DitherEnable", "RenderState"),
718        13 => ("AlphaBlendEnable", "RenderState"),
719        14 => ("FogEnable", "RenderState"),
720        15 => ("SpecularEnable", "RenderState"),
721        16 => ("FogColor", "RenderState"),
722        17 => ("FogTableMode", "RenderState"),
723        18 => ("FogStart", "RenderState"),
724        19 => ("FogEnd", "RenderState"),
725        20 => ("FogDensity", "RenderState"),
726        21 => ("RangeFogEnable", "RenderState"),
727        22 => ("StencilEnable", "RenderState"),
728        23 => ("StencilFail", "RenderState"),
729        24 => ("StencilZFail", "RenderState"),
730        25 => ("StencilPass", "RenderState"),
731        26 => ("StencilFunc", "RenderState"),
732        27 => ("StencilRef", "RenderState"),
733        28 => ("StencilMask", "RenderState"),
734        29 => ("StencilWriteMask", "RenderState"),
735        30 => ("TextureFactor", "RenderState"),
736        31 => ("Wrap0", "RenderState"),
737        32 => ("Wrap1", "RenderState"),
738        33 => ("Wrap2", "RenderState"),
739        34 => ("Wrap3", "RenderState"),
740        35 => ("Wrap4", "RenderState"),
741        36 => ("Wrap5", "RenderState"),
742        37 => ("Wrap6", "RenderState"),
743        38 => ("Wrap7", "RenderState"),
744        39 => ("Clipping", "RenderState"),
745        40 => ("Lighting", "RenderState"),
746        41 => ("Ambient", "RenderState"),
747        42 => ("FogVertexMode", "RenderState"),
748        43 => ("ColorVertex", "RenderState"),
749        44 => ("LocalViewer", "RenderState"),
750        45 => ("NormalizeNormals", "RenderState"),
751        46 => ("DiffuseMaterialSource", "RenderState"),
752        47 => ("SpecularMaterialSource", "RenderState"),
753        48 => ("AmbientMaterialSource", "RenderState"),
754        49 => ("EmissiveMaterialSource", "RenderState"),
755        50 => ("VertexBlend", "RenderState"),
756        51 => ("ClipPlaneEnable", "RenderState"),
757        52 => ("PointSize", "RenderState"),
758        53 => ("PointSizeMin", "RenderState"),
759        54 => ("PointSpriteEnable", "RenderState"),
760        55 => ("PointScaleEnable", "RenderState"),
761        56 => ("PointScaleA", "RenderState"),
762        57 => ("PointScaleB", "RenderState"),
763        58 => ("PointScaleC", "RenderState"),
764        59 => ("MultiSampleAntiAlias", "RenderState"),
765        60 => ("MultiSampleMask", "RenderState"),
766        61 => ("PatchEdgeStyle", "RenderState"),
767        62 => ("DebugMonitorToken", "RenderState"),
768        63 => ("PointSizeMax", "RenderState"),
769        64 => ("IndexedVertexBlendEnable", "RenderState"),
770        65 => ("ColorWriteEnable", "RenderState"),
771        66 => ("TweenFactor", "RenderState"),
772        67 => ("BlendOp", "RenderState"),
773        68 => ("PositionDegree", "RenderState"),
774        69 => ("NormalDegree", "RenderState"),
775        70 => ("ScissorTestEnable", "RenderState"),
776        71 => ("SlopeScaleDepthBias", "RenderState"),
777        72 => ("AntiAliasedLineEnable", "RenderState"),
778        73 => ("MinTessellationLevel", "RenderState"),
779        74 => ("MaxTessellationLevel", "RenderState"),
780        75 => ("AdaptiveTessX", "RenderState"),
781        76 => ("AdaptiveTessY", "RenderState"),
782        77 => ("AdaptiveTessZ", "RenderState"),
783        78 => ("AdaptiveTessW", "RenderState"),
784        79 => ("EnableAdaptiveTessellation", "RenderState"),
785        80 => ("TwoSidedStencilMode", "RenderState"),
786        81 => ("CCWStencilFail", "RenderState"),
787        82 => ("CCWStencilZFail", "RenderState"),
788        83 => ("CCWStencilPass", "RenderState"),
789        84 => ("CCWStencilFunc", "RenderState"),
790        85 => ("ColorWriteEnable1", "RenderState"),
791        86 => ("ColorWriteEnable2", "RenderState"),
792        87 => ("ColorWriteEnable3", "RenderState"),
793        88 => ("BlendFactor", "RenderState"),
794        89 => ("SRGBWriteEnable", "RenderState"),
795        90 => ("DepthBias", "RenderState"),
796        91 => ("Wrap8", "RenderState"),
797        92 => ("Wrap9", "RenderState"),
798        93 => ("Wrap10", "RenderState"),
799        94 => ("Wrap11", "RenderState"),
800        95 => ("Wrap12", "RenderState"),
801        96 => ("Wrap13", "RenderState"),
802        97 => ("Wrap14", "RenderState"),
803        98 => ("Wrap15", "RenderState"),
804        99 => ("SeparateAlphaBlendEnable", "RenderState"),
805        100 => ("SrcBlendAlpha", "RenderState"),
806        101 => ("DestBlendAlpha", "RenderState"),
807        102 => ("BlendOpAlpha", "RenderState"),
808        146 => ("VertexShader", "VertexShader"),
809        147 => ("PixelShader", "PixelShader"),
810        _ => ("UnknownState", "Unknown"),
811    }
812}
813
814fn read_u32_at(data: &[u8], off: usize) -> Result<u32, String> {
815    let b = data
816        .get(off..off + 4)
817        .ok_or_else(|| format!("truncated dword at 0x{off:x}"))?;
818    Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
819}
820
821fn checked_base_offset(base: usize, offset: u32) -> Result<usize, String> {
822    base.checked_add(offset as usize)
823        .ok_or_else(|| format!("offset overflow: base=0x{base:x} offset=0x{offset:x}"))
824}
825
826fn read_name_at(data: &[u8], base: usize, offset: u32) -> Result<String, String> {
827    if offset == 0 {
828        return Ok(String::new());
829    }
830    let pos = checked_base_offset(base, offset)?;
831    let len = read_u32_at(data, pos)? as usize;
832    let start = pos + 4;
833    let end = start
834        .checked_add(len)
835        .ok_or_else(|| "name length overflow".to_string())?;
836    let bytes = data
837        .get(start..end)
838        .ok_or_else(|| format!("name outside file at 0x{pos:x} len={len}"))?;
839    let nul = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
840    Ok(String::from_utf8_lossy(&bytes[..nul]).to_string())
841}
842
843fn read_nul_string(data: &[u8], pos: usize) -> Result<String, String> {
844    let bytes = data.get(pos..).ok_or_else(|| format!("string offset outside file: 0x{pos:x}"))?;
845    let nul = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
846    Ok(String::from_utf8_lossy(&bytes[..nul]).to_string())
847}
848
849fn align4(v: usize) -> usize {
850    (v + 3) & !3
851}
852
853pub fn safe_name(name: &str, fallback: &str) -> String {
854    let mut out = String::new();
855    for ch in name.chars() {
856        if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '.' {
857            out.push(ch);
858        } else if ch.is_whitespace() {
859            out.push('_');
860        }
861    }
862    if out.is_empty() {
863        fallback.to_string()
864    } else {
865        out
866    }
867}
868
869pub fn format_effect_map(effect: &EffectFile, shaders: &[ShaderBlob]) -> String {
870    let mut s = String::new();
871    let _ = writeln!(s, "tag: 0x{:08x}", effect.tag);
872    let _ = writeln!(s, "table_base: 0x{:08x}", effect.table_base);
873    let _ = writeln!(s, "start_offset: 0x{:08x}", effect.start_offset);
874    let _ = writeln!(s, "parameters: {}", effect.parameter_count);
875    let _ = writeln!(s, "techniques: {}", effect.technique_count);
876    let _ = writeln!(s, "objects: {}", effect.object_count);
877    let _ = writeln!(s, "strings: {}", effect.string_count);
878    let _ = writeln!(s, "resources: {}", effect.resource_count);
879    if !effect.notes.is_empty() {
880        let _ = writeln!(s, "notes:");
881        for note in &effect.notes {
882            let _ = writeln!(s, "  - {note}");
883        }
884    }
885    let _ = writeln!(s);
886    let _ = writeln!(s, "top_level_parameters:");
887    for p in &effect.parameters {
888        let _ = writeln!(
889            s,
890            "  [{}] {} type={} class={} rows={} cols={} elems={} bytes={} object_id={:?} semantic={}",
891            p.index, p.name, p.value_type, p.class, p.rows, p.columns, p.elements, p.bytes, p.object_id, p.semantic
892        );
893    }
894    let _ = writeln!(s);
895    let _ = writeln!(s, "objects:");
896    for o in &effect.objects {
897        let _ = writeln!(
898            s,
899            "  [{}] owner={:?} type={:?} offset={} size={}",
900            o.id,
901            o.owner_name,
902            o.owner_type,
903            o.data_offset.map(|v| format!("0x{v:08x}")).unwrap_or_else(|| "none".to_string()),
904            o.size
905        );
906    }
907    let _ = writeln!(s);
908    let _ = writeln!(s, "shader_blobs:");
909    for sh in shaders {
910        let _ = writeln!(s, "  [{}] {} offset=0x{:08x} size={} ctab={}", sh.index, sh.profile(), sh.offset, sh.bytes.len(), sh.ctab.is_some());
911    }
912    let _ = writeln!(s);
913    let _ = writeln!(s, "techniques:");
914    for tech in &effect.techniques {
915        let _ = writeln!(s, "technique [{}] {} passes={}", tech.index, tech.name, tech.passes.len());
916        for pass in &tech.passes {
917            let _ = writeln!(s, "  pass [{}] {} states={}", pass.index, pass.name, pass.states.len());
918            if let Some(vs) = &pass.vertex_shader {
919                write_shader_ref(&mut s, "    VS", vs);
920            }
921            if let Some(ps) = &pass.pixel_shader {
922                write_shader_ref(&mut s, "    PS", ps);
923            }
924            for state in &pass.states {
925                let _ = writeln!(
926                    s,
927                    "    state [{}] op={} {} class={} index={} type={} param={} usage={:?} value={}",
928                    state.index,
929                    state.operation,
930                    state.operation_name,
931                    state.class_name,
932                    state.state_index,
933                    state.parameter.value_type,
934                    state.parameter.name,
935                    state.resource_usage,
936                    format_state_value(&state.value)
937                );
938            }
939        }
940    }
941    s
942}
943
944fn write_shader_ref(s: &mut String, label: &str, sr: &ShaderRef) {
945    let _ = writeln!(
946        s,
947        "{} object_id={:?} object_offset={} object_size={} shader_index={:?} shader_offset={} unresolved={:?}",
948        label,
949        sr.object_id,
950        sr.object_data_offset.map(|v| format!("0x{v:08x}")).unwrap_or_else(|| "none".to_string()),
951        sr.object_size,
952        sr.shader_index,
953        sr.shader_offset.map(|v| format!("0x{v:08x}")).unwrap_or_else(|| "none".to_string()),
954        sr.unresolved_reason
955    );
956}
957
958fn format_state_value(v: &StateValue) -> String {
959    match v {
960        StateValue::Empty => "empty".to_string(),
961        StateValue::ObjectId(id) => format!("object_id({id})"),
962        StateValue::Int(xs) => format!("int{:?}", xs),
963        StateValue::Float(xs) => format!("float{:?}", xs),
964        StateValue::Bool(xs) => format!("bool{:?}", xs),
965        StateValue::StringObject { object_id, text } => format!("string_object({object_id}, {:?})", text),
966        StateValue::Raw { offset, bytes } => format!("raw(offset=0x{offset:x}, bytes={bytes})"),
967    }
968}
969
970pub fn used_shader_indices(effect: &EffectFile) -> BTreeSet<usize> {
971    let mut out = BTreeSet::new();
972    for tech in &effect.techniques {
973        for pass in &tech.passes {
974            if let Some(vs) = &pass.vertex_shader {
975                if let Some(i) = vs.shader_index {
976                    out.insert(i);
977                }
978            }
979            if let Some(ps) = &pass.pixel_shader {
980                if let Some(i) = ps.shader_index {
981                    out.insert(i);
982                }
983            }
984        }
985    }
986    out
987}
988
989pub fn technique_shader_outputs<'a>(
990    effect: &'a EffectFile,
991    shaders: &'a [ShaderBlob],
992) -> Vec<(String, &'a ShaderBlob)> {
993    let by_index: BTreeMap<usize, &ShaderBlob> = shaders.iter().map(|s| (s.index, s)).collect();
994    let mut out = Vec::new();
995    for tech in &effect.techniques {
996        let tech_name = safe_name(&tech.name, &format!("technique_{}", tech.index));
997        for pass in &tech.passes {
998            let pass_name = safe_name(&pass.name, &format!("pass{}", pass.index));
999            let prefix_base = format!("t{:04}_{}__p{:02}_{}", tech.index, tech_name, pass.index, pass_name);
1000            if let Some(vs) = &pass.vertex_shader {
1001                if let Some(idx) = vs.shader_index {
1002                    if let Some(blob) = by_index.get(&idx) {
1003                        out.push((format!("{}__vs", prefix_base), *blob));
1004                    }
1005                }
1006            }
1007            if let Some(ps) = &pass.pixel_shader {
1008                if let Some(idx) = ps.shader_index {
1009                    if let Some(blob) = by_index.get(&idx) {
1010                        out.push((format!("{}__ps", prefix_base), *blob));
1011                    }
1012                }
1013            }
1014        }
1015    }
1016    out
1017}
1018
1019pub fn write_outputs_for_blob(
1020    out_dir: &Path,
1021    prefix: &str,
1022    blob: &ShaderBlob,
1023    hlsl: &str,
1024    asm: &str,
1025    ctab_text: Option<&str>,
1026) -> std::io::Result<()> {
1027    std::fs::write(out_dir.join("bytecode").join(format!("{prefix}.bin")), &blob.bytes)?;
1028    std::fs::write(out_dir.join("hlsl").join(format!("{prefix}.hlsl")), hlsl)?;
1029    std::fs::write(out_dir.join("asm").join(format!("{prefix}.asm")), asm)?;
1030    if let Some(ctab) = ctab_text {
1031        std::fs::write(out_dir.join("hlsl").join(format!("{prefix}.ctab.txt")), ctab)?;
1032    }
1033    Ok(())
1034}