siglus_cfx_decompiler/
cfx.rs1use crate::ctab::{ConstantTable, parse_ctab};
2use crate::disasm::{ShaderKind, disassemble};
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, major) {
43 (0xffff_0000, 2) => Some((ShaderKind::Pixel, major, minor)),
44 (0xfffe_0000, 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 {
81 break;
82 };
83 let opcode = tok & 0xffff;
84
85 if opcode == 0xfffe {
86 let len = ((tok >> 16) & 0x7fff) as usize;
87 p = p.saturating_add(4).saturating_add(len.saturating_mul(4));
88 continue;
89 }
90
91 if opcode == 0xffff {
92 return (p + 4).min(hard_end);
93 }
94
95 let len = ((tok >> 24) & 0x0f) as usize;
96 if len == 0 {
97 p += 4;
98 } else {
99 p = p.saturating_add(4).saturating_add(len.saturating_mul(4));
100 }
101 }
102 hard_end
103}
104
105fn parse_leading_ctab(data: &[u8], off: usize, end: usize) -> Option<ConstantTable> {
106 let comment_token = read_u32_le(data, off + 4)?;
107 if (comment_token & 0xffff) != 0xfffe {
108 return None;
109 }
110 let dwords = ((comment_token >> 16) & 0x7fff) as usize;
111 let payload_start = off + 8;
112 let payload_end = payload_start.checked_add(dwords.checked_mul(4)?)?;
113 if payload_end > end || payload_end > data.len() {
114 return None;
115 }
116 parse_ctab(&data[payload_start..payload_end]).ok()
117}
118
119pub fn scan_shaders(data: &[u8]) -> Vec<ShaderBlob> {
120 let mut starts: Vec<(usize, ShaderKind, u8, u8)> = Vec::new();
121
122 let mut i = 0usize;
123 while i + 12 <= data.len() {
124 if let Some((kind, major, minor)) = is_real_shader_start(data, i) {
125 starts.push((i, kind, major, minor));
126 i += 4;
127 } else {
128 i += 1;
129 }
130 }
131
132 let mut out = Vec::with_capacity(starts.len());
133 for idx in 0..starts.len() {
134 let (off, kind, major, minor) = starts[idx];
135 let hard_end = if idx + 1 < starts.len() {
136 starts[idx + 1].0
137 } else {
138 data.len()
139 };
140 let end = shader_end_by_end_token(data, off, hard_end);
141 let ctab = parse_leading_ctab(data, off, end);
142
143 out.push(ShaderBlob {
144 index: idx,
145 kind,
146 major,
147 minor,
148 offset: off,
149 end_offset: end,
150 bytes: data[off..end].to_vec(),
151 ctab,
152 });
153 }
154
155 out
156}
157
158pub fn disassemble_blob(blob: &ShaderBlob) -> String {
159 disassemble(&blob.bytes)
160}