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 {
149 save_video(&engine.frames, &mut renderer, args)?;
150 }
151
152 match result.stop_reason {
154 StopReason::Error(e) => Err(e),
155 _ => Ok(()),
156 }
157}
158
159fn handle_rom_info(args: &CliArgs) -> Result<(), String> {
165 let rom_path = args
166 .rom
167 .rom
168 .as_ref()
169 .ok_or("--rom-info requires --rom to be specified")?;
170 print_rom_info(rom_path)
171}
172
173pub fn print_rom_info(rom_path: &Path) -> Result<(), String> {
175 let path_str = rom_path.to_string_lossy().to_string();
176 let data = std::fs::read(rom_path).map_err(|e| format!("Failed to read ROM file: {}", e))?;
177 let rom =
178 RomFile::load(&data, Some(path_str)).map_err(|e| format!("Failed to parse ROM: {}", e))?;
179
180 println!("ROM Information:");
181 println!(" File: {}", rom_path.display());
182 if let Some(ref name) = rom.name {
183 println!(" Name: {}", name);
184 }
185 println!(" Mapper: {}", rom.mapper);
186 println!(" PRG ROM: {} KB", rom.prg_memory.prg_rom_size / 1024);
187 println!(" CHR ROM: {} KB", rom.chr_memory.chr_rom_size / 1024);
188 println!(
189 " PRG RAM: {} KB (battery-backed: {})",
190 rom.prg_memory.prg_ram_size / 1024,
191 if rom.is_battery_backed { "yes" } else { "no" }
192 );
193 println!(
194 " Mirroring: {}",
195 if rom.hardwired_nametable_layout {
196 "Vertical"
197 } else {
198 "Horizontal"
199 }
200 );
201 println!(
202 " Checksum (SHA-256): {}",
203 rom.data_checksum
204 .iter()
205 .map(|b| format!("{:02x}", b))
206 .collect::<String>()
207 );
208
209 Ok(())
210}
211
212pub fn apply_memory_initialization(emu: &mut Nes, args: &CliArgs) -> Result<(), String> {
218 let cpu_inits: Vec<MemoryInit> = args
220 .memory
221 .init_cpu
222 .iter()
223 .map(|s| MemoryInit::parse(s))
224 .collect::<Result<Vec<_>, _>>()?;
225
226 let ppu_inits: Vec<MemoryInit> = args
228 .memory
229 .init_ppu
230 .iter()
231 .map(|s| MemoryInit::parse(s))
232 .collect::<Result<Vec<_>, _>>()?;
233
234 let oam_inits: Vec<MemoryInit> = args
236 .memory
237 .init_oam
238 .iter()
239 .map(|s| MemoryInit::parse(s))
240 .collect::<Result<Vec<_>, _>>()?;
241
242 if !cpu_inits.is_empty() || !ppu_inits.is_empty() || !oam_inits.is_empty() {
244 apply_memory_init(emu, &cpu_inits, &ppu_inits, &oam_inits);
245 if args.verbose {
246 eprintln!(
247 "Applied memory init: {} CPU, {} PPU, {} OAM operations",
248 cpu_inits.len(),
249 ppu_inits.len(),
250 oam_inits.len()
251 );
252 }
253 }
254
255 if let Some(ref init_file) = args.memory.init_file {
257 let config = MemoryInitConfig::load_from_file(init_file)?;
258 apply_memory_init_config(emu, &config);
259 if args.verbose {
260 eprintln!(
261 "Applied memory init from file: {} CPU, {} PPU, {} OAM addresses",
262 config.cpu.len(),
263 config.ppu.len(),
264 config.oam.len()
265 );
266 }
267 }
268
269 Ok(())
270}
271
272fn run_with_streaming_video(
282 engine: &mut ExecutionEngine,
283 renderer: &mut Box<dyn ScreenRenderer>,
284 args: &CliArgs,
285) -> Result<ExecutionResult, String> {
286 let video_path = args.video.video_path.as_ref().unwrap();
287
288 if args.video.video_format == VideoFormat::Mp4 && !is_ffmpeg_available() {
290 return Err(
291 "MP4 export requires FFmpeg to be installed. Use --video-format png or --video-format \
292 ppm for self-contained export."
293 .to_string(),
294 );
295 }
296
297 let resolution = VideoResolution::parse(args.video.video_scale.as_ref().unwrap())
299 .map_err(|e| format!("Invalid video scale: {}", e))?;
300
301 let fps_config = FpsConfig::parse(&args.video.video_fps, args.video.video_mode)
303 .map_err(|e| format!("Invalid video FPS: {}", e))?;
304
305 let (dst_width, dst_height) = resolution.dimensions(NES_WIDTH, NES_HEIGHT);
306
307 if !args.quiet {
309 print_video_info(
310 video_path,
311 &args.video.video_format,
312 &resolution,
313 NES_WIDTH,
314 NES_HEIGHT,
315 dst_width,
316 dst_height,
317 true,
318 &fps_config,
319 );
320 }
321
322 let mut encoder = StreamingVideoEncoder::with_fps_config(
324 args.video.video_format,
325 video_path,
326 NES_WIDTH,
327 NES_HEIGHT,
328 &resolution,
329 fps_config,
330 )
331 .map_err(|e| format!("Failed to create video encoder: {}", e))?;
332
333 let result = engine.run_with_video_encoder(&mut encoder, renderer)?;
335
336 encoder
338 .finish()
339 .map_err(|e| format!("Failed to finalize video: {}", e))?;
340
341 if !args.quiet {
342 eprintln!("Exported {} frames successfully", encoder.frames_written());
343 }
344
345 if args.video.screenshot.is_some() {
347 let last_frame = engine.emulator().get_pixel_buffer();
348 let rgb_frame = renderer.buffer_to_image(&last_frame);
349 save_single_screenshot(rgb_frame, args)?;
350 }
351
352 Ok(result)
353}
354
355#[allow(clippy::too_many_arguments)]
357fn print_video_info(
358 video_path: &Path,
359 format: &VideoFormat,
360 resolution: &VideoResolution,
361 src_width: u32,
362 src_height: u32,
363 dst_width: u32,
364 dst_height: u32,
365 streaming: bool,
366 fps_config: &FpsConfig,
367) {
368 let mode_str = if streaming { " [streaming mode]" } else { "" };
369 let fps_str = format!(
370 "{:.4} fps ({:?}, {}x)",
371 fps_config.output_fps(),
372 fps_config.mode,
373 fps_config.multiplier
374 );
375
376 let capture_note = if fps_config.multiplier > 1 {
378 format!(" ({} captures per PPU frame)", fps_config.multiplier)
379 } else {
380 String::new()
381 };
382
383 if *resolution == VideoResolution::Native {
384 eprintln!(
385 "Exporting to {} as {:?} ({}x{}, {}{}){}...",
386 video_path.display(),
387 format,
388 src_width,
389 src_height,
390 fps_str,
391 capture_note,
392 mode_str
393 );
394 } else if matches!(format, VideoFormat::Mp4) {
395 eprintln!(
396 "Exporting to {} as {:?} ({}x{} → {}x{} via FFmpeg nearest-neighbor, {}{}){}...",
397 video_path.display(),
398 format,
399 src_width,
400 src_height,
401 dst_width,
402 dst_height,
403 fps_str,
404 capture_note,
405 mode_str
406 );
407 } else {
408 let scale_note = if streaming {
409 " [streaming mode, scaling only supported for MP4]"
410 } else {
411 ""
412 };
413 eprintln!(
414 "Exporting to {} as {:?} ({}x{}, {}{}){}...",
415 video_path.display(),
416 format,
417 src_width,
418 src_height,
419 fps_str,
420 capture_note,
421 scale_note
422 );
423 }
424}
425
426fn save_single_screenshot(frame: &[RgbColor], args: &CliArgs) -> Result<(), String> {
432 if let Some(ref screenshot_path) = args.video.screenshot {
433 let img: image::RgbaImage = image::ImageBuffer::from_fn(NES_WIDTH, NES_HEIGHT, |x, y| {
434 let color = frame[(y * NES_WIDTH + x) as usize];
435 image::Rgba([color.r, color.g, color.b, 255])
436 });
437
438 img.save(screenshot_path)
439 .map_err(|e| format!("Failed to save screenshot: {}", e))?;
440
441 if !args.quiet {
442 eprintln!("Screenshot saved to {}", screenshot_path.display());
443 }
444 }
445 Ok(())
446}
447
448pub fn save_screenshot(
450 frames: &[Vec<u16>],
451 renderer: &mut Box<dyn ScreenRenderer>,
452 args: &CliArgs,
453) -> Result<(), String> {
454 if let Some(ref screenshot_path) = args.video.screenshot {
455 if frames.is_empty() {
456 eprintln!("Warning: No frames to screenshot");
457 return Ok(());
458 }
459
460 let frame = frames.last().unwrap();
462 let rgb_frame = renderer.buffer_to_image(frame);
463
464 if !args.quiet {
465 eprintln!("Saving screenshot to {}...", screenshot_path.display());
466 }
467
468 let rgb_data: Vec<u8> = rgb_frame.iter().flat_map(|c| [c.r, c.g, c.b]).collect();
470
471 let img: image::ImageBuffer<image::Rgb<u8>, Vec<u8>> =
473 image::ImageBuffer::from_raw(NES_WIDTH, NES_HEIGHT, rgb_data)
474 .ok_or_else(|| "Failed to create image buffer".to_string())?;
475
476 img.save(screenshot_path)
477 .map_err(|e| format!("Failed to save screenshot: {}", e))?;
478
479 if !args.quiet {
480 eprintln!("Screenshot saved successfully");
481 }
482 }
483
484 Ok(())
485}
486
487pub fn save_video(
493 frames: &[Vec<u16>],
494 renderer: &mut Box<dyn ScreenRenderer>,
495 args: &CliArgs,
496) -> Result<(), String> {
497 if let Some(ref video_path) = args.video.video_path {
498 if args.video.video_format == VideoFormat::Mp4 && !is_ffmpeg_available() {
500 return Err(
501 "MP4 export requires FFmpeg to be installed. Use --video-format png or \
502 --video-format ppm for self-contained export."
503 .to_string(),
504 );
505 }
506
507 if frames.is_empty() {
508 eprintln!("Warning: No frames to export");
509 return Ok(());
510 }
511
512 let resolution = VideoResolution::parse(&args.video.video_scale.clone().unwrap())
514 .map_err(|e| format!("Invalid video scale: {}", e))?;
515
516 let fps_config = FpsConfig::parse(&args.video.video_fps, args.video.video_mode)
518 .map_err(|e| format!("Invalid video FPS: {}", e))?;
519
520 let (dst_width, dst_height) = resolution.dimensions(NES_WIDTH, NES_HEIGHT);
521
522 if !args.quiet {
523 eprintln!(
524 "Exporting {} frames to {} as {:?} ({:.2} fps, {:?})...",
525 frames.len(),
526 video_path.display(),
527 args.video.video_format,
528 fps_config.output_fps(),
529 fps_config.mode
530 );
531 if resolution != VideoResolution::Native && args.video.video_format == VideoFormat::Mp4
532 {
533 eprintln!(
534 " Resolution: {}x{} → {}x{} via FFmpeg nearest-neighbor",
535 NES_WIDTH, NES_HEIGHT, dst_width, dst_height
536 );
537 }
538 }
539
540 let mut encoder = StreamingVideoEncoder::with_fps_config(
542 args.video.video_format,
543 video_path,
544 NES_WIDTH,
545 NES_HEIGHT,
546 &resolution,
547 fps_config,
548 )
549 .map_err(|e| format!("Failed to create video encoder: {}", e))?;
550
551 for frame in frames {
552 let rgb_frame = renderer.buffer_to_image(frame);
553 encoder.write_frame(rgb_frame).map_err(|e| e.to_string())?;
554 }
555
556 encoder.finish().map_err(|e| e.to_string())?;
557
558 if !args.quiet {
559 eprintln!("Exported {} frames successfully", encoder.frames_written());
560 }
561 }
562
563 Ok(())
564}
565
566pub fn output_results(emu: &Nes, args: &CliArgs) -> Result<(), String> {
572 OutputWriter::reset();
574
575 let writer = OutputWriter::new(args.output.output.clone(), args.output.effective_format());
577
578 if let Some(ref range) = args.memory.read_cpu {
580 let dump = create_cpu_dump(emu, range)?;
581 writer.write(&dump)?;
582 }
583
584 if let Some(ref range) = args.memory.read_ppu {
585 let dump = create_ppu_dump(emu, range)?;
586 writer.write(&dump)?;
587 }
588
589 if args.memory.dump_oam {
590 let dump = create_oam_dump(emu);
591 writer.write(&dump)?;
592 }
593
594 if args.memory.dump_nametables {
595 let dump = create_nametables_dump(emu);
596 writer.write(&dump)?;
597 }
598
599 if args.memory.dump_palette {
600 let dump = create_palette_dump(emu);
601 writer.write(&dump)?;
602 }
603
604 Ok(())
605}
606
607fn create_cpu_dump(emu: &Nes, range: &str) -> Result<MemoryDump, String> {
613 let (start, end) = parse_memory_range(range)?;
614 let mem = &emu.get_memory_debug(Some(start..=end))[0];
615 Ok(MemoryDump::new(MemoryType::Cpu, start, mem.to_vec()))
616}
617
618fn create_ppu_dump(emu: &Nes, range: &str) -> Result<MemoryDump, String> {
620 let (start, end) = parse_memory_range(range)?;
621 let mem = &emu.get_memory_debug(Some(start..=end))[1];
622 Ok(MemoryDump::new(MemoryType::Ppu, start, mem.to_vec()))
623}
624
625fn create_oam_dump(emu: &Nes) -> MemoryDump {
627 let mem = emu.get_oam_debug();
628 MemoryDump::oam(mem)
629}
630
631fn create_nametables_dump(emu: &Nes) -> MemoryDump {
633 let mem = emu.get_memory_debug(Some(0x2000..=0x2FFF))[1].to_vec();
634 MemoryDump::nametables(mem)
635}
636
637fn create_palette_dump(emu: &Nes) -> MemoryDump {
639 let mem = emu.get_memory_debug(Some(0x3F00..=0x3F1F))[1].to_vec();
641 MemoryDump::palette_ram(mem)
642}