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