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