diff options
author | Ben Bridle <ben@derelict.engineering> | 2024-07-30 21:52:11 +1200 |
---|---|---|
committer | Ben Bridle <ben@derelict.engineering> | 2024-07-30 21:52:11 +1200 |
commit | 820666fbbb10e4f8a288c738fa97af402eea9c06 (patch) | |
tree | e15e453fa899a47dbc03b2946eba9933e3a2b3a5 | |
parent | e55907ff1c3067ed3326091a477698ffc8db0026 (diff) | |
download | bedrock-pc-820666fbbb10e4f8a288c738fa97af402eea9c06.zip |
Rework main loop timings
The program is vastly more efficient to run, consuming only 1.5% CPU
instead of 13% during testing on Windows 10. Input events are handled
four times as frequently now, which makes for smoother lines when
drawing in Cobalt.
-rw-r--r-- | src/emulator.rs | 81 |
1 files changed, 51 insertions, 30 deletions
diff --git a/src/emulator.rs b/src/emulator.rs index 47d4bd6..606704d 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -7,7 +7,8 @@ use std::cmp::{min, max}; use std::time::*; use std::thread::sleep; -const FRAME: Duration = Duration::from_micros(16666); +const FRAME: Duration = Duration::from_micros(4_000); +const RENDER_WAIT: Duration = Duration::from_micros(500_000); const LINE_HEIGHT: f64 = 20.0; macro_rules! u16 { ($u32:expr) => { $u32.try_into().unwrap_or(u16::MAX) }; } @@ -18,8 +19,9 @@ pub struct BedrockEmulator { initialising: bool, sleeping: bool, pixel_scale: u32, - process_mark: Instant, - frame_mark: Instant, + start_of_process: Instant, + end_of_process: Instant, + end_of_render: Instant, debug_mark: Instant, cycles_elapsed: usize, } @@ -36,8 +38,9 @@ impl BedrockEmulator { initialising: true, sleeping: false, pixel_scale: 3, - process_mark: Instant::now(), - frame_mark: Instant::now(), + start_of_process: Instant::now(), + end_of_process: Instant::now(), + end_of_render: Instant::now(), debug_mark: Instant::now(), cycles_elapsed: 0, } @@ -187,37 +190,55 @@ impl WindowController for BedrockEmulator { } fn on_process(&mut self) { - // The duration remaining until the next frame is due to be rendered - let frame_remaining = match self.frame_mark.elapsed() < FRAME { - true => FRAME.saturating_sub(self.frame_mark.elapsed()), - false => FRAME, - }; - // Wait for the initial resize event to come through + // Prevent program starting before the window receives an initial size. if self.initialising { - sleep(FRAME / 10); - self.process_mark = Instant::now(); + sleep(FRAME); return; } - // Sleep for the remainder of the frame, or until a timer expires - if self.sleeping && !self.vm.dev.can_wake() { - let sleep_duration = match self.vm.dev.clock.time_to_next_wake() { - Some(ms) => min(ms, frame_remaining), - None => frame_remaining, - }; - sleep(sleep_duration); - self.process_mark = Instant::now(); - return; + + if self.sleeping { + // Pause the processor until the current frame is rendered. This + // prevents the current frame from being overdrawn before rendering. + if self.vm.dev.screen.dirty { + sleep(FRAME); + self.end_of_process = Instant::now(); + return; + } + // Ensure a minimum delay of FRAME between the start of consecutive + // process frames when asleep, unless a timer has expired. + let frame_start = self.start_of_process + FRAME; + let time_to_frame_start = frame_start.duration_since(Instant::now()); + if !time_to_frame_start.is_zero() { + let time_to_sleep = match self.vm.dev.clock.time_to_next_wake() { + Some(ms) => min(ms, time_to_frame_start), + None => time_to_frame_start, + }; + sleep(time_to_sleep); + self.end_of_process = Instant::now(); + return; + } + // Stay asleep if there are no pending wake events. + if !self.vm.dev.can_wake() { + sleep(FRAME); + self.end_of_process = Instant::now(); + return; + } } - // Wake from sleep and evaluate the program for the remainder of the frame + + // Run the processor for the remainder of the frame. + self.start_of_process = Instant::now(); self.sleeping = false; - while self.process_mark.elapsed() < frame_remaining { + let frame_end = self.end_of_process + FRAME; + + while Instant::now() < frame_end { if let Some(signal) = self.vm.evaluate(1000) { match signal { Signal::Debug(var) => self.debug(var), Signal::Sleep => { self.sleeping = true; - let frame_elapsed = self.process_mark.elapsed(); - sleep(frame_remaining.saturating_sub(frame_elapsed)); + // Sleep for the remainer of the frame. + sleep(frame_end.duration_since(Instant::now())); + self.end_of_process = Instant::now(); break; }, Signal::Halt => { @@ -230,26 +251,26 @@ impl WindowController for BedrockEmulator { } self.vm.dev.stream.flush_local(); self.vm.dev.file.flush_entry(); - self.process_mark = Instant::now(); + self.end_of_process = Instant::now(); } fn render_request(&mut self) -> RenderRequest { if self.vm.dev.screen.dirty { match self.sleeping { true => RenderRequest::UPDATE, - false => match self.frame_mark.elapsed() >= 6 * FRAME { + false => match self.end_of_render.elapsed() >= RENDER_WAIT { true => RenderRequest::UPDATE, false => RenderRequest::NONE, } } } else { - self.frame_mark = Instant::now(); + self.end_of_render = Instant::now(); RenderRequest::NONE } } fn on_render(&mut self, buffer: &mut Buffer, _hint: RenderHint) { self.vm.dev.screen.render(buffer); - self.frame_mark = Instant::now(); + self.end_of_render = Instant::now(); } } |