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