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