1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt::Write as _;
3use std::path::Path;
4
5use crate::cfx::ShaderBlob;
6use crate::disasm::ShaderKind;
7
8const FX_MAGIC_LE: u32 = 0xfeff_0901;
9const FX_MAGIC_BYTESWAPPED: u32 = 0x0109_fffe;
10const D3DXPC_SCALAR: u32 = 0;
11const D3DXPC_VECTOR: u32 = 1;
12const D3DXPC_MATRIX_ROWS: u32 = 2;
13const D3DXPC_MATRIX_COLUMNS: u32 = 3;
14const D3DXPC_OBJECT: u32 = 4;
15const D3DXPC_STRUCT: u32 = 5;
16
17const D3DXPT_STRING: u32 = 4;
18const D3DXPT_TEXTURE: u32 = 5;
19const D3DXPT_TEXTURE1D: u32 = 6;
20const D3DXPT_TEXTURE2D: u32 = 7;
21const D3DXPT_TEXTURE3D: u32 = 8;
22const D3DXPT_TEXTURECUBE: u32 = 9;
23const D3DXPT_SAMPLER: u32 = 10;
24const D3DXPT_SAMPLER1D: u32 = 11;
25const D3DXPT_SAMPLER2D: u32 = 12;
26const D3DXPT_SAMPLER3D: u32 = 13;
27const D3DXPT_SAMPLERCUBE: u32 = 14;
28const D3DXPT_PIXELSHADER: u32 = 15;
29const D3DXPT_VERTEXSHADER: u32 = 16;
30
31#[derive(Debug, Clone)]
32pub struct EffectFile {
33 pub tag: u32,
34 pub table_base: usize,
35 pub start_offset: usize,
36 pub parameter_count: u32,
37 pub technique_count: u32,
38 pub object_count: u32,
39 pub string_count: u32,
40 pub resource_count: u32,
41 pub parameters: Vec<EffectParameter>,
42 pub techniques: Vec<Technique>,
43 pub objects: Vec<EffectObject>,
44 pub notes: Vec<String>,
45}
46
47#[derive(Debug, Clone)]
48pub struct EffectParameter {
49 pub index: usize,
50 pub name: String,
51 pub semantic: String,
52 pub value_type: u32,
53 pub class: u32,
54 pub rows: u32,
55 pub columns: u32,
56 pub elements: u32,
57 pub bytes: u32,
58 pub flags: u32,
59 pub object_id: Option<u32>,
60}
61
62#[derive(Debug, Clone)]
63pub struct Technique {
64 pub index: usize,
65 pub name: String,
66 pub annotation_count: u32,
67 pub passes: Vec<Pass>,
68}
69
70#[derive(Debug, Clone)]
71pub struct Pass {
72 pub index: usize,
73 pub name: String,
74 pub annotation_count: u32,
75 pub states: Vec<State>,
76 pub vertex_shader: Option<ShaderRef>,
77 pub pixel_shader: Option<ShaderRef>,
78}
79
80#[derive(Debug, Clone)]
81pub struct State {
82 pub index: usize,
83 pub operation: u32,
84 pub operation_name: String,
85 pub class_name: String,
86 pub state_index: u32,
87 pub typedef_offset: u32,
88 pub value_offset: u32,
89 pub parameter: EffectParameter,
90 pub value: StateValue,
91 pub resource_usage: Option<u32>,
92}
93
94#[derive(Debug, Clone)]
95pub enum StateValue {
96 Empty,
97 ObjectId(u32),
98 Int(Vec<i32>),
99 Float(Vec<f32>),
100 Bool(Vec<bool>),
101 StringObject { object_id: u32, text: Option<String> },
102 Raw { offset: u32, bytes: usize },
103}
104
105#[derive(Debug, Clone)]
106pub struct ShaderRef {
107 pub kind: ShaderKind,
108 pub object_id: Option<u32>,
109 pub object_data_offset: Option<usize>,
110 pub object_size: usize,
111 pub shader_index: Option<usize>,
112 pub shader_offset: Option<usize>,
113 pub unresolved_reason: Option<String>,
114}
115
116#[derive(Debug, Clone, Default)]
117pub struct EffectObject {
118 pub id: usize,
119 pub data_offset: Option<usize>,
120 pub size: usize,
121 pub owner_name: Option<String>,
122 pub owner_type: Option<u32>,
123}
124
125struct Reader<'a> {
126 data: &'a [u8],
127 pos: usize,
128}
129
130impl<'a> Reader<'a> {
131 fn new(data: &'a [u8], pos: usize) -> Self {
132 Self { data, pos }
133 }
134
135 fn read_u32(&mut self) -> Result<u32, String> {
136 let off = self.pos;
137 let b = self
138 .data
139 .get(off..off + 4)
140 .ok_or_else(|| format!("truncated dword at 0x{off:x}"))?;
141 self.pos += 4;
142 Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
143 }
144
145 fn skip_dwords(&mut self, count: u32) -> Result<(), String> {
146 let bytes = (count as usize)
147 .checked_mul(4)
148 .ok_or_else(|| "dword skip overflow".to_string())?;
149 let new_pos = self
150 .pos
151 .checked_add(bytes)
152 .ok_or_else(|| "reader overflow".to_string())?;
153 if new_pos > self.data.len() {
154 return Err(format!("skip beyond EOF: 0x{:x} > 0x{:x}", new_pos, self.data.len()));
155 }
156 self.pos = new_pos;
157 Ok(())
158 }
159}
160
161#[derive(Debug, Clone)]
162struct TypeDef {
163 name: String,
164 semantic: String,
165 value_type: u32,
166 class: u32,
167 rows: u32,
168 columns: u32,
169 elements: u32,
170 member_count: u32,
171 bytes: u32,
172}
173
174pub fn parse_effect(data: &[u8], shaders: &[ShaderBlob]) -> Result<EffectFile, String> {
175 if data.len() < 8 {
176 return Err("file is too short for a D3DX effect header".to_string());
177 }
178
179 let tag = read_u32_at(data, 0)?;
180 if tag != FX_MAGIC_LE && tag != FX_MAGIC_BYTESWAPPED {
181 return Err(format!("not a D3DX9 compiled effect tag: 0x{tag:08x}"));
182 }
183
184 let table_base = 8usize;
185 let start_rel = read_u32_at(data, 4)? as usize;
186 let start_offset = table_base
187 .checked_add(start_rel)
188 .ok_or_else(|| "effect start offset overflow".to_string())?;
189 if start_offset + 16 > data.len() {
190 return Err(format!("effect start offset outside file: 0x{start_offset:x}"));
191 }
192
193 let mut notes = Vec::new();
194 let mut objects = Vec::new();
195 let mut r = Reader::new(data, start_offset);
196 let parameter_count = r.read_u32()?;
197 let technique_count = r.read_u32()?;
198 let unknown = r.read_u32()?;
199 let object_count = r.read_u32()?;
200 if unknown != 0 {
201 notes.push(format!("effect header unknown dword is 0x{unknown:08x}"));
202 }
203 for id in 0..object_count as usize {
204 objects.push(EffectObject { id, ..EffectObject::default() });
205 }
206
207 let mut parameters = Vec::new();
208 for index in 0..parameter_count as usize {
209 let parameter = parse_top_level_parameter(data, table_base, &mut r, index, &mut objects)?;
210 parameters.push(parameter);
211 }
212
213 let mut techniques = Vec::new();
214 for index in 0..technique_count as usize {
215 let technique = parse_technique(data, table_base, &mut r, index, &mut objects)?;
216 techniques.push(technique);
217 }
218
219 let string_count = if r.pos + 4 <= data.len() { r.read_u32()? } else { 0 };
224 let resource_count = if r.pos + 4 <= data.len() { r.read_u32()? } else { 0 };
225
226 for _ in 0..string_count {
227 let object_id = r.read_u32()? as usize;
228 copy_object_data(data, &mut r, object_id, &mut objects, &mut notes)?;
229 }
230
231 for resource_index in 0..resource_count {
232 match parse_resource_like(data, &mut r, &mut techniques, &mut parameters, &mut objects, &mut notes) {
233 Ok(()) => {}
234 Err(e) => {
235 notes.push(format!("resource {resource_index} parse stopped: {e}"));
236 break;
237 }
238 }
239 }
240
241 resolve_shader_refs(&mut techniques, &objects, shaders);
242
243 Ok(EffectFile {
244 tag,
245 table_base,
246 start_offset,
247 parameter_count,
248 technique_count,
249 object_count,
250 string_count,
251 resource_count,
252 parameters,
253 techniques,
254 objects,
255 notes,
256 })
257}
258
259fn parse_top_level_parameter(
260 data: &[u8],
261 base: usize,
262 r: &mut Reader<'_>,
263 index: usize,
264 objects: &mut [EffectObject],
265) -> Result<EffectParameter, String> {
266 let typedef_offset = r.read_u32()?;
267 let value_offset = r.read_u32()?;
268 let flags = r.read_u32()?;
269 let annotation_count = r.read_u32()?;
270 let td = parse_typedef_at(data, base, typedef_offset)?;
271 let value = parse_value_at(data, base, value_offset, &td, objects)?;
272 let mut param = parameter_from_typedef(index, td, flags, value.object_id());
273 if let Some(id) = param.object_id {
274 if let Some(obj) = objects.get_mut(id as usize) {
275 obj.owner_name = Some(param.name.clone());
276 obj.owner_type = Some(param.value_type);
277 }
278 }
279 r.skip_dwords(annotation_count.saturating_mul(2))?;
280 Ok(param)
281}
282
283fn parse_technique(
284 data: &[u8],
285 base: usize,
286 r: &mut Reader<'_>,
287 index: usize,
288 objects: &mut [EffectObject],
289) -> Result<Technique, String> {
290 let name_offset = r.read_u32()?;
291 let name = read_name_at(data, base, name_offset).unwrap_or_else(|_| format!("technique_{index}"));
292 let annotation_count = r.read_u32()?;
293 let pass_count = r.read_u32()?;
294 r.skip_dwords(annotation_count.saturating_mul(2))?;
295
296 let mut passes = Vec::new();
297 for pass_index in 0..pass_count as usize {
298 passes.push(parse_pass(data, base, r, pass_index, objects)?);
299 }
300
301 Ok(Technique { index, name, annotation_count, passes })
302}
303
304fn parse_pass(
305 data: &[u8],
306 base: usize,
307 r: &mut Reader<'_>,
308 index: usize,
309 objects: &mut [EffectObject],
310) -> Result<Pass, String> {
311 let name_offset = r.read_u32()?;
312 let name = read_name_at(data, base, name_offset).unwrap_or_else(|_| format!("pass{index}"));
313 let annotation_count = r.read_u32()?;
314 let state_count = r.read_u32()?;
315 r.skip_dwords(annotation_count.saturating_mul(2))?;
316
317 let mut states = Vec::new();
318 let mut vertex_shader = None;
319 let mut pixel_shader = None;
320 for state_index in 0..state_count as usize {
321 let state = parse_state(data, base, r, state_index, objects)?;
322 if is_vertex_shader_state(&state) {
323 vertex_shader = Some(ShaderRef {
324 kind: ShaderKind::Vertex,
325 object_id: state.value.object_id(),
326 object_data_offset: None,
327 object_size: 0,
328 shader_index: None,
329 shader_offset: None,
330 unresolved_reason: None,
331 });
332 } else if is_pixel_shader_state(&state) {
333 pixel_shader = Some(ShaderRef {
334 kind: ShaderKind::Pixel,
335 object_id: state.value.object_id(),
336 object_data_offset: None,
337 object_size: 0,
338 shader_index: None,
339 shader_offset: None,
340 unresolved_reason: None,
341 });
342 }
343 states.push(state);
344 }
345
346 Ok(Pass { index, name, annotation_count, states, vertex_shader, pixel_shader })
347}
348
349fn parse_state(
350 data: &[u8],
351 base: usize,
352 r: &mut Reader<'_>,
353 index: usize,
354 objects: &mut [EffectObject],
355) -> Result<State, String> {
356 let operation = r.read_u32()?;
357 let state_index = r.read_u32()?;
358 let typedef_offset = r.read_u32()?;
359 let td = parse_typedef_at(data, base, typedef_offset)?;
360 let value_offset = r.read_u32()?;
361 let parsed = parse_value_at(data, base, value_offset, &td, objects)?;
362 let parameter = parameter_from_typedef(index, td, 0, parsed.object_id());
363 let (operation_name, class_name) = operation_info(operation);
364 Ok(State {
365 index,
366 operation,
367 operation_name: operation_name.to_string(),
368 class_name: class_name.to_string(),
369 state_index,
370 typedef_offset,
371 value_offset,
372 parameter,
373 value: parsed.value,
374 resource_usage: None,
375 })
376}
377
378#[derive(Debug, Clone)]
379struct ParsedValue {
380 value: StateValue,
381}
382
383impl ParsedValue {
384 fn object_id(&self) -> Option<u32> {
385 self.value.object_id()
386 }
387}
388
389impl StateValue {
390 pub fn object_id(&self) -> Option<u32> {
391 match self {
392 StateValue::ObjectId(id) => Some(*id),
393 StateValue::StringObject { object_id, .. } => Some(*object_id),
394 _ => None,
395 }
396 }
397}
398
399fn parse_value_at(
400 data: &[u8],
401 base: usize,
402 value_offset: u32,
403 td: &TypeDef,
404 objects: &mut [EffectObject],
405) -> Result<ParsedValue, String> {
406 let off = checked_base_offset(base, value_offset)?;
407 if off > data.len() {
408 return Err(format!("value offset outside file: 0x{off:x}"));
409 }
410
411 let value = if td.class == D3DXPC_OBJECT || is_object_type(td.value_type) {
412 if off + 4 > data.len() {
413 StateValue::Empty
414 } else {
415 let object_id = read_u32_at(data, off)?;
416 if let Some(obj) = objects.get_mut(object_id as usize) {
417 if !td.name.is_empty() {
418 obj.owner_name = Some(td.name.clone());
419 }
420 obj.owner_type = Some(td.value_type);
421 }
422 if td.value_type == D3DXPT_STRING {
423 let text = objects.get(object_id as usize)
424 .and_then(|o| o.data_offset)
425 .and_then(|pos| read_nul_string(data, pos).ok());
426 StateValue::StringObject { object_id, text }
427 } else {
428 StateValue::ObjectId(object_id)
429 }
430 }
431 } else if td.value_type == 3 {
432 let count = scalar_count(td).min(256);
433 let mut values = Vec::new();
434 for i in 0..count {
435 let p = off + (i as usize) * 4;
436 if p + 4 <= data.len() {
437 values.push(f32::from_le_bytes([data[p], data[p + 1], data[p + 2], data[p + 3]]));
438 }
439 }
440 StateValue::Float(values)
441 } else if td.value_type == 2 {
442 let count = scalar_count(td).min(256);
443 let mut values = Vec::new();
444 for i in 0..count {
445 let p = off + (i as usize) * 4;
446 if p + 4 <= data.len() {
447 values.push(i32::from_le_bytes([data[p], data[p + 1], data[p + 2], data[p + 3]]));
448 }
449 }
450 StateValue::Int(values)
451 } else if td.value_type == 1 {
452 let count = scalar_count(td).min(256);
453 let mut values = Vec::new();
454 for i in 0..count {
455 let p = off + (i as usize) * 4;
456 if p + 4 <= data.len() {
457 values.push(read_u32_at(data, p)? != 0);
458 }
459 }
460 StateValue::Bool(values)
461 } else {
462 StateValue::Raw { offset: value_offset, bytes: td.bytes as usize }
463 };
464
465 Ok(ParsedValue { value })
466}
467
468fn scalar_count(td: &TypeDef) -> u32 {
469 let elems = if td.elements == 0 { 1 } else { td.elements };
470 (td.rows.max(1)).saturating_mul(td.columns.max(1)).saturating_mul(elems)
471}
472
473fn parse_typedef_at(data: &[u8], base: usize, offset: u32) -> Result<TypeDef, String> {
474 let pos = checked_base_offset(base, offset)?;
475 let mut r = Reader::new(data, pos);
476 let value_type = r.read_u32()?;
477 let class = r.read_u32()?;
478 let name_offset = r.read_u32()?;
479 let name = read_name_at(data, base, name_offset).unwrap_or_default();
480 let semantic_offset = r.read_u32()?;
481 let semantic = read_name_at(data, base, semantic_offset).unwrap_or_default();
482 let elements = r.read_u32()?;
483
484 let (rows, columns, member_count, object_size) = match class {
488 D3DXPC_VECTOR => {
489 let columns = r.read_u32()?;
490 let rows = r.read_u32()?;
491 (rows, columns, 0, rows.saturating_mul(columns).saturating_mul(4))
492 }
493 D3DXPC_SCALAR | D3DXPC_MATRIX_ROWS | D3DXPC_MATRIX_COLUMNS => {
494 let rows = r.read_u32()?;
495 let columns = r.read_u32()?;
496 (rows, columns, 0, rows.saturating_mul(columns).saturating_mul(4))
497 }
498 D3DXPC_STRUCT => {
499 let members = r.read_u32()?;
500 (0, 0, members, 0)
501 }
502 D3DXPC_OBJECT => (0, 0, 0, 4),
503 _ => (0, 0, 0, 0),
504 };
505
506 let elem_count = if elements == 0 { 1 } else { elements };
507 let bytes = if class == D3DXPC_STRUCT {
508 0
509 } else if class == D3DXPC_OBJECT {
510 if matches!(value_type, D3DXPT_SAMPLER | D3DXPT_SAMPLER1D | D3DXPT_SAMPLER2D | D3DXPT_SAMPLER3D | D3DXPT_SAMPLERCUBE) {
511 0
512 } else {
513 object_size.saturating_mul(elem_count)
514 }
515 } else {
516 object_size.saturating_mul(elem_count)
517 };
518
519 Ok(TypeDef { name, semantic, value_type, class, rows, columns, elements, member_count, bytes })
520}
521
522fn parameter_from_typedef(index: usize, td: TypeDef, flags: u32, object_id: Option<u32>) -> EffectParameter {
523 EffectParameter {
524 index,
525 name: td.name,
526 semantic: td.semantic,
527 value_type: td.value_type,
528 class: td.class,
529 rows: td.rows,
530 columns: td.columns,
531 elements: td.elements,
532 bytes: td.bytes,
533 flags,
534 object_id,
535 }
536}
537
538fn copy_object_data(
539 data: &[u8],
540 r: &mut Reader<'_>,
541 object_id: usize,
542 objects: &mut [EffectObject],
543 notes: &mut Vec<String>,
544) -> Result<(), String> {
545 let size = r.read_u32()? as usize;
546 let start = r.pos;
547 let end = start
548 .checked_add(size)
549 .ok_or_else(|| "object data size overflow".to_string())?;
550 if end > data.len() {
551 return Err(format!("object {object_id} data outside file: 0x{end:x}"));
552 }
553 if let Some(obj) = objects.get_mut(object_id) {
554 obj.data_offset = Some(start);
555 obj.size = size;
556 } else {
557 notes.push(format!("object data references out-of-range object id {object_id}"));
558 }
559 r.pos = align4(end);
560 if r.pos > data.len() {
561 return Err(format!("object {object_id} aligned data outside file"));
562 }
563 Ok(())
564}
565
566fn parse_resource_like(
567 data: &[u8],
568 r: &mut Reader<'_>,
569 techniques: &mut [Technique],
570 parameters: &mut [EffectParameter],
571 objects: &mut [EffectObject],
572 notes: &mut Vec<String>,
573) -> Result<(), String> {
574 let rec_start = r.pos;
578 let technique_index = r.read_u32()?;
579 let index = r.read_u32()?;
580 let element_index = r.read_u32()?;
581 let state_index = r.read_u32()?;
582 let usage = r.read_u32()?;
583
584 let object_id = if technique_index == 0xffff_ffff {
585 let param = parameters
586 .get(index as usize)
587 .ok_or_else(|| format!("resource parameter index {index} outside parameter table"))?;
588 if element_index != 0xffff_ffff {
589 notes.push(format!(
590 "resource at 0x{rec_start:x} references parameter element {element_index}; using parent object id"
591 ));
592 }
593 param.object_id.ok_or_else(|| {
594 format!("resource at 0x{rec_start:x} top-level parameter {index} has no object id")
595 })?
596 } else {
597 let tech = techniques
598 .get_mut(technique_index as usize)
599 .ok_or_else(|| format!("resource technique index {technique_index} outside technique table"))?;
600 let pass = tech
601 .passes
602 .get_mut(index as usize)
603 .ok_or_else(|| format!("resource pass index {index} outside technique {technique_index}"))?;
604 let state = pass
605 .states
606 .get_mut(state_index as usize)
607 .ok_or_else(|| format!("resource state index {state_index} outside technique {technique_index} pass {index}"))?;
608 state.resource_usage = Some(usage);
609 state.parameter.object_id.ok_or_else(|| {
610 format!(
611 "resource at 0x{rec_start:x} technique {technique_index} pass {index} state {state_index} has no object id"
612 )
613 })?
614 };
615
616 copy_object_data(data, r, object_id as usize, objects, notes)?;
617 Ok(())
618}
619
620fn resolve_shader_refs(techniques: &mut [Technique], objects: &[EffectObject], shaders: &[ShaderBlob]) {
621 for tech in techniques {
622 for pass in &mut tech.passes {
623 if let Some(sr) = &mut pass.vertex_shader {
624 resolve_one_shader_ref(sr, objects, shaders);
625 }
626 if let Some(sr) = &mut pass.pixel_shader {
627 resolve_one_shader_ref(sr, objects, shaders);
628 }
629 }
630 }
631}
632
633fn resolve_one_shader_ref(sr: &mut ShaderRef, objects: &[EffectObject], shaders: &[ShaderBlob]) {
634 let Some(id) = sr.object_id else {
635 sr.unresolved_reason = Some("state did not contain an object id".to_string());
636 return;
637 };
638 let Some(obj) = objects.get(id as usize) else {
639 sr.unresolved_reason = Some(format!("object id {id} is outside object table"));
640 return;
641 };
642 sr.object_data_offset = obj.data_offset;
643 sr.object_size = obj.size;
644 let Some(data_off) = obj.data_offset else {
645 sr.unresolved_reason = Some(format!("object id {id} has no copied data payload"));
646 return;
647 };
648
649 let by_offset = shaders
650 .iter()
651 .find(|s| s.kind == sr.kind && s.offset == data_off);
652 let by_range = shaders.iter().find(|s| {
653 s.kind == sr.kind
654 && data_off >= s.offset
655 && data_off < s.end_offset
656 && s.offset >= data_off.saturating_sub(16)
657 });
658 let by_size = shaders.iter().find(|s| {
659 s.kind == sr.kind
660 && obj.size == s.bytes.len()
661 && obj.data_offset == Some(s.offset)
662 });
663 let matched = by_offset.or(by_size).or(by_range);
664 if let Some(shader) = matched {
665 sr.shader_index = Some(shader.index);
666 sr.shader_offset = Some(shader.offset);
667 sr.unresolved_reason = None;
668 } else {
669 sr.unresolved_reason = Some(format!(
670 "object id {id} payload offset=0x{data_off:x} size={} did not match any scanned {:?} shader",
671 obj.size, sr.kind
672 ));
673 }
674}
675
676fn is_object_type(t: u32) -> bool {
677 matches!(
678 t,
679 D3DXPT_STRING
680 | D3DXPT_TEXTURE
681 | D3DXPT_TEXTURE1D
682 | D3DXPT_TEXTURE2D
683 | D3DXPT_TEXTURE3D
684 | D3DXPT_TEXTURECUBE
685 | D3DXPT_SAMPLER
686 | D3DXPT_SAMPLER1D
687 | D3DXPT_SAMPLER2D
688 | D3DXPT_SAMPLER3D
689 | D3DXPT_SAMPLERCUBE
690 | D3DXPT_PIXELSHADER
691 | D3DXPT_VERTEXSHADER
692 )
693}
694
695fn is_vertex_shader_state(state: &State) -> bool {
696 state.operation == 146 || state.parameter.value_type == D3DXPT_VERTEXSHADER
697}
698
699fn is_pixel_shader_state(state: &State) -> bool {
700 state.operation == 147 || state.parameter.value_type == D3DXPT_PIXELSHADER
701}
702
703pub fn operation_info(op: u32) -> (&'static str, &'static str) {
704 match op {
705 0 => ("ZEnable", "RenderState"),
706 1 => ("FillMode", "RenderState"),
707 2 => ("ShadeMode", "RenderState"),
708 3 => ("ZWriteEnable", "RenderState"),
709 4 => ("AlphaTestEnable", "RenderState"),
710 5 => ("LastPixel", "RenderState"),
711 6 => ("SrcBlend", "RenderState"),
712 7 => ("DestBlend", "RenderState"),
713 8 => ("CullMode", "RenderState"),
714 9 => ("ZFunc", "RenderState"),
715 10 => ("AlphaRef", "RenderState"),
716 11 => ("AlphaFunc", "RenderState"),
717 12 => ("DitherEnable", "RenderState"),
718 13 => ("AlphaBlendEnable", "RenderState"),
719 14 => ("FogEnable", "RenderState"),
720 15 => ("SpecularEnable", "RenderState"),
721 16 => ("FogColor", "RenderState"),
722 17 => ("FogTableMode", "RenderState"),
723 18 => ("FogStart", "RenderState"),
724 19 => ("FogEnd", "RenderState"),
725 20 => ("FogDensity", "RenderState"),
726 21 => ("RangeFogEnable", "RenderState"),
727 22 => ("StencilEnable", "RenderState"),
728 23 => ("StencilFail", "RenderState"),
729 24 => ("StencilZFail", "RenderState"),
730 25 => ("StencilPass", "RenderState"),
731 26 => ("StencilFunc", "RenderState"),
732 27 => ("StencilRef", "RenderState"),
733 28 => ("StencilMask", "RenderState"),
734 29 => ("StencilWriteMask", "RenderState"),
735 30 => ("TextureFactor", "RenderState"),
736 31 => ("Wrap0", "RenderState"),
737 32 => ("Wrap1", "RenderState"),
738 33 => ("Wrap2", "RenderState"),
739 34 => ("Wrap3", "RenderState"),
740 35 => ("Wrap4", "RenderState"),
741 36 => ("Wrap5", "RenderState"),
742 37 => ("Wrap6", "RenderState"),
743 38 => ("Wrap7", "RenderState"),
744 39 => ("Clipping", "RenderState"),
745 40 => ("Lighting", "RenderState"),
746 41 => ("Ambient", "RenderState"),
747 42 => ("FogVertexMode", "RenderState"),
748 43 => ("ColorVertex", "RenderState"),
749 44 => ("LocalViewer", "RenderState"),
750 45 => ("NormalizeNormals", "RenderState"),
751 46 => ("DiffuseMaterialSource", "RenderState"),
752 47 => ("SpecularMaterialSource", "RenderState"),
753 48 => ("AmbientMaterialSource", "RenderState"),
754 49 => ("EmissiveMaterialSource", "RenderState"),
755 50 => ("VertexBlend", "RenderState"),
756 51 => ("ClipPlaneEnable", "RenderState"),
757 52 => ("PointSize", "RenderState"),
758 53 => ("PointSizeMin", "RenderState"),
759 54 => ("PointSpriteEnable", "RenderState"),
760 55 => ("PointScaleEnable", "RenderState"),
761 56 => ("PointScaleA", "RenderState"),
762 57 => ("PointScaleB", "RenderState"),
763 58 => ("PointScaleC", "RenderState"),
764 59 => ("MultiSampleAntiAlias", "RenderState"),
765 60 => ("MultiSampleMask", "RenderState"),
766 61 => ("PatchEdgeStyle", "RenderState"),
767 62 => ("DebugMonitorToken", "RenderState"),
768 63 => ("PointSizeMax", "RenderState"),
769 64 => ("IndexedVertexBlendEnable", "RenderState"),
770 65 => ("ColorWriteEnable", "RenderState"),
771 66 => ("TweenFactor", "RenderState"),
772 67 => ("BlendOp", "RenderState"),
773 68 => ("PositionDegree", "RenderState"),
774 69 => ("NormalDegree", "RenderState"),
775 70 => ("ScissorTestEnable", "RenderState"),
776 71 => ("SlopeScaleDepthBias", "RenderState"),
777 72 => ("AntiAliasedLineEnable", "RenderState"),
778 73 => ("MinTessellationLevel", "RenderState"),
779 74 => ("MaxTessellationLevel", "RenderState"),
780 75 => ("AdaptiveTessX", "RenderState"),
781 76 => ("AdaptiveTessY", "RenderState"),
782 77 => ("AdaptiveTessZ", "RenderState"),
783 78 => ("AdaptiveTessW", "RenderState"),
784 79 => ("EnableAdaptiveTessellation", "RenderState"),
785 80 => ("TwoSidedStencilMode", "RenderState"),
786 81 => ("CCWStencilFail", "RenderState"),
787 82 => ("CCWStencilZFail", "RenderState"),
788 83 => ("CCWStencilPass", "RenderState"),
789 84 => ("CCWStencilFunc", "RenderState"),
790 85 => ("ColorWriteEnable1", "RenderState"),
791 86 => ("ColorWriteEnable2", "RenderState"),
792 87 => ("ColorWriteEnable3", "RenderState"),
793 88 => ("BlendFactor", "RenderState"),
794 89 => ("SRGBWriteEnable", "RenderState"),
795 90 => ("DepthBias", "RenderState"),
796 91 => ("Wrap8", "RenderState"),
797 92 => ("Wrap9", "RenderState"),
798 93 => ("Wrap10", "RenderState"),
799 94 => ("Wrap11", "RenderState"),
800 95 => ("Wrap12", "RenderState"),
801 96 => ("Wrap13", "RenderState"),
802 97 => ("Wrap14", "RenderState"),
803 98 => ("Wrap15", "RenderState"),
804 99 => ("SeparateAlphaBlendEnable", "RenderState"),
805 100 => ("SrcBlendAlpha", "RenderState"),
806 101 => ("DestBlendAlpha", "RenderState"),
807 102 => ("BlendOpAlpha", "RenderState"),
808 146 => ("VertexShader", "VertexShader"),
809 147 => ("PixelShader", "PixelShader"),
810 _ => ("UnknownState", "Unknown"),
811 }
812}
813
814fn read_u32_at(data: &[u8], off: usize) -> Result<u32, String> {
815 let b = data
816 .get(off..off + 4)
817 .ok_or_else(|| format!("truncated dword at 0x{off:x}"))?;
818 Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
819}
820
821fn checked_base_offset(base: usize, offset: u32) -> Result<usize, String> {
822 base.checked_add(offset as usize)
823 .ok_or_else(|| format!("offset overflow: base=0x{base:x} offset=0x{offset:x}"))
824}
825
826fn read_name_at(data: &[u8], base: usize, offset: u32) -> Result<String, String> {
827 if offset == 0 {
828 return Ok(String::new());
829 }
830 let pos = checked_base_offset(base, offset)?;
831 let len = read_u32_at(data, pos)? as usize;
832 let start = pos + 4;
833 let end = start
834 .checked_add(len)
835 .ok_or_else(|| "name length overflow".to_string())?;
836 let bytes = data
837 .get(start..end)
838 .ok_or_else(|| format!("name outside file at 0x{pos:x} len={len}"))?;
839 let nul = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
840 Ok(String::from_utf8_lossy(&bytes[..nul]).to_string())
841}
842
843fn read_nul_string(data: &[u8], pos: usize) -> Result<String, String> {
844 let bytes = data.get(pos..).ok_or_else(|| format!("string offset outside file: 0x{pos:x}"))?;
845 let nul = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
846 Ok(String::from_utf8_lossy(&bytes[..nul]).to_string())
847}
848
849fn align4(v: usize) -> usize {
850 (v + 3) & !3
851}
852
853pub fn safe_name(name: &str, fallback: &str) -> String {
854 let mut out = String::new();
855 for ch in name.chars() {
856 if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '.' {
857 out.push(ch);
858 } else if ch.is_whitespace() {
859 out.push('_');
860 }
861 }
862 if out.is_empty() {
863 fallback.to_string()
864 } else {
865 out
866 }
867}
868
869pub fn format_effect_map(effect: &EffectFile, shaders: &[ShaderBlob]) -> String {
870 let mut s = String::new();
871 let _ = writeln!(s, "tag: 0x{:08x}", effect.tag);
872 let _ = writeln!(s, "table_base: 0x{:08x}", effect.table_base);
873 let _ = writeln!(s, "start_offset: 0x{:08x}", effect.start_offset);
874 let _ = writeln!(s, "parameters: {}", effect.parameter_count);
875 let _ = writeln!(s, "techniques: {}", effect.technique_count);
876 let _ = writeln!(s, "objects: {}", effect.object_count);
877 let _ = writeln!(s, "strings: {}", effect.string_count);
878 let _ = writeln!(s, "resources: {}", effect.resource_count);
879 if !effect.notes.is_empty() {
880 let _ = writeln!(s, "notes:");
881 for note in &effect.notes {
882 let _ = writeln!(s, " - {note}");
883 }
884 }
885 let _ = writeln!(s);
886 let _ = writeln!(s, "top_level_parameters:");
887 for p in &effect.parameters {
888 let _ = writeln!(
889 s,
890 " [{}] {} type={} class={} rows={} cols={} elems={} bytes={} object_id={:?} semantic={}",
891 p.index, p.name, p.value_type, p.class, p.rows, p.columns, p.elements, p.bytes, p.object_id, p.semantic
892 );
893 }
894 let _ = writeln!(s);
895 let _ = writeln!(s, "objects:");
896 for o in &effect.objects {
897 let _ = writeln!(
898 s,
899 " [{}] owner={:?} type={:?} offset={} size={}",
900 o.id,
901 o.owner_name,
902 o.owner_type,
903 o.data_offset.map(|v| format!("0x{v:08x}")).unwrap_or_else(|| "none".to_string()),
904 o.size
905 );
906 }
907 let _ = writeln!(s);
908 let _ = writeln!(s, "shader_blobs:");
909 for sh in shaders {
910 let _ = writeln!(s, " [{}] {} offset=0x{:08x} size={} ctab={}", sh.index, sh.profile(), sh.offset, sh.bytes.len(), sh.ctab.is_some());
911 }
912 let _ = writeln!(s);
913 let _ = writeln!(s, "techniques:");
914 for tech in &effect.techniques {
915 let _ = writeln!(s, "technique [{}] {} passes={}", tech.index, tech.name, tech.passes.len());
916 for pass in &tech.passes {
917 let _ = writeln!(s, " pass [{}] {} states={}", pass.index, pass.name, pass.states.len());
918 if let Some(vs) = &pass.vertex_shader {
919 write_shader_ref(&mut s, " VS", vs);
920 }
921 if let Some(ps) = &pass.pixel_shader {
922 write_shader_ref(&mut s, " PS", ps);
923 }
924 for state in &pass.states {
925 let _ = writeln!(
926 s,
927 " state [{}] op={} {} class={} index={} type={} param={} usage={:?} value={}",
928 state.index,
929 state.operation,
930 state.operation_name,
931 state.class_name,
932 state.state_index,
933 state.parameter.value_type,
934 state.parameter.name,
935 state.resource_usage,
936 format_state_value(&state.value)
937 );
938 }
939 }
940 }
941 s
942}
943
944fn write_shader_ref(s: &mut String, label: &str, sr: &ShaderRef) {
945 let _ = writeln!(
946 s,
947 "{} object_id={:?} object_offset={} object_size={} shader_index={:?} shader_offset={} unresolved={:?}",
948 label,
949 sr.object_id,
950 sr.object_data_offset.map(|v| format!("0x{v:08x}")).unwrap_or_else(|| "none".to_string()),
951 sr.object_size,
952 sr.shader_index,
953 sr.shader_offset.map(|v| format!("0x{v:08x}")).unwrap_or_else(|| "none".to_string()),
954 sr.unresolved_reason
955 );
956}
957
958fn format_state_value(v: &StateValue) -> String {
959 match v {
960 StateValue::Empty => "empty".to_string(),
961 StateValue::ObjectId(id) => format!("object_id({id})"),
962 StateValue::Int(xs) => format!("int{:?}", xs),
963 StateValue::Float(xs) => format!("float{:?}", xs),
964 StateValue::Bool(xs) => format!("bool{:?}", xs),
965 StateValue::StringObject { object_id, text } => format!("string_object({object_id}, {:?})", text),
966 StateValue::Raw { offset, bytes } => format!("raw(offset=0x{offset:x}, bytes={bytes})"),
967 }
968}
969
970pub fn used_shader_indices(effect: &EffectFile) -> BTreeSet<usize> {
971 let mut out = BTreeSet::new();
972 for tech in &effect.techniques {
973 for pass in &tech.passes {
974 if let Some(vs) = &pass.vertex_shader {
975 if let Some(i) = vs.shader_index {
976 out.insert(i);
977 }
978 }
979 if let Some(ps) = &pass.pixel_shader {
980 if let Some(i) = ps.shader_index {
981 out.insert(i);
982 }
983 }
984 }
985 }
986 out
987}
988
989pub fn technique_shader_outputs<'a>(
990 effect: &'a EffectFile,
991 shaders: &'a [ShaderBlob],
992) -> Vec<(String, &'a ShaderBlob)> {
993 let by_index: BTreeMap<usize, &ShaderBlob> = shaders.iter().map(|s| (s.index, s)).collect();
994 let mut out = Vec::new();
995 for tech in &effect.techniques {
996 let tech_name = safe_name(&tech.name, &format!("technique_{}", tech.index));
997 for pass in &tech.passes {
998 let pass_name = safe_name(&pass.name, &format!("pass{}", pass.index));
999 let prefix_base = format!("t{:04}_{}__p{:02}_{}", tech.index, tech_name, pass.index, pass_name);
1000 if let Some(vs) = &pass.vertex_shader {
1001 if let Some(idx) = vs.shader_index {
1002 if let Some(blob) = by_index.get(&idx) {
1003 out.push((format!("{}__vs", prefix_base), *blob));
1004 }
1005 }
1006 }
1007 if let Some(ps) = &pass.pixel_shader {
1008 if let Some(idx) = ps.shader_index {
1009 if let Some(blob) = by_index.get(&idx) {
1010 out.push((format!("{}__ps", prefix_base), *blob));
1011 }
1012 }
1013 }
1014 }
1015 }
1016 out
1017}
1018
1019pub fn write_outputs_for_blob(
1020 out_dir: &Path,
1021 prefix: &str,
1022 blob: &ShaderBlob,
1023 hlsl: &str,
1024 asm: &str,
1025 ctab_text: Option<&str>,
1026) -> std::io::Result<()> {
1027 std::fs::write(out_dir.join("bytecode").join(format!("{prefix}.bin")), &blob.bytes)?;
1028 std::fs::write(out_dir.join("hlsl").join(format!("{prefix}.hlsl")), hlsl)?;
1029 std::fs::write(out_dir.join("asm").join(format!("{prefix}.asm")), asm)?;
1030 if let Some(ctab) = ctab_text {
1031 std::fs::write(out_dir.join("hlsl").join(format!("{prefix}.ctab.txt")), ctab)?;
1032 }
1033 Ok(())
1034}