diff --git a/docs/debugger/window.md b/docs/debugger/window.md index b646293..0cdc989 100644 --- a/docs/debugger/window.md +++ b/docs/debugger/window.md @@ -324,6 +324,10 @@ render reads a snapshot only -- it never perturbs the emulation. **Beam scrub** turns the underlay into a beam-time scrubber: only what the CRT had drawn by the time the beam reached the selected slot shows at normal underlay brightness; everything past it ghosts at low brightness. +(Enabling it with the selection still at or before the display window's +top-left corner -- where nothing has been drawn yet -- would ghost the +whole picture, so the selection snaps to the end of the frame instead +and scrubbing backward peels the picture away.) Because Copperline's renderer replays register writes at their recorded beam positions, every pixel before the cursor is exactly what the display carried at that instant -- drag the selection (or hold a cursor key) and diff --git a/src/video/window.rs b/src/video/window.rs index 7de6027..80eacb0 100644 --- a/src/video/window.rs +++ b/src/video/window.rs @@ -3528,8 +3528,38 @@ impl App { } fn frame_analyzer_toggle_scrub(&mut self) { + // Snap inputs for enabling scrub: the traced frame's last slot and + // the frame-start DIW top-left corner in (vpos, cck) beam units + // (same decode as build_frame_analyzer_view's DIW overlay). + let trace_end = self.emu.bus().frame_bus_trace().map(|trace| { + ( + trace.rows.saturating_sub(1).min(u16::MAX as usize) as u16, + trace.cols.saturating_sub(1).min(u16::MAX as usize) as u16, + ) + }); + let base = self.emu.bus().frame_render_base(); + let diw_top_left = (!(base.diwstrt == 0 && base.diwstop == 0)).then(|| { + ( + base.diwhigh.v_start(base.diwstrt), + base.diwhigh.h_start(base.diwstrt) / 2, + ) + }); if let Some(panel) = self.frame_analyzer_panel.as_mut() { panel.show_scrub = !panel.show_scrub; + // Enabling scrub with the selection at or before the display + // window's top-left corner would ghost the whole picture (the + // CRT has drawn none of it at that beam position), which reads + // as the underlay switching off. Snap the selection to the end + // of the traced frame instead: the picture starts fully drawn + // and scrubbing backward peels it away. + if panel.show_scrub { + if let (Some((end_v, end_h)), Some(diw)) = (trace_end, diw_top_left) { + if (panel.selected_vpos, panel.selected_hpos) <= diw { + panel.selected_vpos = end_v; + panel.selected_hpos = end_h; + } + } + } self.request_redraw(); } } diff --git a/src/video/window/tests.rs b/src/video/window/tests.rs index 8ad758b..4fa3d3d 100644 --- a/src/video/window/tests.rs +++ b/src/video/window/tests.rs @@ -2516,6 +2516,67 @@ fn frame_analyzer_underlay_toggles_and_renders() { assert!(app.analyzer_underlay_input.is_none()); } +#[test] +fn frame_analyzer_scrub_enable_snaps_predisplay_selection_to_frame_end() { + let mut app = test_app(); + app.open_frame_analyzer(); + + // Program a standard PAL display window. The analyzer reads the + // frame-start register snapshot, so run two frames: the first ends + // with a pre-write snapshot, the second starts after the writes. + { + let bus = app.emu.bus_mut(); + bus.custom_write(0x08E, 2, 0x2C81); // DIWSTRT + bus.custom_write(0x090, 2, 0x2CC1); // DIWSTOP + } + app.frame_analyzer_step_frame(); + app.frame_analyzer_step_frame(); + let (max_vpos, max_hpos) = app + .emu + .bus() + .frame_bus_trace() + .map(|trace| (trace.rows as u16 - 1, trace.cols as u16 - 1)) + .expect("frame trace armed"); + + // A fresh panel's selection sits at the DIW top-left corner, where the + // CRT has drawn nothing: enabling scrub there would ghost the whole + // picture, so the selection snaps to the end of the traced frame. + let panel = app.frame_analyzer_panel.as_ref().unwrap(); + assert_eq!((panel.selected_vpos, panel.selected_hpos), (0x2C, 0x28)); + app.activate_ui_control(UiControl::AnalyzerScrub); + let panel = app.frame_analyzer_panel.as_ref().unwrap(); + assert!(panel.show_scrub); + assert_eq!( + (panel.selected_vpos, panel.selected_hpos), + (max_vpos, max_hpos) + ); + + // A selection inside the display window is a deliberate scrub point + // and survives re-enabling scrub. + app.activate_ui_control(UiControl::AnalyzerScrub); + if let Some(panel) = app.frame_analyzer_panel.as_mut() { + panel.selected_vpos = 100; + panel.selected_hpos = 0x80; + } + app.activate_ui_control(UiControl::AnalyzerScrub); + let panel = app.frame_analyzer_panel.as_ref().unwrap(); + assert!(panel.show_scrub); + assert_eq!((panel.selected_vpos, panel.selected_hpos), (100, 0x80)); +} + +#[test] +fn frame_analyzer_scrub_enable_without_display_window_keeps_selection() { + // No DIWSTRT/DIWSTOP programmed: there is no picture to reveal, so + // enabling scrub leaves the selection alone. + let mut app = test_app(); + app.open_frame_analyzer(); + app.frame_analyzer_step_frame(); + app.activate_ui_control(UiControl::AnalyzerScrub); + let panel = app.frame_analyzer_panel.as_ref().unwrap(); + assert!(panel.show_scrub); + assert_eq!((panel.selected_vpos, panel.selected_hpos), (0x2C, 0x28)); +} + /// Type a command into the open console and return the lines it printed. fn console_run(app: &mut super::App, cmd: &str) -> Vec { if let Some(panel) = app.console_panel.as_mut() {