1use std::path::Path;
11use std::time::Instant;
12
13use monsoon_core::emulation::nes::Nes;
14use monsoon_core::emulation::palette_util::RgbColor;
15use monsoon_core::emulation::ppu_util::{TOTAL_OUTPUT_HEIGHT, TOTAL_OUTPUT_WIDTH};
16use monsoon_core::emulation::rom::RomFile;
17use monsoon_core::emulation::screen_renderer::{ScreenRenderer, create_renderer};
18
19use crate::cli::{
20 CliArgs, ExecutionConfig, ExecutionEngine, ExecutionResult, FpsConfig, MemoryDump, MemoryInit,
21 MemoryInitConfig, MemoryType, OutputWriter, SavestateConfig, StopReason, StreamingVideoEncoder,
22 VideoFormat, VideoResolution, apply_memory_init, apply_memory_init_config, is_ffmpeg_available,
23 parse_memory_range,
24};
25
26pub const NES_WIDTH: u32 = TOTAL_OUTPUT_WIDTH as u32;
33
34pub const NES_HEIGHT: u32 = TOTAL_OUTPUT_HEIGHT as u32;
37
38pub fn create_renderer_from_args(args: &CliArgs) -> Box<dyn ScreenRenderer> {
43 let renderer_name = args.video.renderer.as_deref().unwrap_or("PaletteLookup");
44 let renderers = crate::get_all_renderers();
45 create_renderer(Some(renderer_name), renderers)
46}
47
48pub fn list_renderers() {
50 let renderers = crate::get_all_renderers();
51 println!("Available renderers:");
52 for reg in &renderers {
53 println!(" {} - {}", reg.key, reg.display_name);
54 }
55}
56
57pub fn run_headless(args: &CliArgs) -> Result<(), String> {
65 if args.video.list_renderers {
67 list_renderers();
68 return Ok(());
69 }
70
71 if args.rom.rom_info {
73 return handle_rom_info(args);
74 }
75
76 let start = Instant::now();
77
78 let mut renderer = create_renderer_from_args(args);
80
81 if args.verbose {
82 eprintln!("Using renderer: {}", renderer.get_display_name());
83 }
84
85 let exec_config = ExecutionConfig::from_cli_args(args);
87 let savestate_config = SavestateConfig::from_cli_args(args);
88
89 let mut engine = ExecutionEngine::new()
91 .with_config(exec_config)
92 .with_savestate_config(savestate_config);
93
94 if let Some(ref rom_path) = args.rom.rom {
96 engine.load_rom(rom_path)?;
97 }
98
99 engine.load_savestate()?;
101
102 if !args.power.no_power {
104 engine.power_on();
105 }
106
107 if args.power.reset {
109 engine.reset();
110 }
111
112 apply_memory_initialization(engine.emulator_mut(), args)?;
114
115 let use_streaming = args.video.video_path.is_some();
119
120 let result = if use_streaming {
121 run_with_streaming_video(&mut engine, &mut renderer, args)?
122 } else {
123 engine.run()?
125 };
126
127 if args.verbose {
129 eprintln!("Execution time: {:?}", start.elapsed());
130 eprintln!("Total cycles: {}", result.total_cycles);
131 eprintln!("Total frames: {}", result.total_frames);
132 eprintln!("Stop reason: {:?}", result.stop_reason);
133 }
134
135 engine.save_savestate()?;
137
138 output_results(engine.emulator(), args)?;
140
141 if !use_streaming {
143 save_screenshot(&engine.frames, &mut renderer, args)?;
144 }
145
146 if !use_streaming {
148 save_video(&engine.frames, &mut renderer, args)?;
149 }
150
151 match result.stop_reason {
153 StopReason::Error(e) => Err(e),
154 _ => Ok(()),
155 }
156}
157
158fn handle_rom_info(args: &CliArgs) -> Result<(), String> {
164 let rom_path = args
165 .rom
166 .rom
167 .as_ref()
168 .ok_or("--rom-info requires --rom to be specified")?;
169 print_rom_info(rom_path)
170}
171
172pub fn print_rom_info(rom_path: &Path) -> Result<(), String> {
174 let path_str = rom_path.to_string_lossy().to_string();
175 let data = std::fs::read(rom_path).map_err(|e| format!("Failed to read ROM file: {}", e))?;
176 let rom =
177 RomFile::load(&data, Some(path_str)).map_err(|e| format!("Failed to parse ROM: {}", e))?;
178
179 println!("ROM Information:");
180 println!(" File: {}", rom_path.display());
181 if let Some(ref name) = rom.name {
182 println!(" Name: {}", name);
183 }
184 println!(" Mapper: {}", rom.mapper_number);
185 println!(" PRG ROM: {} KB", rom.prg_memory.prg_rom_size / 1024);
186 println!(" CHR ROM: {} KB", rom.chr_memory.chr_rom_size / 1024);
187 println!(
188 " PRG RAM: {} KB (battery-backed: {})",
189 rom.prg_memory.prg_ram_size / 1024,
190 if rom.is_battery_backed { "yes" } else { "no" }
191 );
192 println!(
193 " Mirroring: {}",
194 if rom.hardwired_nametable_layout {
195 "Vertical"
196 } else {
197 "Horizontal"
198 }
199 );
200 println!(
201 " Checksum (SHA-256): {}",
202 rom.data_checksum
203 .iter()
204 .map(|b| format!("{:02x}", b))
205 .collect::<String>()
206 );
207
208 Ok(())
209}
210
211pub fn apply_memory_initialization(emu: &mut Nes, args: &CliArgs) -> Result<(), String> {
217 let cpu_inits: Vec<MemoryInit> = args
219 .memory
220 .init_cpu
221 .iter()
222 .map(|s| MemoryInit::parse(s))
223 .collect::<Result<Vec<_>, _>>()?;
224
225 let ppu_inits: Vec<MemoryInit> = args
227 .memory
228 .init_ppu
229 .iter()
230 .map(|s| MemoryInit::parse(s))
231 .collect::<Result<Vec<_>, _>>()?;
232
233 let oam_inits: Vec<MemoryInit> = args
235 .memory
236 .init_oam
237 .iter()
238 .map(|s| MemoryInit::parse(s))
239 .collect::<Result<Vec<_>, _>>()?;
240
241 if !cpu_inits.is_empty() || !ppu_inits.is_empty() || !oam_inits.is_empty() {
243 apply_memory_init(emu, &cpu_inits, &ppu_inits, &oam_inits);
244 if args.verbose {
245 eprintln!(
246 "Applied memory init: {} CPU, {} PPU, {} OAM operations",
247 cpu_inits.len(),
248 ppu_inits.len(),
249 oam_inits.len()
250 );
251 }
252 }
253
254 if let Some(ref init_file) = args.memory.init_file {
256 let config = MemoryInitConfig::load_from_file(init_file)?;
257 apply_memory_init_config(emu, &config);
258 if args.verbose {
259 eprintln!(
260 "Applied memory init from file: {} CPU, {} PPU, {} OAM addresses",
261 config.cpu.len(),
262 config.ppu.len(),
263 config.oam.len()
264 );
265 }
266 }
267
268 Ok(())
269}
270
271fn run_with_streaming_video(
281 engine: &mut ExecutionEngine,
282 renderer: &mut Box<dyn ScreenRenderer>,
283 args: &CliArgs,
284) -> Result<ExecutionResult, String> {
285 let video_path = args.video.video_path.as_ref().unwrap();
286
287 if args.video.video_format == VideoFormat::Mp4 && !is_ffmpeg_available() {
289 return Err("MP4 export requires FFmpeg to be installed. \
290 Use --video-format png or --video-format ppm for self-contained export."
291 .to_string());
292 }
293
294 let resolution = VideoResolution::parse(args.video.video_scale.as_ref().unwrap())
296 .map_err(|e| format!("Invalid video scale: {}", e))?;
297
298 let fps_config = FpsConfig::parse(&args.video.video_fps, args.video.video_mode)
300 .map_err(|e| format!("Invalid video FPS: {}", e))?;
301
302 let (dst_width, dst_height) = resolution.dimensions(NES_WIDTH, NES_HEIGHT);
303
304 if !args.quiet {
306 print_video_info(
307 video_path,
308 &args.video.video_format,
309 &resolution,
310 NES_WIDTH,
311 NES_HEIGHT,
312 dst_width,
313 dst_height,
314 true,
315 &fps_config,
316 );
317 }
318
319 let mut encoder = StreamingVideoEncoder::with_fps_config(
321 args.video.video_format,
322 video_path,
323 NES_WIDTH,
324 NES_HEIGHT,
325 &resolution,
326 fps_config,
327 )
328 .map_err(|e| format!("Failed to create video encoder: {}", e))?;
329
330 let result = engine.run_with_video_encoder(&mut encoder, renderer)?;
332
333 encoder
335 .finish()
336 .map_err(|e| format!("Failed to finalize video: {}", e))?;
337
338 if !args.quiet {
339 eprintln!("Exported {} frames successfully", encoder.frames_written());
340 }
341
342 if args.video.screenshot.is_some() {
344 let last_frame = engine.emulator().get_pixel_buffer();
345 let rgb_frame = renderer.buffer_to_image(&last_frame);
346 save_single_screenshot(rgb_frame, args)?;
347 }
348
349 Ok(result)
350}
351
352#[allow(clippy::too_many_arguments)]
354fn print_video_info(
355 video_path: &Path,
356 format: &VideoFormat,
357 resolution: &VideoResolution,
358 src_width: u32,
359 src_height: u32,
360 dst_width: u32,
361 dst_height: u32,
362 streaming: bool,
363 fps_config: &FpsConfig,
364) {
365 let mode_str = if streaming { " [streaming mode]" } else { "" };
366 let fps_str = format!(
367 "{:.4} fps ({:?}, {}x)",
368 fps_config.output_fps(),
369 fps_config.mode,
370 fps_config.multiplier
371 );
372
373 let capture_note = if fps_config.multiplier > 1 {
375 format!(" ({} captures per PPU frame)", fps_config.multiplier)
376 } else {
377 String::new()
378 };
379
380 if *resolution == VideoResolution::Native {
381 eprintln!(
382 "Exporting to {} as {:?} ({}x{}, {}{}){}...",
383 video_path.display(),
384 format,
385 src_width,
386 src_height,
387 fps_str,
388 capture_note,
389 mode_str
390 );
391 } else if matches!(format, VideoFormat::Mp4) {
392 eprintln!(
393 "Exporting to {} as {:?} ({}x{} → {}x{} via FFmpeg nearest-neighbor, {}{}){}...",
394 video_path.display(),
395 format,
396 src_width,
397 src_height,
398 dst_width,
399 dst_height,
400 fps_str,
401 capture_note,
402 mode_str
403 );
404 } else {
405 let scale_note = if streaming {
406 " [streaming mode, scaling only supported for MP4]"
407 } else {
408 ""
409 };
410 eprintln!(
411 "Exporting to {} as {:?} ({}x{}, {}{}){}...",
412 video_path.display(),
413 format,
414 src_width,
415 src_height,
416 fps_str,
417 capture_note,
418 scale_note
419 );
420 }
421}
422
423fn save_single_screenshot(frame: &[RgbColor], args: &CliArgs) -> Result<(), String> {
429 if let Some(ref screenshot_path) = args.video.screenshot {
430 let img: image::RgbaImage = image::ImageBuffer::from_fn(NES_WIDTH, NES_HEIGHT, |x, y| {
431 let color = frame[(y * NES_WIDTH + x) as usize];
432 image::Rgba([color.r, color.g, color.b, 255])
433 });
434
435 img.save(screenshot_path)
436 .map_err(|e| format!("Failed to save screenshot: {}", e))?;
437
438 if !args.quiet {
439 eprintln!("Screenshot saved to {}", screenshot_path.display());
440 }
441 }
442 Ok(())
443}
444
445pub fn save_screenshot(
447 frames: &[Vec<u16>],
448 renderer: &mut Box<dyn ScreenRenderer>,
449 args: &CliArgs,
450) -> Result<(), String> {
451 if let Some(ref screenshot_path) = args.video.screenshot {
452 if frames.is_empty() {
453 eprintln!("Warning: No frames to screenshot");
454 return Ok(());
455 }
456
457 let frame = frames.last().unwrap();
459 let rgb_frame = renderer.buffer_to_image(frame);
460
461 if !args.quiet {
462 eprintln!("Saving screenshot to {}...", screenshot_path.display());
463 }
464
465 let rgb_data: Vec<u8> = rgb_frame.iter().flat_map(|c| [c.r, c.g, c.b]).collect();
467
468 let img: image::ImageBuffer<image::Rgb<u8>, Vec<u8>> =
470 image::ImageBuffer::from_raw(NES_WIDTH, NES_HEIGHT, rgb_data)
471 .ok_or_else(|| "Failed to create image buffer".to_string())?;
472
473 img.save(screenshot_path)
474 .map_err(|e| format!("Failed to save screenshot: {}", e))?;
475
476 if !args.quiet {
477 eprintln!("Screenshot saved successfully");
478 }
479 }
480
481 Ok(())
482}
483
484pub fn save_video(
490 frames: &[Vec<u16>],
491 renderer: &mut Box<dyn ScreenRenderer>,
492 args: &CliArgs,
493) -> Result<(), String> {
494 if let Some(ref video_path) = args.video.video_path {
495 if args.video.video_format == VideoFormat::Mp4 && !is_ffmpeg_available() {
497 return Err("MP4 export requires FFmpeg to be installed. \
498 Use --video-format png or --video-format ppm for self-contained export."
499 .to_string());
500 }
501
502 if frames.is_empty() {
503 eprintln!("Warning: No frames to export");
504 return Ok(());
505 }
506
507 let resolution = VideoResolution::parse(&args.video.video_scale.clone().unwrap())
509 .map_err(|e| format!("Invalid video scale: {}", e))?;
510
511 let fps_config = FpsConfig::parse(&args.video.video_fps, args.video.video_mode)
513 .map_err(|e| format!("Invalid video FPS: {}", e))?;
514
515 let (dst_width, dst_height) = resolution.dimensions(NES_WIDTH, NES_HEIGHT);
516
517 if !args.quiet {
518 eprintln!(
519 "Exporting {} frames to {} as {:?} ({:.2} fps, {:?})...",
520 frames.len(),
521 video_path.display(),
522 args.video.video_format,
523 fps_config.output_fps(),
524 fps_config.mode
525 );
526 if resolution != VideoResolution::Native && args.video.video_format == VideoFormat::Mp4
527 {
528 eprintln!(
529 " Resolution: {}x{} → {}x{} via FFmpeg nearest-neighbor",
530 NES_WIDTH, NES_HEIGHT, dst_width, dst_height
531 );
532 }
533 }
534
535 let mut encoder = StreamingVideoEncoder::with_fps_config(
537 args.video.video_format,
538 video_path,
539 NES_WIDTH,
540 NES_HEIGHT,
541 &resolution,
542 fps_config,
543 )
544 .map_err(|e| format!("Failed to create video encoder: {}", e))?;
545
546 for frame in frames {
547 let rgb_frame = renderer.buffer_to_image(frame);
548 encoder.write_frame(rgb_frame).map_err(|e| e.to_string())?;
549 }
550
551 encoder.finish().map_err(|e| e.to_string())?;
552
553 if !args.quiet {
554 eprintln!("Exported {} frames successfully", encoder.frames_written());
555 }
556 }
557
558 Ok(())
559}
560
561pub fn output_results(emu: &Nes, args: &CliArgs) -> Result<(), String> {
567 OutputWriter::reset();
569
570 let writer = OutputWriter::new(args.output.output.clone(), args.output.effective_format());
572
573 if let Some(ref range) = args.memory.read_cpu {
575 let dump = create_cpu_dump(emu, range)?;
576 writer.write(&dump)?;
577 }
578
579 if let Some(ref range) = args.memory.read_ppu {
580 let dump = create_ppu_dump(emu, range)?;
581 writer.write(&dump)?;
582 }
583
584 if args.memory.dump_oam {
585 let dump = create_oam_dump(emu);
586 writer.write(&dump)?;
587 }
588
589 if args.memory.dump_nametables {
590 let dump = create_nametables_dump(emu);
591 writer.write(&dump)?;
592 }
593
594 if args.memory.dump_palette {
595 let dump = create_palette_dump(emu);
596 writer.write(&dump)?;
597 }
598
599 Ok(())
600}
601
602fn create_cpu_dump(emu: &Nes, range: &str) -> Result<MemoryDump, String> {
608 let (start, end) = parse_memory_range(range)?;
609 let mem = &emu.get_memory_debug(Some(start..=end))[0];
610 Ok(MemoryDump::new(MemoryType::Cpu, start, mem.to_vec()))
611}
612
613fn create_ppu_dump(emu: &Nes, range: &str) -> Result<MemoryDump, String> {
615 let (start, end) = parse_memory_range(range)?;
616 let mem = &emu.get_memory_debug(Some(start..=end))[1];
617 Ok(MemoryDump::new(MemoryType::Ppu, start, mem.to_vec()))
618}
619
620fn create_oam_dump(emu: &Nes) -> MemoryDump {
622 let mem = emu.get_oam_debug();
623 MemoryDump::oam(mem)
624}
625
626fn create_nametables_dump(emu: &Nes) -> MemoryDump {
628 let mem = emu.get_memory_debug(Some(0x2000..=0x2FFF))[1].to_vec();
629 MemoryDump::nametables(mem)
630}
631
632fn create_palette_dump(emu: &Nes) -> MemoryDump {
634 let mem = emu.get_memory_debug(Some(0x3F00..=0x3F1F))[1].to_vec();
636 MemoryDump::palette_ram(mem)
637}