siglus_cfx_decompiler/
cfx.rs1use 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}