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                out.push_str(&format!("struct {} {{\n", type_name));
272                for m in &type_info.members {
273                    out.push_str(&format!("    {} {};\n", m.type_info.hlsl_type_name(), m.name));
274                }
275                out.push_str("};\n");
276            }
277        }
278
279        for c in &self.constants {
280            let ty = c.hlsl_decl_type();
281            let reg = c.register_name();
282            out.push_str(&format!("uniform {} {};\n", ty, c.name));
283        }
284        out
285    }
286}
287
288#[derive(Debug, Clone)]
289pub enum CtabError {
290    TooSmall,
291    BadMagic,
292    OutOfBounds(&'static str, usize),
293    InvalidStringOffset(u32),
294}
295
296impl fmt::Display for CtabError {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        match self {
299            Self::TooSmall => write!(f, "CTAB payload is too small"),
300            Self::BadMagic => write!(f, "CTAB magic is missing"),
301            Self::OutOfBounds(what, off) => write!(f, "{} is out of bounds at offset {}", what, off),
302            Self::InvalidStringOffset(off) => write!(f, "invalid CTAB string offset {}", off),
303        }
304    }
305}
306
307impl std::error::Error for CtabError {}
308
309fn read_u16(data: &[u8], off: usize) -> Result<u16, CtabError> {
310    let b = data.get(off..off + 2).ok_or(CtabError::OutOfBounds("u16", off))?;
311    Ok(u16::from_le_bytes([b[0], b[1]]))
312}
313
314fn read_u32(data: &[u8], off: usize) -> Result<u32, CtabError> {
315    let b = data.get(off..off + 4).ok_or(CtabError::OutOfBounds("u32", off))?;
316    Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
317}
318
319fn read_c_string(data: &[u8], off: u32) -> Result<String, CtabError> {
320    let off = off as usize;
321    if off >= data.len() {
322        return Err(CtabError::InvalidStringOffset(off as u32));
323    }
324    let mut end = off;
325    while end < data.len() && data[end] != 0 {
326        end += 1;
327    }
328    Ok(String::from_utf8_lossy(&data[off..end]).to_string())
329}
330
331fn parse_type_info(data: &[u8], off: usize, depth: usize) -> Result<TypeInfo, CtabError> {
332    if depth > 16 {
333        return Err(CtabError::OutOfBounds("recursive type info", off));
334    }
335    if off + 16 > data.len() {
336        return Err(CtabError::OutOfBounds("type info", off));
337    }
338
339    let class = TypeClass::from_u16(read_u16(data, off)?);
340    let value_type = ValueType::from_u16(read_u16(data, off + 2)?);
341    let rows = read_u16(data, off + 4)?;
342    let columns = read_u16(data, off + 6)?;
343    let elements = read_u16(data, off + 8)?;
344    let struct_members = read_u16(data, off + 10)?;
345    let struct_member_info_offset = read_u32(data, off + 12)?;
346
347    let mut members = Vec::new();
348    if class == TypeClass::Struct && struct_member_info_offset != 0 {
349        let member_base = struct_member_info_offset as usize;
350        for idx in 0..struct_members as usize {
351            let m_off = member_base + idx * 8;
352            if m_off + 8 > data.len() {
353                break;
354            }
355            let name_off = read_u32(data, m_off)?;
356            let type_off = read_u32(data, m_off + 4)? as usize;
357            let name = read_c_string(data, name_off).unwrap_or_else(|_| format!("member_{}", idx));
358            if type_off != 0 && type_off + 16 <= data.len() {
359                let type_info = parse_type_info(data, type_off, depth + 1)?;
360                members.push(StructMemberInfo { name, type_info });
361            }
362        }
363    }
364
365    Ok(TypeInfo {
366        class,
367        value_type,
368        rows,
369        columns,
370        elements,
371        struct_members,
372        struct_member_info_offset,
373        members,
374    })
375}
376
377pub fn parse_ctab(payload: &[u8]) -> Result<ConstantTable, CtabError> {
378    if payload.len() < 28 {
379        return Err(CtabError::TooSmall);
380    }
381    if payload.get(0..4) != Some(b"CTAB") {
382        return Err(CtabError::BadMagic);
383    }
384
385    let size = read_u32(payload, 4)? as usize;
386    let table_len = if size >= 28 && size <= payload.len() { size } else { payload.len() };
387    let data = &payload[..table_len];
388
389    let creator_off = read_u32(data, 8)?;
390    let version = read_u32(data, 12)?;
391    let constants_count = read_u32(data, 16)? as usize;
392    let constant_info_off = read_u32(data, 20)? as usize;
393    let flags = read_u32(data, 24)?;
394    let target_off = if data.len() >= 32 { read_u32(data, 28).unwrap_or(0) } else { 0 };
395
396    let creator = if creator_off != 0 { read_c_string(data, creator_off).ok() } else { None };
397    let target = if target_off != 0 { read_c_string(data, target_off).ok() } else { None };
398
399    let mut constants = Vec::with_capacity(constants_count);
400    for idx in 0..constants_count {
401        let off = constant_info_off + idx * 20;
402        if off + 20 > data.len() {
403            break;
404        }
405
406        let name_off = read_u32(data, off)?;
407        let register_set = RegisterSet::from_u16(read_u16(data, off + 4)?);
408        let register_index = read_u16(data, off + 6)?;
409        let register_count = read_u16(data, off + 8)?;
410        let type_info_off = read_u32(data, off + 12)? as usize;
411
412        let name = read_c_string(data, name_off).unwrap_or_else(|_| format!("unnamed_{}", idx));
413        let type_info = if type_info_off != 0 && type_info_off + 16 <= data.len() {
414            parse_type_info(data, type_info_off, 0).ok()
415        } else {
416            None
417        };
418
419        constants.push(ConstantInfo {
420            name,
421            register_set,
422            register_index,
423            register_count,
424            type_info,
425        });
426    }
427
428    Ok(ConstantTable {
429        creator,
430        version,
431        flags,
432        target,
433        constants,
434    })
435}