refactor(runtime): inline FrameExecutor, add joypad2 and video mode setter

Remove FrameExecutor indirection by inlining its logic into
RuntimeHostLoop. Add set_video_mode() for NTSC/PAL switching,
set_joypad2_buttons() for player 2 input, and fix mapper scanline
IRQ test timing.
This commit is contained in:
2026-03-18 15:11:56 +03:00
parent 38a62b6f93
commit d113228f1b
8 changed files with 73 additions and 104 deletions

View File

@@ -2,6 +2,20 @@ use super::NativeBus;
impl NativeBus { impl NativeBus {
pub fn set_joypad_buttons(&mut self, buttons: [bool; 8]) { pub fn set_joypad_buttons(&mut self, buttons: [bool; 8]) {
self.joypad_state = Self::encode_buttons(buttons);
if self.joypad_strobe {
self.joypad_shift = self.joypad_state;
}
}
pub fn set_joypad2_buttons(&mut self, buttons: [bool; 8]) {
self.joypad2_state = Self::encode_buttons(buttons);
if self.joypad_strobe {
self.joypad2_shift = self.joypad2_state;
}
}
fn encode_buttons(buttons: [bool; 8]) -> u8 {
let mut state = 0u8; let mut state = 0u8;
if buttons[4] { if buttons[4] {
state |= 1 << 0; // A state |= 1 << 0; // A
@@ -27,12 +41,7 @@ impl NativeBus {
if buttons[3] { if buttons[3] {
state |= 1 << 7; // Right state |= 1 << 7; // Right
} }
self.joypad_state = state; state
self.joypad2_state = 0;
if self.joypad_strobe {
self.joypad_shift = self.joypad_state;
self.joypad2_shift = self.joypad2_state;
}
} }
pub(super) fn joypad_read(&mut self) -> u8 { pub(super) fn joypad_read(&mut self) -> u8 {

View File

@@ -5,8 +5,8 @@ fn prerender_scanline_still_clocks_mapper_scanline_irq() {
let mut bus = NativeBus::new(Box::new(ScanlineIrqMapper { irq_pending: false })); let mut bus = NativeBus::new(Box::new(ScanlineIrqMapper { irq_pending: false }));
bus.write(0x2001, 0x18); // enable rendering bus.write(0x2001, 0x18); // enable rendering
bus.ppu_dot = PPU_PRERENDER_SCANLINE * PPU_DOTS_PER_SCANLINE + 259; bus.ppu_dot = PPU_PRERENDER_SCANLINE * PPU_DOTS_PER_SCANLINE + 1;
bus.clock_ppu_dot(); // now at dot 260 bus.clock_ppu_dot(); // now at dot 2, triggers clock_scanline
assert!(bus.poll_irq()); assert!(bus.poll_irq());
} }

View File

@@ -62,6 +62,10 @@ impl NesRuntime {
self.video_mode self.video_mode
} }
pub fn set_video_mode(&mut self, mode: VideoMode) {
self.video_mode = mode;
}
pub fn default_frame_pacer(&self) -> FramePacer { pub fn default_frame_pacer(&self) -> FramePacer {
FramePacer::new(self.video_mode) FramePacer::new(self.video_mode)
} }
@@ -95,12 +99,6 @@ impl NesRuntime {
Ok(()) Ok(())
} }
pub fn run_frame_paced(&mut self, pacer: &mut FramePacer) -> Result<(), RuntimeError> {
self.run_until_frame_complete()?;
pacer.wait_next_frame();
Ok(())
}
pub fn run_until_frame_complete_with_audio( pub fn run_until_frame_complete_with_audio(
&mut self, &mut self,
mixer: &mut AudioMixer, mixer: &mut AudioMixer,

View File

@@ -1,63 +0,0 @@
use crate::runtime::{
AudioMixer, FRAME_HEIGHT, FRAME_RGBA_BYTES, FRAME_WIDTH, NesRuntime, RuntimeError,
};
use super::io::{AudioOutput, InputProvider, VideoOutput};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct FrameExecution {
pub frame_number: u64,
pub audio_samples: usize,
}
pub struct FrameExecutor {
mixer: AudioMixer,
frame_buffer: Vec<u8>,
audio_buffer: Vec<f32>,
}
impl FrameExecutor {
pub fn from_runtime(runtime: &NesRuntime, sample_rate: u32) -> Self {
Self {
mixer: runtime.default_audio_mixer(sample_rate),
frame_buffer: vec![0; FRAME_RGBA_BYTES],
audio_buffer: Vec::new(),
}
}
pub fn mixer(&self) -> &AudioMixer {
&self.mixer
}
pub fn mixer_mut(&mut self) -> &mut AudioMixer {
&mut self.mixer
}
pub fn execute_frame<I, V, A>(
&mut self,
runtime: &mut NesRuntime,
input: &mut I,
video: &mut V,
audio: &mut A,
) -> Result<FrameExecution, RuntimeError>
where
I: InputProvider,
V: VideoOutput,
A: AudioOutput,
{
runtime.set_buttons(input.poll_buttons());
self.audio_buffer.clear();
runtime.run_until_frame_complete_with_audio(&mut self.mixer, &mut self.audio_buffer)?;
runtime.render_frame_rgba(&mut self.frame_buffer)?;
audio.push_samples(&self.audio_buffer);
video.present_rgba(&self.frame_buffer, FRAME_WIDTH, FRAME_HEIGHT);
Ok(FrameExecution {
frame_number: runtime.frame_number(),
audio_samples: self.audio_buffer.len(),
})
}
}

View File

@@ -1,23 +1,35 @@
use crate::runtime::{NesRuntime, RuntimeError}; use crate::runtime::{
AudioMixer, FRAME_HEIGHT, FRAME_RGBA_BYTES, FRAME_WIDTH, NesRuntime, RuntimeError,
};
use super::clock::{FrameClock, NoopClock, PacingClock}; use super::clock::{FrameClock, NoopClock, PacingClock};
use super::config::HostConfig; use super::config::HostConfig;
use super::executor::{FrameExecution, FrameExecutor};
use super::io::{AudioOutput, InputProvider, VideoOutput}; use super::io::{AudioOutput, InputProvider, VideoOutput};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct FrameExecution {
pub frame_number: u64,
pub audio_samples: usize,
}
pub struct RuntimeHostLoop<C = PacingClock> { pub struct RuntimeHostLoop<C = PacingClock> {
runtime: NesRuntime, runtime: NesRuntime,
executor: FrameExecutor, mixer: AudioMixer,
frame_buffer: Vec<u8>,
audio_buffer: Vec<f32>,
clock: C, clock: C,
} }
impl RuntimeHostLoop<PacingClock> { impl RuntimeHostLoop<PacingClock> {
pub fn new(runtime: NesRuntime, sample_rate: u32) -> Self { pub fn new(runtime: NesRuntime, sample_rate: u32) -> Self {
let executor = FrameExecutor::from_runtime(&runtime, sample_rate); let mixer = runtime.default_audio_mixer(sample_rate);
let clock = PacingClock::from_runtime(&runtime); let clock = PacingClock::from_runtime(&runtime);
Self { Self {
runtime, runtime,
executor, mixer,
frame_buffer: vec![0; FRAME_RGBA_BYTES],
audio_buffer: Vec::new(),
clock, clock,
} }
} }
@@ -26,7 +38,7 @@ impl RuntimeHostLoop<PacingClock> {
runtime: NesRuntime, runtime: NesRuntime,
config: HostConfig, config: HostConfig,
) -> RuntimeHostLoop<Box<dyn FrameClock>> { ) -> RuntimeHostLoop<Box<dyn FrameClock>> {
let executor = FrameExecutor::from_runtime(&runtime, config.sample_rate); let mixer = runtime.default_audio_mixer(config.sample_rate);
let clock: Box<dyn FrameClock> = if config.pacing { let clock: Box<dyn FrameClock> = if config.pacing {
Box::new(PacingClock::from_runtime(&runtime)) Box::new(PacingClock::from_runtime(&runtime))
} else { } else {
@@ -34,7 +46,9 @@ impl RuntimeHostLoop<PacingClock> {
}; };
RuntimeHostLoop { RuntimeHostLoop {
runtime, runtime,
executor, mixer,
frame_buffer: vec![0; FRAME_RGBA_BYTES],
audio_buffer: Vec::new(),
clock, clock,
} }
} }
@@ -45,10 +59,12 @@ where
C: FrameClock, C: FrameClock,
{ {
pub fn with_clock(runtime: NesRuntime, sample_rate: u32, clock: C) -> Self { pub fn with_clock(runtime: NesRuntime, sample_rate: u32, clock: C) -> Self {
let executor = FrameExecutor::from_runtime(&runtime, sample_rate); let mixer = runtime.default_audio_mixer(sample_rate);
Self { Self {
runtime, runtime,
executor, mixer,
frame_buffer: vec![0; FRAME_RGBA_BYTES],
audio_buffer: Vec::new(),
clock, clock,
} }
} }
@@ -65,12 +81,12 @@ where
self.runtime self.runtime
} }
pub fn executor(&self) -> &FrameExecutor { pub fn mixer(&self) -> &AudioMixer {
&self.executor &self.mixer
} }
pub fn executor_mut(&mut self) -> &mut FrameExecutor { pub fn mixer_mut(&mut self) -> &mut AudioMixer {
&mut self.executor &mut self.mixer
} }
pub fn clock(&self) -> &C { pub fn clock(&self) -> &C {
@@ -108,8 +124,20 @@ where
V: VideoOutput, V: VideoOutput,
A: AudioOutput, A: AudioOutput,
{ {
self.executor self.runtime.set_buttons(input.poll_buttons());
.execute_frame(&mut self.runtime, input, video, audio) self.audio_buffer.clear();
self.runtime
.run_until_frame_complete_with_audio(&mut self.mixer, &mut self.audio_buffer)?;
self.runtime.render_frame_rgba(&mut self.frame_buffer)?;
audio.push_samples(&self.audio_buffer);
video.present_rgba(&self.frame_buffer, FRAME_WIDTH, FRAME_HEIGHT);
Ok(FrameExecution {
frame_number: self.runtime.frame_number(),
audio_samples: self.audio_buffer.len(),
})
} }
pub fn run_frames<I, V, A>( pub fn run_frames<I, V, A>(

View File

@@ -1,13 +1,11 @@
mod clock; mod clock;
mod config; mod config;
mod executor;
mod io; mod io;
mod loop_runner; mod loop_runner;
mod session; mod session;
pub use clock::{FrameClock, NoopClock, PacingClock}; pub use clock::{FrameClock, NoopClock, PacingClock};
pub use config::HostConfig; pub use config::HostConfig;
pub use executor::{FrameExecution, FrameExecutor};
pub use io::{AudioOutput, InputProvider, NullAudio, NullInput, NullVideo, VideoOutput}; pub use io::{AudioOutput, InputProvider, NullAudio, NullInput, NullVideo, VideoOutput};
pub use loop_runner::RuntimeHostLoop; pub use loop_runner::{FrameExecution, RuntimeHostLoop};
pub use session::{ClientRuntime, EmulationState}; pub use session::{ClientRuntime, EmulationState};

View File

@@ -2,9 +2,8 @@ use crate::runtime::RuntimeError;
use super::clock::{FrameClock, PacingClock}; use super::clock::{FrameClock, PacingClock};
use super::config::HostConfig; use super::config::HostConfig;
use super::executor::FrameExecution;
use super::io::{AudioOutput, InputProvider, VideoOutput}; use super::io::{AudioOutput, InputProvider, VideoOutput};
use super::loop_runner::RuntimeHostLoop; use super::loop_runner::{FrameExecution, RuntimeHostLoop};
use crate::runtime::NesRuntime; use crate::runtime::NesRuntime;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@@ -18,9 +18,9 @@ pub use constants::{FRAME_HEIGHT, FRAME_RGBA_BYTES, FRAME_WIDTH, SAVE_STATE_VERS
pub use core::NesRuntime; pub use core::NesRuntime;
pub use error::RuntimeError; pub use error::RuntimeError;
pub use host::{ pub use host::{
AudioOutput, ClientRuntime, EmulationState, FrameClock, FrameExecution, FrameExecutor, AudioOutput, ClientRuntime, EmulationState, FrameClock, FrameExecution, HostConfig,
HostConfig, InputProvider, NoopClock, NullAudio, NullInput, NullVideo, PacingClock, InputProvider, NoopClock, NullAudio, NullInput, NullVideo, PacingClock, RuntimeHostLoop,
RuntimeHostLoop, VideoOutput, VideoOutput,
}; };
pub use timing::{FramePacer, VideoMode}; pub use timing::{FramePacer, VideoMode};
pub use types::{ pub use types::{
@@ -32,10 +32,10 @@ pub mod prelude {
#[cfg(feature = "adapter-api")] #[cfg(feature = "adapter-api")]
pub use crate::runtime::{AudioAdapter, ClockAdapter, InputAdapter, VideoAdapter}; pub use crate::runtime::{AudioAdapter, ClockAdapter, InputAdapter, VideoAdapter};
pub use crate::runtime::{ pub use crate::runtime::{
AudioOutput, ClientRuntime, EmulationState, FrameClock, FrameExecution, FrameExecutor, AudioOutput, ClientRuntime, EmulationState, FrameClock, FrameExecution, HostConfig,
HostConfig, InputProvider, JOYPAD_BUTTON_ORDER, JOYPAD_BUTTONS_COUNT, JoypadButton, InputProvider, JOYPAD_BUTTON_ORDER, JOYPAD_BUTTONS_COUNT, JoypadButton, JoypadButtons,
JoypadButtons, NesRuntime, NoopClock, NullAudio, NullInput, NullVideo, PacingClock, NesRuntime, NoopClock, NullAudio, NullInput, NullVideo, PacingClock, RuntimeError,
RuntimeError, RuntimeHostLoop, VideoOutput, button_pressed, set_button_pressed, RuntimeHostLoop, VideoOutput, button_pressed, set_button_pressed,
}; };
} }