gfxd_rs/
disassembler.rs

1/* SPDX-FileCopyrightText: © 2025 Decompollaborate */
2/* SPDX-License-Identifier: MIT OR Apache-2.0 */
3
4use alloc::{borrow::ToOwned as _, string::String};
5use gfxd_sys::{ffi, ptr::NonNullConst};
6
7use crate::{
8    lib_data::{LibData, LibDataWrap},
9    Customizer, Microcode,
10};
11
12// TODO: figure out where and how to expose gfxd_arg_callbacks
13
14/// `Gfx` packet macro disassembler.
15///
16/// This struct allows configuring general settings that control the generated
17/// output. Once the disassembler is configured, call [`disassemble`] to
18/// produce a string with the disassembly of the passed Gfx packets.
19///
20/// Refer to the [`Customizer`] struct to further customize how each `Gfx`
21/// macro is outputted or register callbacks for the different kinds of macros.
22///
23/// ## Examples
24///
25/// Disassemble F3DEX packets without any kind of customization
26///
27/// ```rust
28/// use gfxd_rs::{Customizer, Disassembler, Microcode};
29///
30/// pub fn plain_disasm_f3dex(data: &[u8]) -> String {
31///     let mut customizer = Customizer::new();
32///
33///     Disassembler::new()
34///         .disassemble(data, Microcode::F3dex, &mut customizer)
35/// }
36/// ```
37///
38/// Use a dynamic argument and print each macro on a different line.
39///
40/// ```rust
41/// use gfxd_rs::{Customizer, Disassembler, MacroPrinter, Microcode};
42///
43/// pub fn disasm_pretty(data: &[u8], microcode: Microcode, dynamic: &str) -> String {
44///     let mut customizer = Customizer::new();
45///
46///     // This has to be binded to a local variable to avoid dropping it too soon.
47///     let mut macro_fn = |printer: &mut MacroPrinter, _info: &mut _| {
48///         // Write 4 spaces.
49///         printer.write_str("    ");
50///         // Call the original macro handler, to emit the macro as-is.
51///         let ret = printer.macro_dflt();
52///         // Write a newline after the macro.
53///         printer.write_str(",\n");
54///         ret
55///     };
56///     customizer
57///         .macro_fn(&mut macro_fn);
58///
59///     Disassembler::new()
60///         .dynamic(Some(dynamic))
61///         .disassemble(data, microcode, &mut customizer)
62/// }
63/// ```
64///
65/// [`disassemble`]: Disassembler::disassemble
66#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
67pub struct Disassembler<'d> {
68    // TODO: endian and wordsize
69    dynamic: Option<&'d str>,
70    stop_on_invalid: bool,
71    stop_on_end: bool,
72    emit_dec_color: bool,
73    emit_q_macro: bool,
74    emit_ext_macro: bool,
75}
76
77impl<'d> Disassembler<'d> {
78    /// Construct a `Disassembler` instance.
79    ///
80    /// Consult the `Disassembler` struct documentation for more information.
81    #[must_use]
82    pub const fn new() -> Self {
83        Self {
84            dynamic: None,
85            stop_on_invalid: true,
86            stop_on_end: true,
87            emit_dec_color: false,
88            emit_q_macro: false,
89            emit_ext_macro: false,
90        }
91    }
92
93    /// Enable or disable the use of dynamic `g` macros instead of static `gs`
94    /// macros, and select the dynamic display list pointer argument to be used.
95    ///
96    /// If `Some`, this value will be used by [`macro_dflt`] as the first
97    /// argument to dynamic macros.
98    /// If `None`, dynamic macros are disabled and `gs` macros are used.
99    /// Defaults to `None`.
100    ///
101    /// Also affects the result of [`macro_name`], as it will return either the
102    /// dynamic or static version of the macro name as selected by this
103    /// setting.
104    ///
105    /// [`macro_dflt`]: crate::MacroPrinter::macro_dflt
106    /// [`macro_name`]: crate::MacroInfo::macro_name
107    pub fn dynamic(&mut self, dynamic: Option<&'d str>) -> &mut Self {
108        self.dynamic = dynamic;
109        self
110    }
111
112    /// Stop execution when encountering an invalid macro.
113    ///
114    /// Enabled by default.
115    pub fn stop_on_invalid(&mut self, value: bool) -> &mut Self {
116        self.stop_on_invalid = value;
117        self
118    }
119    /// Stop execution when encountering a [`SPBranchList`] or
120    /// [`SPEndDisplayList`].
121    ///
122    /// Enabled by default.
123    ///
124    /// [`SPBranchList`]: crate::MacroId::SPBranchList
125    /// [`SPEndDisplayList`]: crate::MacroId::SPEndDisplayList
126    pub fn stop_on_end(&mut self, value: bool) -> &mut Self {
127        self.stop_on_end = value;
128        self
129    }
130    /// Print color components as decimal instead of hexadecimal.
131    ///
132    /// Disabled by default.
133    pub fn emit_dec_color(&mut self, value: bool) -> &mut Self {
134        self.emit_dec_color = value;
135        self
136    }
137    /// Print fixed-point conversion `q` macros for fixed-point values.
138    ///
139    /// Disabled by default.
140    pub fn emit_q_macro(&mut self, value: bool) -> &mut Self {
141        self.emit_q_macro = value;
142        self
143    }
144    /// Emit non-standard macros.
145    ///
146    /// Some commands are valid (though possibly meaningless), but have no
147    /// macros associated with them, such as a standalone `G_RDPHALF_1`.
148    /// When this feature is enabled, such a command will produce a
149    /// non-standard [`gsDPHalf1`] macro instead of a raw hexadecimal command.
150    ///
151    /// Also enables some non-standard multi-packet texture loading macros.
152    ///
153    /// Disabled by default.
154    ///
155    /// [`gsDPHalf1`]: crate::MacroId::DPHalf1
156    pub fn emit_ext_macro(&mut self, value: bool) -> &mut Self {
157        self.emit_ext_macro = value;
158        self
159    }
160
161    /// Start executing `gfxd` with the current settings.
162    ///
163    /// The `data` argument is a big endian byte array containing the `Gfx`
164    /// packets to be disassembled.
165    ///
166    /// `microcode` corresponds to the target microcode to decode the data.
167    ///
168    /// `customizer` allows registering callbacks to customize the output or to
169    /// extract data from each macro type.
170    ///
171    /// For each macro, the macro handler registered with [`macro_fn`] is
172    /// called.
173    ///
174    /// Execution ends when:
175    /// - the input ends,
176    /// - the macro handler returns [`Stop`],
177    /// - when an invalid macro is encountered and [`stop_on_invalid`] is
178    ///   enabled,
179    /// - or when [`SPBranchList`] or [`SPEndDisplayList`] is encountered and
180    ///   [`stop_on_end`] is enabled.
181    ///
182    /// [`macro_fn`]: Customizer::macro_fn
183    /// [`Stop`]: crate::MacroFnRet::Stop
184    /// [`stop_on_invalid`]: Disassembler::stop_on_invalid
185    /// [`stop_on_end`]: Disassembler::stop_on_end
186    /// [`SPBranchList`]: crate::MacroId::SPBranchList
187    /// [`SPEndDisplayList`]: crate::MacroId::SPEndDisplayList
188    #[must_use]
189    pub fn disassemble(
190        self,
191        data: &[u8],
192        microcode: Microcode,
193        customizer: &mut Customizer,
194    ) -> String {
195        let mut lib_data = LibData::new(customizer);
196        // Make sure we don't get surprises by the data somehow moving.
197        // tbh I'm not sure if this is needed at all, I need to investigate.
198        // let lib_data = alloc::boxed::Box::pin(lib_data);
199
200        // We need to ensure this String doesn't get dropped before we execute
201        // gfxd. Allocating it outside disassemble_impl and passing the
202        // reference should ensure that, as far as I understand it.
203        let dynamic = self.dynamic.map(|x| {
204            let mut x = x.to_owned();
205            // nul-terminate the string
206            x.push('\0');
207            x
208        });
209        // `as_ref` is needed, otherwise the string gets consumed and this ends
210        // up pointing to itself or something weird like that.
211        let dynamic_ptr = dynamic
212            .as_ref()
213            .and_then(|x| NonNullConst::new(x.as_ptr().cast()));
214
215        {
216            let mut lib_data_wrap = lib_data.gfxd_set();
217
218            self.disassemble_impl(data, microcode, &mut lib_data_wrap, dynamic_ptr);
219        }
220
221        lib_data.consume()
222    }
223
224    // Use a wrapper function to make sure lib_data does not get dropped too
225    // soon.
226    fn disassemble_impl(
227        self,
228        data: &[u8],
229        microcode: Microcode,
230        lib_data_wrap: &mut LibDataWrap,
231        dynamic: Option<NonNullConst<ffi::c_char>>,
232    ) {
233        #[allow(clippy::cast_possible_truncation)]
234        let data_len = data.len() as _;
235        // Setup input and output
236        unsafe {
237            // We only use the input_buffer and the output_callback functions
238            // of libgfxd, completely ignoring the output_buffer, input_callback
239            // or the fd ones.
240            // I don't know if it is worth to expose those in the API.
241
242            gfxd_sys::io::gfxd_input_buffer(NonNullConst::new_void(data.as_ptr()), data_len);
243        }
244
245        // Set the microcode
246        unsafe {
247            gfxd_sys::settings::gfxd_target(Some(microcode.to_microcode_ptr()));
248        }
249
250        // Set the dynamic arg, if any
251        unsafe {
252            gfxd_sys::settings::gfxd_dynamic(dynamic);
253        }
254
255        // Set the options
256        set_feature_option(
257            self.stop_on_invalid,
258            gfxd_sys::settings::FeatureOption::gfxd_stop_on_invalid,
259        );
260        set_feature_option(
261            self.stop_on_end,
262            gfxd_sys::settings::FeatureOption::gfxd_stop_on_end,
263        );
264        set_feature_option(
265            self.emit_dec_color,
266            gfxd_sys::settings::FeatureOption::gfxd_emit_dec_color,
267        );
268        set_feature_option(
269            self.emit_q_macro,
270            gfxd_sys::settings::FeatureOption::gfxd_emit_q_macro,
271        );
272        set_feature_option(
273            self.emit_ext_macro,
274            gfxd_sys::settings::FeatureOption::gfxd_emit_ext_macro,
275        );
276
277        // Run
278        lib_data_wrap.do_before();
279        unsafe {
280            gfxd_sys::execution::gfxd_execute();
281        }
282        lib_data_wrap.do_after();
283    }
284}
285
286fn set_feature_option(on: bool, cap: gfxd_sys::settings::FeatureOption) {
287    #[allow(clippy::multiple_unsafe_ops_per_block)]
288    unsafe {
289        if on {
290            gfxd_sys::settings::gfxd_enable(cap);
291        } else {
292            gfxd_sys::settings::gfxd_disable(cap);
293        }
294    }
295}
296
297impl Default for Disassembler<'_> {
298    fn default() -> Self {
299        Self::new()
300    }
301}