Skip to main content

siglus_cfx_decompiler/
cfx.rs

1use crate::ctab::{parse_ctab, ConstantTable};
2use crate::disasm::{disassemble, ShaderKind};
3
4#[derive(Debug, Clone)]
5pub struct ShaderBlob {
6    pub index: usize,
7    pub kind: ShaderKind,
8    pub major: u8,
9    pub minor: u8,
10    pub offset: usize,
11    pub end_offset: usize,
12    pub bytes: Vec<u8>,
13    pub ctab: Option<ConstantTable>,
14}
15
16impl ShaderBlob {
17    pub fn profile(&self) -> String {
18        match self.kind {
19            ShaderKind::Pixel => format!("ps_{}_{}", self.major, self.minor),
20            ShaderKind::Vertex => format!("vs_{}_{}", self.major, self.minor),
21        }
22    }
23
24    pub fn file_prefix(&self) -> String {
25        let k = match self.kind {
26            ShaderKind::Pixel => "ps",
27            ShaderKind::Vertex => "vs",
28        };
29        format!("{}_{:04}_{:08x}", k, self.index, self.offset)
30    }
31}
32
33fn read_u32_le(data: &[u8], off: usize) -> Option<u32> {
34    let b = data.get(off..off + 4)?;
35    Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
36}
37
38fn decode_shader_version(tok: u32) -> Option<(ShaderKind, u8, u8)> {
39    let major = ((tok >> 8) & 0xff) as u8;
40    let minor = (tok & 0xff) as u8;
41
42    match tok & 0xffff_0000 {
43        0xffff_0000 if major == 2 => Some((ShaderKind::Pixel, major, minor)),
44        0xfffe_0000 if major == 2 => Some((ShaderKind::Vertex, major, minor)),
45        _ => None,
46    }
47}
48
49fn has_leading_ctab(data: &[u8], off: usize) -> bool {
50    let Some(comment_token) = read_u32_le(data, off + 4) else {
51        return false;
52    };
53
54    if (comment_token & 0xffff) != 0xfffe {
55        return false;
56    }
57
58    let comment_dwords = ((comment_token >> 16) & 0x7fff) as usize;
59    if comment_dwords < 1 {
60        return false;
61    }
62
63    data.get(off + 8..off + 12) == Some(b"CTAB")
64}
65
66fn is_real_shader_start(data: &[u8], off: usize) -> Option<(ShaderKind, u8, u8)> {
67    let version = read_u32_le(data, off)?;
68    let decoded = decode_shader_version(version)?;
69
70    if !has_leading_ctab(data, off) {
71        return None;
72    }
73
74    Some(decoded)
75}
76
77fn shader_end_by_end_token(data: &[u8], off: usize, hard_end: usize) -> usize {
78    let mut p = off + 4;
79    while p + 4 <= hard_end {
80        let Some(tok) = read_u32_le(data, p) else { break; };
81        let opcode = tok & 0xffff;
82
83        if opcode == 0xfffe {
84            let len = ((tok >> 16) & 0x7fff) as usize;
85            p = p.saturating_add(4).saturating_add(len.saturating_mul(4));
86            continue;
87        }
88
89        if opcode == 0xffff {
90            return (p + 4).min(hard_end);
91        }
92
93        let len = ((tok >> 24) & 0x0f) as usize;
94        if len == 0 {
95            p += 4;
96        } else {
97            p = p.saturating_add(4).saturating_add(len.saturating_mul(4));
98        }
99    }
100    hard_end
101}
102
103fn parse_leading_ctab(data: &[u8], off: usize, end: usize) -> Option<ConstantTable> {
104    let comment_token = read_u32_le(data, off + 4)?;
105    if (comment_token & 0xffff) != 0xfffe {
106        return None;
107    }
108    let dwords = ((comment_token >> 16) & 0x7fff) as usize;
109    let payload_start = off + 8;
110    let payload_end = payload_start.checked_add(dwords.checked_mul(4)?)?;
111    if payload_end > end || payload_end > data.len() {
112        return None;
113    }
114    parse_ctab(&data[payload_start..payload_end]).ok()
115}
116
117pub fn scan_shaders(data: &[u8]) -> Vec<ShaderBlob> {
118    let mut starts: Vec<(usize, ShaderKind, u8, u8)> = Vec::new();
119
120    let mut i = 0usize;
121    while i + 12 <= data.len() {
122        if let Some((kind, major, minor)) = is_real_shader_start(data, i) {
123            starts.push((i, kind, major, minor));
124            i += 4;
125        } else {
126            i += 1;
127        }
128    }
129
130    let mut out = Vec::with_capacity(starts.len());
131    for idx in 0..starts.len() {
132        let (off, kind, major, minor) = starts[idx];
133        let hard_end = if idx + 1 < starts.len() { starts[idx + 1].0 } else { data.len() };
134        let end = shader_end_by_end_token(data, off, hard_end);
135        let ctab = parse_leading_ctab(data, off, end);
136
137        out.push(ShaderBlob {
138            index: idx,
139            kind,
140            major,
141            minor,
142            offset: off,
143            end_offset: end,
144            bytes: data[off..end].to_vec(),
145            ctab,
146        });
147    }
148
149    out
150}
151
152pub fn disassemble_blob(blob: &ShaderBlob) -> String {
153    disassemble(&blob.bytes)
154}