Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/debugger/window.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions src/video/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Expand Down
61 changes: 61 additions & 0 deletions src/video/window/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
if let Some(panel) = app.console_panel.as_mut() {
Expand Down
Loading