Skip to main content

analyze_state/
analyze_state.rs

1use nes_sim::NES;
2use nes_sim::headless::{frame_to_ppm, stable_byte_hash};
3use nes_sim::{ControllerButton, ControllerState};
4use std::env;
5use std::path::Path;
6use std::process::ExitCode;
7
8fn usage(program: &str) {
9    eprintln!("Usage: {program} <rom-path> <state-path> [frames] [input-mode] [out-dir]");
10    eprintln!(
11        r#"Example: {program} "E:\roms\mapper_007\Time Lord\Time Lord (U) [!].nes" "E:\roms\mapper_007\Time Lord\Time Lord (U) [!].state" 600 right out/timelord-state"#
12    );
13    eprintln!("input-mode: none | right | right_a");
14}
15
16fn changed_pixels(a: &[u8], b: &[u8]) -> usize {
17    a.iter().zip(b).filter(|(x, y)| x != y).count()
18}
19
20fn changed_pixels_region(a: &[u8], b: &[u8], y0: usize, y1: usize) -> usize {
21    let width = 256usize;
22    let start = y0 * width;
23    let end = y1 * width;
24    a[start..end]
25        .iter()
26        .zip(&b[start..end])
27        .filter(|(x, y)| x != y)
28        .count()
29}
30
31fn changed_bbox(a: &[u8], b: &[u8]) -> Option<(usize, usize, usize, usize)> {
32    let width = 256usize;
33    let height = 240usize;
34    let mut min_x = width;
35    let mut min_y = height;
36    let mut max_x = 0usize;
37    let mut max_y = 0usize;
38    let mut any = false;
39
40    for y in 0..height {
41        for x in 0..width {
42            let i = y * width + x;
43            if a[i] != b[i] {
44                any = true;
45                if x < min_x {
46                    min_x = x;
47                }
48                if y < min_y {
49                    min_y = y;
50                }
51                if x > max_x {
52                    max_x = x;
53                }
54                if y > max_y {
55                    max_y = y;
56                }
57            }
58        }
59    }
60
61    if any {
62        Some((min_x, min_y, max_x, max_y))
63    } else {
64        None
65    }
66}
67
68fn main() -> ExitCode {
69    let mut args = env::args();
70    let program = args.next().unwrap_or_else(|| "analyze_state".to_string());
71
72    let Some(rom_path) = args.next() else {
73        usage(&program);
74        return ExitCode::from(2);
75    };
76    let Some(state_path) = args.next() else {
77        usage(&program);
78        return ExitCode::from(2);
79    };
80    let frames = match args.next() {
81        Some(value) => match value.parse::<usize>() {
82            Ok(frames) => frames,
83            Err(error) => {
84                eprintln!("invalid frame count {value:?}: {error}");
85                return ExitCode::from(2);
86            }
87        },
88        None => 240,
89    };
90    let input_mode = args.next().unwrap_or_else(|| "none".to_string());
91    let out_dir = args.next();
92
93    let rom = match std::fs::read(&rom_path) {
94        Ok(rom) => rom,
95        Err(error) => {
96            eprintln!("failed to read ROM {rom_path:?}: {error}");
97            return ExitCode::from(1);
98        }
99    };
100    let state = match std::fs::read(&state_path) {
101        Ok(state) => state,
102        Err(error) => {
103            eprintln!("failed to read state {state_path:?}: {error}");
104            return ExitCode::from(1);
105        }
106    };
107
108    let mut nes = NES::new();
109    if let Err(error) = nes.load_cartridge_ines(&rom) {
110        eprintln!("failed to load ROM {rom_path:?}: {error}");
111        return ExitCode::from(1);
112    }
113    if let Err(error) = nes.load_state(&state) {
114        eprintln!("failed to load state {state_path:?}: {error}");
115        return ExitCode::from(1);
116    }
117
118    let mut prev = nes.frame_pixels().to_vec();
119    let mut prev_hash = stable_byte_hash(&frame_to_ppm(nes.video_frame()));
120    let mut same_hash_run = 0usize;
121    println!(
122        "start frame={} hash=0x{:016X}",
123        nes.frame_number(),
124        prev_hash
125    );
126
127    if let Some(ref out_dir) = out_dir {
128        if !out_dir.is_empty() {
129            let path = Path::new(out_dir);
130            if let Err(error) = std::fs::create_dir_all(path) {
131                eprintln!("failed to create output directory {:?}: {}", path, error);
132                return ExitCode::from(1);
133            }
134        }
135    }
136
137    for i in 1..=frames {
138        let mut controller = ControllerState::new();
139        if input_mode == "right" || input_mode == "right_a" {
140            controller.set_pressed(ControllerButton::Right, true);
141        }
142        if input_mode == "right_a" {
143            controller.set_pressed(ControllerButton::A, true);
144        }
145        nes.set_controller_state(0, controller);
146        nes.run_frame();
147        let frame = nes.frame_pixels().to_vec();
148        let hash = stable_byte_hash(&frame_to_ppm(nes.video_frame()));
149        let changed_all = changed_pixels(&prev, &frame);
150        let changed_top_hud = changed_pixels_region(&prev, &frame, 0, 48);
151        let changed_bottom_hud = changed_pixels_region(&prev, &frame, 208, 240);
152        let changed_gameplay = changed_pixels_region(&prev, &frame, 48, 208);
153        let debug = nes.debug_snapshot();
154
155        if hash == prev_hash {
156            same_hash_run += 1;
157        } else {
158            same_hash_run = 0;
159        }
160
161        if let Some((min_x, min_y, max_x, max_y)) = changed_bbox(&prev, &frame) {
162            println!(
163                "i={} frame={} hash=0x{:016X} changed_all={} changed_top_hud={} changed_bottom_hud={} changed_gameplay={} bbox=({},{})->({},{}) same_hash_run={} pc={:04X} ppu_scanline={} in_vblank={}",
164                i,
165                nes.frame_number(),
166                hash,
167                changed_all,
168                changed_top_hud,
169                changed_bottom_hud,
170                changed_gameplay,
171                min_x,
172                min_y,
173                max_x,
174                max_y,
175                same_hash_run,
176                debug.cpu.pc,
177                debug.ppu.scanline,
178                debug.ppu.in_vblank
179            );
180        } else {
181            println!(
182                "i={} frame={} hash=0x{:016X} changed_all=0 changed_top_hud=0 changed_bottom_hud=0 changed_gameplay=0 bbox=none same_hash_run={} pc={:04X} ppu_scanline={} in_vblank={}",
183                i,
184                nes.frame_number(),
185                hash,
186                same_hash_run,
187                debug.cpu.pc,
188                debug.ppu.scanline,
189                debug.ppu.in_vblank
190            );
191        }
192
193        if let Some(ref out_dir) = out_dir
194            && !out_dir.is_empty()
195            && (i <= 8 || same_hash_run >= 30 || i % 60 == 0)
196        {
197            let ppm = frame_to_ppm(nes.video_frame());
198            let output = Path::new(out_dir).join(format!("frame_{:04}.ppm", i));
199            if let Err(error) = std::fs::write(&output, ppm) {
200                eprintln!("failed to write {:?}: {}", output, error);
201                return ExitCode::from(1);
202            }
203        }
204
205        prev = frame;
206        prev_hash = hash;
207    }
208
209    ExitCode::SUCCESS
210}