Skip to main content

siglus_cfx_decompiler/
ctab.rs

1use std::fmt;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum RegisterSet {
5    Bool,
6    Int4,
7    Float4,
8    Sampler,
9    Unknown(u16),
10}
11
12impl RegisterSet {
13    pub fn from_u16(v: u16) -> Self {
14        match v {
15            0 => Self::Bool,
16            1 => Self::Int4,
17            2 => Self::Float4,
18            3 => Self::Sampler,
19            x => Self::Unknown(x),
20        }
21    }
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum TypeClass {
26    Scalar,
27    Vector,
28    MatrixRows,
29    MatrixColumns,
30    Object,
31    Struct,
32    Unknown(u16),
33}
34
35impl TypeClass {
36    pub fn from_u16(v: u16) -> Self {
37        match v {
38            0 => Self::Scalar,
39            1 => Self::Vector,
40            2 => Self::MatrixRows,
41            3 => Self::MatrixColumns,
42            4 => Self::Object,
43            5 => Self::Struct,
44            x => Self::Unknown(x),
45        }
46    }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum ValueType {
51    Void,
52    Bool,
53    Int,
54    Float,
55    String,
56    Texture,
57    Texture1D,
58    Texture2D,
59    Texture3D,
60    TextureCube,
61    Sampler,
62    Sampler1D,
63    Sampler2D,
64    Sampler3D,
65    SamplerCube,
66    PixelShader,
67    VertexShader,
68    PixelFragment,
69    VertexFragment,
70    Unsupported,
71    Unknown(u16),
72}
73
74impl ValueType {
75    pub fn from_u16(v: u16) -> Self {
76        match v {
77            0 => Self::Void,
78            1 => Self::Bool,
79            2 => Self::Int,
80            3 => Self::Float,
81            4 => Self::String,
82            5 => Self::Texture,
83            6 => Self::Texture1D,
84            7 => Self::Texture2D,
85            8 => Self::Texture3D,
86            9 => Self::TextureCube,
87            10 => Self::Sampler,
88            11 => Self::Sampler1D,
89            12 => Self::Sampler2D,
90            13 => Self::Sampler3D,
91            14 => Self::SamplerCube,
92            15 => Self::PixelShader,
93            16 => Self::VertexShader,
94            17 => Self::PixelFragment,
95            18 => Self::VertexFragment,
96            19 => Self::Unsupported,
97            x => Self::Unknown(x),
98        }
99    }
100}
101
102#[derive(Debug, Clone)]
103pub struct StructMemberInfo {
104    pub name: String,
105    pub type_info: TypeInfo,
106}
107
108#[derive(Debug, Clone)]
109pub struct TypeInfo {
110    pub class: TypeClass,
111    pub value_type: ValueType,
112    pub rows: u16,
113    pub columns: u16,
114    pub elements: u16,
115    pub struct_members: u16,
116    pub struct_member_info_offset: u32,
117    pub members: Vec<StructMemberInfo>,
118}
119
120impl TypeInfo {
121    pub fn hlsl_type_name(&self) -> String {
122        if self.class == TypeClass::Struct {
123            return "struct".to_string();
124        }
125        match self.value_type {
126            ValueType::Sampler | ValueType::Sampler2D => "sampler2D".to_string(),
127            ValueType::Sampler1D => "sampler1D".to_string(),
128            ValueType::Sampler3D => "sampler3D".to_string(),
129            ValueType::SamplerCube => "samplerCUBE".to_string(),
130            ValueType::Texture | ValueType::Texture2D => "texture".to_string(),
131            ValueType::Texture1D => "texture1D".to_string(),
132            ValueType::Texture3D => "texture3D".to_string(),
133            ValueType::TextureCube => "textureCUBE".to_string(),
134            ValueType::Bool => vector_or_matrix_type("bool", self.rows, self.columns, self.class),
135            ValueType::Int => vector_or_matrix_type("int", self.rows, self.columns, self.class),
136            ValueType::Float => vector_or_matrix_type("float", self.rows, self.columns, self.class),
137            _ => "float4".to_string(),
138        }
139    }
140}
141
142#[derive(Debug, Clone)]
143pub struct ConstantInfo {
144    pub name: String,
145    pub register_set: RegisterSet,
146    pub register_index: u16,
147    pub register_count: u16,
148    pub type_info: Option<TypeInfo>,
149}
150
151impl ConstantInfo {
152    pub fn register_name(&self) -> String {
153        match self.register_set {
154            RegisterSet::Bool => format!("b{}", self.register_index),
155            RegisterSet::Int4 => format!("i{}", self.register_index),
156            RegisterSet::Float4 => format!("c{}", self.register_index),
157            RegisterSet::Sampler => format!("s{}", self.register_index),
158            RegisterSet::Unknown(_) => format!("u{}", self.register_index),
159        }
160    }
161
162    pub fn hlsl_decl_type(&self) -> String {
163        let Some(t) = &self.type_info else {
164            return match self.register_set {
165                RegisterSet::Sampler => "sampler2D".to_string(),
166                RegisterSet::Bool => "bool".to_string(),
167                RegisterSet::Int4 => "int4".to_string(),
168                RegisterSet::Float4 => "float4".to_string(),
169                RegisterSet::Unknown(_) => "float4".to_string(),
170            };
171        };
172
173        if t.class == TypeClass::Struct {
174            return format!("{}_type", sanitize_type_name(&self.name));
175        }
176
177        match t.value_type {
178            ValueType::Sampler | ValueType::Sampler2D => "sampler2D".to_string(),
179            ValueType::Sampler1D => "sampler1D".to_string(),
180            ValueType::Sampler3D => "sampler3D".to_string(),
181            ValueType::SamplerCube => "samplerCUBE".to_string(),
182            ValueType::Texture | ValueType::Texture2D => "texture".to_string(),
183            ValueType::Texture1D => "texture1D".to_string(),
184            ValueType::Texture3D => "texture3D".to_string(),
185            ValueType::TextureCube => "textureCUBE".to_string(),
186            ValueType::Bool => vector_or_matrix_type("bool", t.rows, t.columns, t.class),
187            ValueType::Int => vector_or_matrix_type("int", t.rows, t.columns, t.class),
188            ValueType::Float => vector_or_matrix_type("float", t.rows, t.columns, t.class),
189            _ => match self.register_set {
190                RegisterSet::Sampler => "sampler2D".to_string(),
191                RegisterSet::Bool => "bool".to_string(),
192                RegisterSet::Int4 => "int4".to_string(),
193                RegisterSet::Float4 => "float4".to_string(),
194                RegisterSet::Unknown(_) => "float4".to_string(),
195            },
196        }
197    }
198
199    pub fn struct_type_name(&self) -> Option<String> {
200        let t = self.type_info.as_ref()?;
201        if t.class == TypeClass::Struct {
202            Some(format!("{}_type", sanitize_type_name(&self.name)))
203        } else {
204            None
205        }
206    }
207}
208
209fn sanitize_type_name(name: &str) -> String {
210    let mut out = String::new();
211    for (i, ch) in name.chars().enumerate() {
212        let ok = if i == 0 {
213            ch == '_' || ch.is_ascii_alphabetic()
214        } else {
215            ch == '_' || ch.is_ascii_alphanumeric()
216        };
217        out.push(if ok { ch } else { '_' });
218    }
219    if out.is_empty() || out.chars().next().is_some_and(|c| c.is_ascii_digit()) {
220        format!("_{}", out)
221    } else {
222        out
223    }
224}
225
226fn vector_or_matrix_type(base: &str, rows: u16, columns: u16, class: TypeClass) -> String {
227    match class {
228        TypeClass::Scalar => base.to_string(),
229        TypeClass::Vector => format!("{}{}", base, columns.max(rows).max(1)),
230        TypeClass::MatrixRows | TypeClass::MatrixColumns => {
231            if rows > 1 && columns > 1 {
232                format!("{}{}x{}", base, rows, columns)
233            } else {
234                format!("{}4", base)
235            }
236        }
237        _ => {
238            if rows > 1 && columns > 1 {
239                format!("{}{}x{}", base, rows, columns)
240            } else if columns > 1 {
241                format!("{}{}", base, columns)
242            } else {
243                base.to_string()
244            }
245        }
246    }
247}
248
249#[derive(Debug, Clone)]
250pub struct ConstantTable {
251    pub creator: Option<String>,
252    pub version: u32,
253    pub flags: u32,
254    pub target: Option<String>,
255    pub constants: Vec<ConstantInfo>,
256}
257
258impl ConstantTable {
259    pub fn find_register_name(&self, register_set: RegisterSet, index: u16) -> Option<&str> {
260        self.constants
261            .iter()
262            .find(|c| c.register_set == register_set && c.register_index == index)
263            .map(|c| c.name.as_str())
264    }
265
266    pub fn hlsl_uniforms(&self) -> String {
267        let mut out = String::new();
268
269        for c in &self.constants {
270            if let (Some(type_name), Some(type_info)) = (c.struct_type_name(), c.type_info.as_ref())
271            {
272                out.push_str(&format!("struct {} {{\n", type_name));
273                for m in &type_info.members {
274                    out.push_str(&format!(
275                        "    {} {};\n",
276                        m.type_info.hlsl_type_name(),
277                        m.name
278                    ));
279                }
280                out.push_str("};\n");
281            }
282        }
283
284        for c in &self.constants {
285            let ty = c.hlsl_decl_type();
286            let _reg = c.register_name();
287            out.push_str(&format!("uniform {} {};\n", ty, c.name));
288        }
289        out
290    }
291}
292
293#[derive(Debug, Clone)]
294pub enum CtabError {
295    TooSmall,
296    BadMagic,
297    OutOfBounds(&'static str, usize),
298    InvalidStringOffset(u32),
299}
300
301impl fmt::Display for CtabError {
302    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303        match self {
304            Self::TooSmall => write!(f, "CTAB payload is too small"),
305            Self::BadMagic => write!(f, "CTAB magic is missing"),
306            Self::OutOfBounds(what, off) => {
307                write!(f, "{} is out of bounds at offset {}", what, off)
308            }
309            Self::InvalidStringOffset(off) => write!(f, "invalid CTAB string offset {}", off),
310        }
311    }
312}
313
314impl std::error::Error for CtabError {}
315
316fn read_u16(data: &[u8], off: usize) -> Result<u16, CtabError> {
317    let b = data
318        .get(off..off + 2)
319        .ok_or(CtabError::OutOfBounds("u16", off))?;
320    Ok(u16::from_le_bytes([b[0], b[1]]))
321}
322
323fn read_u32(data: &[u8], off: usize) -> Result<u32, CtabError> {
324    let b = data
325        .get(off..off + 4)
326        .ok_or(CtabError::OutOfBounds("u32", off))?;
327    Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
328}
329
330fn read_c_string(data: &[u8], off: u32) -> Result<String, CtabError> {
331    let off = off as usize;
332    if off >= data.len() {
333        return Err(CtabError::InvalidStringOffset(off as u32));
334    }
335    let mut end = off;
336    while end < data.len() && data[end] != 0 {
337        end += 1;
338    }
339    Ok(String::from_utf8_lossy(&data[off..end]).to_string())
340}
341
342fn parse_type_info(data: &[u8], off: usize, depth: usize) -> Result<TypeInfo, CtabError> {
343    if depth > 16 {
344        return Err(CtabError::OutOfBounds("recursive type info", off));
345    }
346    if off + 16 > data.len() {
347        return Err(CtabError::OutOfBounds("type info", off));
348    }
349
350    let class = TypeClass::from_u16(read_u16(data, off)?);
351    let value_type = ValueType::from_u16(read_u16(data, off + 2)?);
352    let rows = read_u16(data, off + 4)?;
353    let columns = read_u16(data, off + 6)?;
354    let elements = read_u16(data, off + 8)?;
355    let struct_members = read_u16(data, off + 10)?;
356    let struct_member_info_offset = read_u32(data, off + 12)?;
357
358    let mut members = Vec::new();
359    if class == TypeClass::Struct && struct_member_info_offset != 0 {
360        let member_base = struct_member_info_offset as usize;
361        for idx in 0..struct_members as usize {
362            let m_off = member_base + idx * 8;
363            if m_off + 8 > data.len() {
364                break;
365            }
366            let name_off = read_u32(data, m_off)?;
367            let type_off = read_u32(data, m_off + 4)? as usize;
368            let name = read_c_string(data, name_off).unwrap_or_else(|_| format!("member_{}", idx));
369            if type_off != 0 && type_off + 16 <= data.len() {
370                let type_info = parse_type_info(data, type_off, depth + 1)?;
371                members.push(StructMemberInfo { name, type_info });
372            }
373        }
374    }
375
376    Ok(TypeInfo {
377        class,
378        value_type,
379        rows,
380        columns,
381        elements,
382        struct_members,
383        struct_member_info_offset,
384        members,
385    })
386}
387
388pub fn parse_ctab(payload: &[u8]) -> Result<ConstantTable, CtabError> {
389    if payload.len() < 28 {
390        return Err(CtabError::TooSmall);
391    }
392    if payload.get(0..4) != Some(b"CTAB") {
393        return Err(CtabError::BadMagic);
394    }
395
396    let size = read_u32(payload, 4)? as usize;
397    let table_len = if size >= 28 && size <= payload.len() {
398        size
399    } else {
400        payload.len()
401    };
402    let data = &payload[..table_len];
403
404    let creator_off = read_u32(data, 8)?;
405    let version = read_u32(data, 12)?;
406    let constants_count = read_u32(data, 16)? as usize;
407    let constant_info_off = read_u32(data, 20)? as usize;
408    let flags = read_u32(data, 24)?;
409    let target_off = if data.len() >= 32 {
410        read_u32(data, 28).unwrap_or(0)
411    } else {
412        0
413    };
414
415    let creator = if creator_off != 0 {
416        read_c_string(data, creator_off).ok()
417    } else {
418        None
419    };
420    let target = if target_off != 0 {
421        read_c_string(data, target_off).ok()
422    } else {
423        None
424    };
425
426    let mut constants = Vec::with_capacity(constants_count);
427    for idx in 0..constants_count {
428        let off = constant_info_off + idx * 20;
429        if off + 20 > data.len() {
430            break;
431        }
432
433        let name_off = read_u32(data, off)?;
434        let register_set = RegisterSet::from_u16(read_u16(data, off + 4)?);
435        let register_index = read_u16(data, off + 6)?;
436        let register_count = read_u16(data, off + 8)?;
437        let type_info_off = read_u32(data, off + 12)? as usize;
438
439        let name = read_c_string(data, name_off).unwrap_or_else(|_| format!("unnamed_{}", idx));
440        let type_info = if type_info_off != 0 && type_info_off + 16 <= data.len() {
441            parse_type_info(data, type_info_off, 0).ok()
442        } else {
443            None
444        };
445
446        constants.push(ConstantInfo {
447            name,
448            register_set,
449            register_index,
450            register_count,
451            type_info,
452        });
453    }
454
455    Ok(ConstantTable {
456        creator,
457        version,
458        flags,
459        target,
460        constants,
461    })
462}