Skip to main content

monsoon_cli/cli/
execution.rs

1//! Execution engine for CLI-driven emulation.
2//!
3//! This module provides a generic, extensible execution engine that can run
4//! emulation until various stop conditions are met. It's designed to be usable
5//! both from the CLI and as a Rust crate API.
6//!
7//! # Design Goals
8//! - Generic stop condition system that's easy to extend
9//! - Support for frames, cycles, PC breakpoints, memory conditions
10//! - Clean separation from CLI argument parsing
11//! - Suitable for exposing as a crate API
12
13use std::io::{Read, Write};
14use std::path::{Path, PathBuf};
15
16use monsoon_core::emulation::nes::{MASTER_CYCLES_PER_FRAME, Nes, RunOptions};
17use monsoon_core::emulation::savestate::{SaveState, try_load_state_from_bytes};
18use monsoon_core::util::ToBytes;
19
20use crate::cli::args::parse_hex_u8;
21// =============================================================================
22// Stop Conditions
23// =============================================================================
24
25/// Memory access type for watchpoints
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum MemoryAccessType {
28    /// Watch for reads only
29    Read,
30    /// Watch for writes only
31    Write,
32    /// Watch for both reads and writes
33    ReadWrite,
34}
35
36impl MemoryAccessType {
37    /// Parse access type from string (r, w, rw)
38    pub fn parse(s: &str) -> Result<Self, String> {
39        match s.to_lowercase().as_str() {
40            "r" | "read" => Ok(Self::Read),
41            "w" | "write" => Ok(Self::Write),
42            "rw" | "readwrite" | "both" => Ok(Self::ReadWrite),
43            _ => Err(format!(
44                "Invalid memory access type '{}'. Expected: r, w, or rw",
45                s
46            )),
47        }
48    }
49}
50
51/// Reason why execution stopped
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum StopReason {
54    /// Reached target cycle count
55    CyclesReached(u128),
56    /// Reached target frame count
57    FramesReached(u64),
58    /// PC reached target address (breakpoint)
59    PcReached(u16),
60    /// Memory condition was met
61    MemoryCondition(u16, u8),
62    /// Memory watchpoint triggered
63    MemoryWatchpoint {
64        addr: u16,
65        access_type: MemoryAccessType,
66        was_read: bool,
67    },
68    /// HLT (illegal halt) instruction executed
69    Halted,
70    /// User-requested stop (e.g., breakpoint)
71    Breakpoint(u16),
72    /// Execution error occurred
73    Error(String),
74    /// No stop condition was set (ran to completion or max cycles)
75    Completed,
76}
77
78/// A stop condition that can be checked during execution
79#[derive(Debug, Clone)]
80pub enum StopCondition {
81    /// Stop after N master cycles
82    Cycles(u128),
83    /// Stop after N frames
84    Frames(u64),
85    /// Stop when PC reaches address (breakpoint)
86    PcEquals(u16),
87    /// Stop when opcode is executed
88    Opcode(u8),
89    /// Stop when memory address equals value
90    MemoryEquals {
91        addr: u16,
92        value: u8,
93        and: Option<Box<StopCondition>>,
94    },
95    /// Stop when memory address does not equal value
96    MemoryNotEquals {
97        addr: u16,
98        value: u8,
99        and: Option<Box<StopCondition>>,
100    },
101    /// Stop on HLT instruction
102    OnHalt,
103    /// Breakpoint at address (alias for PcEquals, kept for backward compatibility)
104    Breakpoint(u16),
105    /// Watch memory address for access
106    MemoryWatch {
107        addr: u16,
108        access_type: MemoryAccessType,
109    },
110}
111
112impl StopCondition {
113    /// Parse a memory condition string like "0x6000==0x80" or "0x6000!=0x00"
114    pub fn parse_memory_condition(vec: &Vec<String>) -> Result<Vec<Self>, String> {
115        let mut res = Vec::new();
116        for s in vec {
117            let cond = Self::parse_single_condition(s);
118
119            #[allow(clippy::question_mark)]
120            if let Ok(cond) = cond {
121                res.push(cond)
122            } else if let Err(cond) = cond {
123                return Err(cond);
124            }
125        }
126
127        Ok(res)
128    }
129
130    pub fn parse_single_condition(s: &String) -> Result<Self, String> {
131        if let Some((cond1, cond2)) = s.split_once("&&") {
132            let cond1 = Self::parse_single_condition(&cond1.to_string());
133            let cond2 = Self::parse_single_condition(&cond2.to_string());
134
135            if let (Ok(cond1), Ok(cond2)) = (cond1, cond2) {
136                match cond1 {
137                    StopCondition::MemoryEquals {
138                        addr,
139                        value,
140                        ..
141                    } => {
142                        return Ok(StopCondition::MemoryEquals {
143                            addr,
144                            value,
145                            and: Some(Box::new(cond2)),
146                        });
147                    }
148                    StopCondition::MemoryNotEquals {
149                        addr,
150                        value,
151                        ..
152                    } => {
153                        return Ok(StopCondition::MemoryNotEquals {
154                            addr,
155                            value,
156                            and: Some(Box::new(cond2)),
157                        });
158                    }
159                    _ => {}
160                }
161            }
162        }
163
164        if let Some((addr_str, val_str)) = s.split_once("==") {
165            let addr = parse_hex_u16(addr_str.trim())?;
166            let value = parse_hex_u8(val_str.trim())?;
167            Ok(StopCondition::MemoryEquals {
168                addr,
169                value,
170                and: None,
171            })
172        } else if let Some((addr_str, val_str)) = s.split_once("!=") {
173            let addr = parse_hex_u16(addr_str.trim())?;
174            let value = parse_hex_u8(val_str.trim())?;
175            Ok(StopCondition::MemoryNotEquals {
176                addr,
177                value,
178                and: None,
179            })
180        } else {
181            Err(format!(
182                "Invalid memory condition '{}'. Expected format: ADDR==VALUE or ADDR!=VALUE",
183                s
184            ))
185        }
186    }
187
188    /// Parse a memory watch string like "0x2002" or "0x2002:r" or "0x4016:w"
189    pub fn parse_memory_watch(s: &str) -> Result<Self, String> {
190        let (addr_str, access_type) = if let Some((addr_part, mode_part)) = s.split_once(':') {
191            (addr_part, MemoryAccessType::parse(mode_part)?)
192        } else {
193            (s, MemoryAccessType::ReadWrite) // Default to both reads and writes
194        };
195
196        let addr = parse_hex_u16(addr_str.trim())?;
197        Ok(StopCondition::MemoryWatch {
198            addr,
199            access_type,
200        })
201    }
202
203    /// Parse multiple memory watch conditions
204    pub fn parse_memory_watches(watches: &[String]) -> Result<Vec<Self>, String> {
205        watches
206            .iter()
207            .map(|s| Self::parse_memory_watch(s))
208            .collect()
209    }
210
211    pub fn check(&self, emu: &Nes, cycles: u128, frames: u64) -> bool {
212        match self {
213            StopCondition::Cycles(target) => cycles >= *target,
214            StopCondition::Frames(target) => frames >= *target,
215            StopCondition::PcEquals(addr) | StopCondition::Breakpoint(addr) => {
216                emu.program_counter() == *addr
217            }
218            StopCondition::Opcode(op) => {
219                if let Some(opcode) = emu.current_opcode_byte()
220                    && opcode == *op
221                {
222                    return true;
223                }
224
225                false
226            }
227            StopCondition::MemoryEquals {
228                addr,
229                value,
230                and,
231            } => {
232                let and = and.as_ref().map(|and| and.check(emu, cycles, frames));
233
234                let mem_val = emu.get_memory_debug(Some(*addr..=*addr))[0]
235                    .first()
236                    .copied()
237                    .unwrap_or(0);
238
239                if let Some(and) = and {
240                    mem_val == *value && and
241                } else {
242                    mem_val == *value
243                }
244            }
245            StopCondition::MemoryNotEquals {
246                addr,
247                value,
248                and,
249            } => {
250                let and = and.as_ref().map(|and| and.check(emu, cycles, frames));
251
252                let mem_val = emu.get_memory_debug(Some(*addr..=*addr))[0]
253                    .first()
254                    .copied()
255                    .unwrap_or(0);
256
257                if let Some(and) = and {
258                    mem_val != *value && and
259                } else {
260                    mem_val != *value
261                }
262            }
263            StopCondition::OnHalt => emu.is_halted(),
264            StopCondition::MemoryWatch {
265                addr,
266                access_type,
267            } => {
268                // Check if CPU accessed this address
269                if let Some(last_access) = emu.last_memory_access() {
270                    let (access_addr, was_read, _) = last_access;
271                    if access_addr == *addr {
272                        match access_type {
273                            MemoryAccessType::Read => was_read,
274                            MemoryAccessType::Write => !was_read,
275                            MemoryAccessType::ReadWrite => true,
276                        }
277                    } else {
278                        false
279                    }
280                } else {
281                    false
282                }
283            }
284        }
285    }
286
287    pub fn reason(&self, emu: &Nes, cycles: u128, frames: u64) -> StopReason {
288        match self {
289            StopCondition::Cycles(_) => StopReason::CyclesReached(cycles),
290            StopCondition::Frames(_) => StopReason::FramesReached(frames),
291            StopCondition::PcEquals(addr) | StopCondition::Breakpoint(addr) => {
292                StopReason::PcReached(*addr)
293            }
294            StopCondition::Opcode(_) => StopReason::PcReached(emu.program_counter()),
295            StopCondition::MemoryEquals {
296                addr, ..
297            }
298            | StopCondition::MemoryNotEquals {
299                addr, ..
300            } => {
301                let mem_val = emu.get_memory_debug(Some(*addr..=*addr))[0]
302                    .first()
303                    .copied()
304                    .unwrap_or(0);
305
306                StopReason::MemoryCondition(*addr, mem_val)
307            }
308            StopCondition::OnHalt => StopReason::Halted,
309            StopCondition::MemoryWatch {
310                addr,
311                access_type,
312            } => {
313                let was_read = emu
314                    .last_memory_access()
315                    .map(|(_, was_read, _)| was_read)
316                    .unwrap_or(true);
317                StopReason::MemoryWatchpoint {
318                    addr: *addr,
319                    access_type: *access_type,
320                    was_read,
321                }
322            }
323        }
324    }
325}
326
327// =============================================================================
328// Execution Configuration
329// =============================================================================
330
331/// Configuration for an execution run.
332///
333/// This struct is designed to be constructed either from CLI arguments
334/// or programmatically when using the crate as a library.
335#[derive(Debug, Clone, Default)]
336pub struct ExecutionConfig {
337    /// Stop conditions (first one met will stop execution)
338    pub stop_conditions: Vec<StopCondition>,
339    /// Whether to stop on any HLT instruction
340    pub stop_on_halt: bool,
341    /// Path to trace log file (if any)
342    pub trace_path: Option<PathBuf>,
343    /// Verbose output
344    pub verbose: bool,
345}
346
347impl ExecutionConfig {
348    /// Create a new empty execution config
349    pub fn new() -> Self { Self::default() }
350
351    /// Add a stop condition
352    pub fn with_stop_condition(mut self, condition: StopCondition) -> Self {
353        self.stop_conditions.push(condition);
354        self
355    }
356
357    /// Set stop after N cycles
358    pub fn with_cycles(mut self, cycles: u128) -> Self {
359        self.stop_conditions.push(StopCondition::Cycles(cycles));
360        self
361    }
362
363    /// Set stop after N frames
364    pub fn with_frames(mut self, frames: u64) -> Self {
365        self.stop_conditions.push(StopCondition::Frames(frames));
366        self
367    }
368
369    /// Set stop when PC equals address
370    pub fn with_pc_breakpoint(mut self, addr: u16) -> Self {
371        self.stop_conditions.push(StopCondition::PcEquals(addr));
372        self
373    }
374
375    /// Add a breakpoint (alias for with_pc_breakpoint)
376    pub fn with_breakpoint(mut self, addr: u16) -> Self {
377        self.stop_conditions.push(StopCondition::PcEquals(addr));
378        self
379    }
380
381    /// Add a memory watchpoint
382    pub fn with_memory_watch(mut self, addr: u16, access_type: MemoryAccessType) -> Self {
383        self.stop_conditions.push(StopCondition::MemoryWatch {
384            addr,
385            access_type,
386        });
387        self
388    }
389
390    /// Set trace log path
391    pub fn with_trace(mut self, path: PathBuf) -> Self {
392        self.trace_path = Some(path);
393        self
394    }
395
396    /// Enable verbose output
397    pub fn with_verbose(mut self, verbose: bool) -> Self {
398        self.verbose = verbose;
399        self
400    }
401
402    /// Enable stop on HLT
403    pub fn with_stop_on_halt(mut self, stop: bool) -> Self {
404        self.stop_on_halt = stop;
405        self
406    }
407
408    /// Calculate the maximum cycles to run based on stop conditions
409    fn max_cycles(&self) -> u128 {
410        let mut max = u128::MAX;
411        for cond in &self.stop_conditions {
412            match cond {
413                StopCondition::Cycles(c) => max = max.min(*c),
414                StopCondition::Frames(f) => {
415                    max = max.min(*f as u128 * MASTER_CYCLES_PER_FRAME as u128)
416                }
417                _ => {}
418            }
419        }
420        max
421    }
422
423    /// Check if any stop condition is met
424    fn check_conditions(&self, emu: &Nes, cycles: u128, frames: u64) -> Option<StopReason> {
425        for cond in &self.stop_conditions {
426            if cond.check(emu, cycles, frames) {
427                return Some(cond.reason(emu, cycles, frames));
428            }
429        }
430
431        None
432    }
433}
434
435// =============================================================================
436// Execution Result
437// =============================================================================
438
439/// Result of an execution run
440#[derive(Debug, Clone)]
441pub struct ExecutionResult {
442    /// Why execution stopped
443    pub stop_reason: StopReason,
444    /// Total cycles executed
445    pub total_cycles: u128,
446    /// Total frames executed
447    pub total_frames: u64,
448}
449
450// =============================================================================
451// Savestate Configuration
452// =============================================================================
453
454/// Source for loading a savestate
455#[derive(Debug, Clone)]
456pub enum SavestateSource {
457    /// Load from file
458    File(PathBuf),
459    /// Load from stdin
460    Stdin,
461    /// Load from bytes (for programmatic use)
462    Bytes(Vec<u8>),
463}
464
465/// Destination for saving a savestate
466#[derive(Debug, Clone)]
467pub enum SavestateDestination {
468    /// Save to file
469    File(PathBuf),
470    /// Save to stdout
471    Stdout,
472}
473
474// Re-export SavestateFormat from args for use in this module
475pub use crate::cli::args::SavestateFormat;
476use crate::cli::{CliArgs, parse_hex_u16};
477
478/// Configuration for savestate operations
479#[derive(Debug, Clone, Default)]
480pub struct SavestateConfig {
481    /// Source to load savestate from (if any)
482    pub load_from: Option<SavestateSource>,
483    /// Destination to save savestate to (if any)
484    pub save_to: Option<SavestateDestination>,
485    /// Format for saving savestates
486    pub format: SavestateFormat,
487}
488
489impl SavestateConfig {
490    /// Create a new empty savestate config
491    pub fn new() -> Self { Self::default() }
492
493    /// Set load source to file
494    pub fn load_from_file(mut self, path: PathBuf) -> Self {
495        self.load_from = Some(SavestateSource::File(path));
496        self
497    }
498
499    /// Set load source to stdin
500    pub fn load_from_stdin(mut self) -> Self {
501        self.load_from = Some(SavestateSource::Stdin);
502        self
503    }
504
505    /// Set save destination to file
506    pub fn save_to_file(mut self, path: PathBuf) -> Self {
507        self.save_to = Some(SavestateDestination::File(path));
508        self
509    }
510
511    /// Set save destination to stdout
512    pub fn save_to_stdout(mut self) -> Self {
513        self.save_to = Some(SavestateDestination::Stdout);
514        self
515    }
516
517    /// Set savestate format
518    pub fn with_format(mut self, format: SavestateFormat) -> Self {
519        self.format = format;
520        self
521    }
522}
523
524// =============================================================================
525// Execution Engine
526// =============================================================================
527
528/// The main execution engine for CLI-driven emulation.
529///
530/// This struct manages the emulator lifecycle and provides a clean API
531/// for running emulation with various configurations.
532///
533/// # Video Export Modes
534///
535/// - **Buffered mode** (default): All frames are stored in memory, then encoded at the end.
536///   Suitable for small exports or when you need access to all frames.
537///
538/// - **Streaming mode**: Frames are encoded immediately as they are generated.
539///   Use `run_with_video_encoder()` for this mode. Significantly reduces memory usage
540///   for long recordings.
541pub struct ExecutionEngine {
542    /// The emulator instance
543    pub emu: Nes,
544    /// Execution configuration
545    pub config: ExecutionConfig,
546    /// Savestate configuration
547    pub savestate_config: SavestateConfig,
548    /// Collected frames (used in buffered mode) - raw palette indices
549    pub frames: Vec<Vec<u16>>,
550    /// Track current frame count
551    frame_count: u64,
552    /// Whether to collect frames (set to false for streaming mode)
553    collect_frames: bool,
554}
555
556impl ExecutionEngine {
557    /// Create a new execution engine with default emulator
558    pub fn new() -> Self {
559        Self {
560            emu: Nes::default(),
561            config: ExecutionConfig::new(),
562            savestate_config: SavestateConfig::new(),
563            frames: vec![],
564            frame_count: 0,
565            collect_frames: true,
566        }
567    }
568
569    /// Create execution engine with existing emulator
570    pub fn with_emulator(emu: Nes) -> Self {
571        Self {
572            emu,
573            config: ExecutionConfig::new(),
574            savestate_config: SavestateConfig::new(),
575            frames: vec![],
576            frame_count: 0,
577            collect_frames: true,
578        }
579    }
580
581    /// Set execution configuration
582    pub fn with_config(mut self, config: ExecutionConfig) -> Self {
583        self.config = config;
584        self
585    }
586
587    /// Set savestate configuration
588    pub fn with_savestate_config(mut self, config: SavestateConfig) -> Self {
589        self.savestate_config = config;
590        self
591    }
592
593    /// Load ROM from path
594    pub fn load_rom(&mut self, path: &Path) -> Result<(), String> {
595        let path_str = path.to_string_lossy().to_string();
596        self.emu.load_rom(&path_str);
597        Ok(())
598    }
599
600    /// Power on the emulator
601    pub fn power_on(&mut self) { self.emu.power(); }
602
603    /// Power off the emulator
604    pub fn power_off(&mut self) { self.emu.power_off(); }
605
606    /// Reset the emulator
607    pub fn reset(&mut self) { self.emu.reset(); }
608
609    /// Load savestate based on configuration
610    pub fn load_savestate(&mut self) -> Result<(), String> {
611        if let Some(ref source) = self.savestate_config.load_from {
612            let state = match source {
613                SavestateSource::File(path) => {
614                    let data = std::fs::read(path).map_err(|e| {
615                        format!("Failed to read savestate from {}: {}", path.display(), e)
616                    })?;
617                    try_load_state_from_bytes(&data).ok_or_else(|| {
618                        format!("Failed to load savestate from {}", path.display())
619                    })?
620                }
621                SavestateSource::Stdin => {
622                    let mut buffer = Vec::new();
623                    std::io::stdin()
624                        .read_to_end(&mut buffer)
625                        .map_err(|e| format!("Failed to read savestate from stdin: {}", e))?;
626                    decode_savestate(&buffer)?
627                }
628                SavestateSource::Bytes(bytes) => decode_savestate(bytes)?,
629            };
630            self.emu.load_state(state);
631        }
632        Ok(())
633    }
634
635    /// Save savestate based on configuration
636    pub fn save_savestate(&self) -> Result<(), String> {
637        if let Some(ref dest) = self.savestate_config.save_to {
638            let state = self
639                .emu
640                .save_state()
641                .ok_or_else(|| "No ROM loaded, cannot save state".to_string())?;
642            let encoded = encode_savestate(&state, self.savestate_config.format)?;
643
644            match dest {
645                SavestateDestination::File(path) => {
646                    std::fs::write(path, &encoded).map_err(|e| {
647                        format!("Failed to write savestate to {}: {}", path.display(), e)
648                    })?;
649                }
650                SavestateDestination::Stdout => {
651                    std::io::stdout()
652                        .write_all(&encoded)
653                        .map_err(|e| format!("Failed to write savestate to stdout: {}", e))?;
654                }
655            }
656        }
657        Ok(())
658    }
659
660    /// Run execution until a stop condition is met
661    pub fn run(&mut self) -> Result<ExecutionResult, String> {
662        // Set up trace if configured
663        if self.config.trace_path.is_some() {
664            self.emu.enable_trace();
665        }
666
667        let max_cycles = self.config.max_cycles();
668        let start_cycles = self.emu.total_cycles;
669
670        // Run frame by frame for stop condition checking
671        let result = loop {
672            // Run one frame
673            match self.emu.step_frame() {
674                Ok(_) => {}
675                Err(e) => {
676                    break ExecutionResult {
677                        stop_reason: StopReason::Error(e),
678                        total_cycles: self.emu.total_cycles - start_cycles,
679                        total_frames: self.frame_count,
680                    };
681                }
682            }
683
684            // Only collect frames if in buffered mode
685            if self.collect_frames {
686                self.frames.push(self.emu.get_pixel_buffer());
687            }
688
689            self.frame_count += 1;
690            let cycles_run = self.emu.total_cycles - start_cycles;
691
692            // Check stop conditions
693            if let Some(reason) =
694                self.config
695                    .check_conditions(&self.emu, cycles_run, self.frame_count)
696            {
697                break ExecutionResult {
698                    stop_reason: reason,
699                    total_cycles: cycles_run,
700                    total_frames: self.frame_count,
701                };
702            }
703
704            // Check max cycles
705            if self.emu.total_cycles >= max_cycles {
706                break ExecutionResult {
707                    stop_reason: StopReason::Completed,
708                    total_cycles: cycles_run,
709                    total_frames: self.frame_count,
710                };
711            }
712        };
713
714        // Write trace log to file if configured
715        self.write_trace_log()?;
716
717        Ok(result)
718    }
719
720    /// Run execution with streaming video export.
721    ///
722    /// This mode writes frames directly to the video encoder as they are generated,
723    /// instead of buffering all frames in memory. This significantly reduces memory
724    /// usage for long recordings.
725    ///
726    /// # Arguments
727    ///
728    /// * `encoder` - A streaming video encoder that will receive frames as they're generated
729    ///
730    /// # Performance
731    ///
732    /// - Uses parallel upscaling via rayon (if encoder has upscaling enabled)
733    /// - O(1) memory usage per frame instead of O(n) for all frames
734    /// - Frames are written immediately, reducing peak memory usage
735    ///
736    /// # FPS Multipliers
737    ///
738    /// When the encoder's FPS config specifies a multiplier > 1 (e.g., 2x, 3x),
739    /// this method captures frames at sub-frame intervals. For example:
740    /// - 2x: Captures at mid-frame and end of frame (2 captures per PPU frame)
741    /// - 3x: Captures at 1/3, 2/3, and end of frame (3 captures per PPU frame)
742    ///
743    /// This produces true intermediate states showing partial rendering progress.
744    pub fn run_with_video_encoder(
745        &mut self,
746        encoder: &mut super::video::StreamingVideoEncoder,
747        renderer: &mut Box<dyn monsoon_core::emulation::screen_renderer::ScreenRenderer>,
748    ) -> Result<ExecutionResult, String> {
749        // Disable frame collection for streaming mode
750        self.collect_frames = false;
751
752        // Set up trace if configured
753        if self.config.trace_path.is_some() {
754            self.emu.enable_trace();
755        }
756
757        let max_cycles = self.config.max_cycles();
758        let start_cycles = self.emu.total_cycles;
759
760        // Get the number of captures per PPU frame from the encoder's FPS config
761        let captures_per_frame = encoder.captures_per_frame();
762
763        // Run frame by frame for stop condition checking
764        loop {
765            // Track the start of this PPU frame to calculate capture targets
766            // This avoids accumulated rounding errors from integer division
767            let frame_start_cycles = self.emu.total_cycles;
768
769            // Run partial frames based on FPS multiplier and capture at each interval
770            for capture_idx in 0..captures_per_frame {
771                // Calculate target cycle for this capture relative to frame start
772                // Using (capture_idx + 1) * MASTER_CYCLES_PER_FRAME / captures_per_frame
773                // ensures the final capture always aligns with the frame boundary
774                let odd_frame_offset = if self.emu.is_even_frame() && self.emu.is_rendering() {
775                    2
776                } else {
777                    -2
778                };
779
780                let base = (capture_idx + 1) as u128 * MASTER_CYCLES_PER_FRAME as u128;
781
782                let base = if odd_frame_offset >= 0 {
783                    base.saturating_add(odd_frame_offset as u128)
784                } else {
785                    base.saturating_sub((-odd_frame_offset) as u128)
786                };
787
788                let capture_point = base / captures_per_frame as u128;
789                let target_cycles = frame_start_cycles + capture_point;
790
791                // Run until the target cycle
792                match self.emu.run_until(target_cycles, RunOptions::default()) {
793                    Ok(_) => {}
794                    Err(e) => {
795                        return Ok(ExecutionResult {
796                            stop_reason: StopReason::Error(e),
797                            total_cycles: self.emu.total_cycles - start_cycles,
798                            total_frames: self.frame_count,
799                        });
800                    }
801                }
802
803                // Write frame directly to encoder (with upscaling if configured)
804                // This captures the current pixel buffer state, which may be mid-render
805                let frame = self.emu.get_pixel_buffer();
806                let rgb_frame = renderer.buffer_to_image(&frame);
807                encoder
808                    .write_frame(rgb_frame)
809                    .map_err(|e| format!("Video encoding error: {}", e))?;
810
811                // Only increment frame_count at the end of a full PPU frame
812                // (when we've done all captures for this frame)
813                if capture_idx == captures_per_frame - 1 {
814                    self.frame_count += 1;
815                }
816            }
817
818            let cycles_run = self.emu.total_cycles - start_cycles;
819
820            // Check stop conditions
821            if let Some(reason) =
822                self.config
823                    .check_conditions(&self.emu, cycles_run, self.frame_count)
824            {
825                self.write_trace_log()?;
826                return Ok(ExecutionResult {
827                    stop_reason: reason,
828                    total_cycles: cycles_run,
829                    total_frames: self.frame_count,
830                });
831            }
832
833            // Check max cycles
834            if self.emu.total_cycles >= max_cycles {
835                self.write_trace_log()?;
836                return Ok(ExecutionResult {
837                    stop_reason: StopReason::Completed,
838                    total_cycles: cycles_run,
839                    total_frames: self.frame_count,
840                });
841            }
842        }
843    }
844
845    /// Enable or disable frame collection.
846    ///
847    /// When disabled, frames are not stored in memory during execution.
848    /// Use this for streaming mode or when you don't need frame data.
849    pub fn set_collect_frames(&mut self, collect: bool) { self.collect_frames = collect; }
850
851    /// Get reference to the emulator
852    pub fn emulator(&self) -> &Nes { &self.emu }
853
854    /// Get mutable reference to the emulator
855    pub fn emulator_mut(&mut self) -> &mut Nes { &mut self.emu }
856
857    /// Write trace log to the configured file path, if tracing was enabled.
858    fn write_trace_log(&self) -> Result<(), String> {
859        if let Some(ref path) = self.config.trace_path
860            && let Some(trace) = self.emu.trace_log()
861        {
862            std::fs::write(path, &trace.log)
863                .map_err(|e| format!("Failed to write trace log to {}: {}", path.display(), e))?;
864        }
865        Ok(())
866    }
867}
868
869impl Default for ExecutionEngine {
870    fn default() -> Self { Self::new() }
871}
872
873// =============================================================================
874// Helper Functions
875// =============================================================================
876
877/// Decode a savestate from bytes (auto-detects format).
878///
879/// Detection strategy: Try JSON first, then binary as fallback.
880/// This is more robust than checking for `{` which could fail with
881/// whitespace-prefixed JSON or misidentify binary data.
882fn decode_savestate(bytes: &[u8]) -> Result<SaveState, String> {
883    try_load_state_from_bytes(bytes)
884        .ok_or_else(|| "Failed to decode savestate (tried all supported formats)".to_string())
885}
886
887/// Encode a savestate to bytes in the specified format
888fn encode_savestate(state: &SaveState, format: SavestateFormat) -> Result<Vec<u8>, String> {
889    match format {
890        SavestateFormat::Binary => Ok(state.to_bytes(None)),
891        SavestateFormat::Json => Ok(state.to_bytes(Some("json".to_string()))),
892    }
893}
894
895// =============================================================================
896// Builder from CLI Args
897// =============================================================================
898
899impl ExecutionConfig {
900    /// Build execution config from CLI arguments
901    pub fn from_cli_args(args: &CliArgs) -> Self {
902        let mut config = Self::new();
903
904        // Add cycle/frame stop conditions
905        if let Some(cycles) = args.execution.cycles {
906            config.stop_conditions.push(StopCondition::Cycles(cycles));
907        }
908        if let Some(frames) = args.execution.frames {
909            config.stop_conditions.push(StopCondition::Frames(frames));
910        }
911
912        // Add opcode stop condition
913        if let Some(op) = args.execution.until_opcode {
914            config.stop_conditions.push(StopCondition::Opcode(op));
915        }
916
917        // Add memory condition
918        if let Some(ref mem_cond) = args.execution.until_mem
919            && let Ok(cond) = StopCondition::parse_memory_condition(mem_cond)
920        {
921            config.stop_conditions.extend(cond);
922        }
923
924        // Add memory watchpoints
925        if !args.execution.watch_mem.is_empty()
926            && let Ok(watches) = StopCondition::parse_memory_watches(&args.execution.watch_mem)
927        {
928            config.stop_conditions.extend(watches);
929        }
930
931        // Add HLT stop
932        if args.execution.until_hlt {
933            config.stop_on_halt = true;
934        }
935
936        // Add breakpoints (these are now the only way to stop at a PC address)
937        for bp in &args.execution.breakpoint {
938            config.stop_conditions.push(StopCondition::PcEquals(*bp));
939        }
940
941        // Add trace
942        config.trace_path = args.execution.trace.clone();
943
944        // Set verbose
945        config.verbose = args.verbose;
946
947        // If no stop conditions, default to 60 frames (1 second)
948        if config.stop_conditions.is_empty() && !config.stop_on_halt {
949            config.stop_conditions.push(StopCondition::Frames(60));
950        }
951
952        config
953    }
954}
955
956impl SavestateConfig {
957    /// Build savestate config from CLI arguments
958    pub fn from_cli_args(args: &CliArgs) -> Self {
959        let mut config = Self::new();
960
961        // Load source
962        if args.savestate.state_stdin {
963            config.load_from = Some(SavestateSource::Stdin);
964        } else if let Some(ref path) = args.savestate.load_state {
965            config.load_from = Some(SavestateSource::File(path.clone()));
966        }
967
968        // Save destination
969        if args.savestate.state_stdout {
970            config.save_to = Some(SavestateDestination::Stdout);
971        } else if let Some(ref path) = args.savestate.save_state {
972            config.save_to = Some(SavestateDestination::File(path.clone()));
973        }
974
975        // Set format directly from CLI args (same type via re-export)
976        config.format = args.savestate.state_format;
977
978        config
979    }
980}