Replace the 7px filled circles with 5px filled squares (13x5 strip, same 3px gap and corner anchoring) — they read better at this size. The plan doc is updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012KgAw4A42H9mgKGWABTJoj
6.9 KiB
Disk Activity LEDs — Implementation Plan
A working document for adding disk activity "LEDs" to the two frontend:
two small squares in the lower-right corner of the screen, one per Disk II
drive. Modeled on WOZ1.md / APPLE_IIE_ENHANCED.md: small phases, each
independently verifiable. The tree must build and pass all verification
gates (cargo fmt --check, cargo clippy --all-targets -- -D warnings,
cargo test) after every phase.
Branch:
claude/disk-activity-leds-fodk1w. This is an S-sized feature; all phases land as a single PR intomaster.
What we are building
When there is disk activity, two small filled squares appear in the lower-right corner of the emulated screen — one for drive 1, one for drive 2:
- Red — the drive is active.
- Grey — the drive is idle (but the other one is active).
- Hidden — when no drive is active, the squares are not drawn at all.
Semantics: what "active" means
The Disk II controller shares a single motor line between its two drives,
and only the selected drive spins. Dsk already models this and exposes
exactly the right signals, today used only by the status bar's [1][2]
text lights (render_status_bar in two.rs):
Dsk::motor_lit(cycles)— true while the platter turns, including the ~1 second spin-down after$C0E8(MOTOR_OFF_DELAY). This matches the real Disk II "IN USE" lamp, which is wired to drive-enable and stays lit through the spin-down.Dsk::active_drive()— the selected drive (0 or 1).
So the mapping is:
| State | Drive 1 LED | Drive 2 LED |
|---|---|---|
| Motor off | hidden | hidden |
| Motor on/spinning down, drive 1 selected | red | grey |
| Motor on/spinning down, drive 2 selected | grey | red |
No new state or IO-access tracking is needed in dsk.rs — the motor is the
activity signal, exactly like the original hardware's lamp. (An alternative
— stamping the cycle of every $C0EC/$C0ED access and lighting the LED for
N ms after — was considered and rejected: it flickers, needs new state, and
is less faithful than the motor line.)
The slot 7 hard drive (hdd.rs) is out of scope; a third LED for it is a
natural follow-up (it would need a last-access cycle stamp since it has no
motor concept).
Rendering approach
An SDL-side overlay texture, exactly like the scanline overlay and the
command palette — scr.rs and the shared pixels buffer are never
touched, so the golden-BMP tests and the hidden --screenshot flag remain
byte-for-byte unchanged, and the Apple 1 (one) frontend is unaffected.
-
A new pure module
ewm/src/led.rsrenders the LED strip into a smallVec<u32>pixel buffer with a transparent background (alpha 0), so it is unit-testable headless, following the house pattern (pure renderer + frame-loop upload). -
Geometry, in emulated (1x) pixels, drawn at 3x by the frame loop like everything else so the LEDs look chunky-pixel consistent with the screen:
- square side 5 px, gap 3 px → strip buffer 13×5 — plain filled squares, no anti-aliasing, matching the crisp nearest-neighbor look. (v1 drew 7 px circles; squares read better at this size.)
-
Colors, packed via the existing
PixelLayout::pack:- active: red
(255, 0, 0, 255)— same red the status bar uses; - idle: grey
(128, 128, 128, 255).
- active: red
-
The texture is created once (
BlendMode::Blend,ScaleMode::Nearest, 13×5), updated per frame while lit (it is tiny — same policy as the status bar texture), and copied to a dst rect anchored to the lower-right corner ofscreen_dstwith a 4-logical-px margin:x = pad + (SCR_WIDTH*3) - margin*3 - 13*3 y = pad + (SCR_HEIGHT*3) - margin*3 - 5*3 w = 13*3, h = 5*3 -
Draw order: after the screen + scanline copy and the status bar, before the paused dim (so the LEDs dim with the screen when paused) and before the palette (so the palette stays on top).
-
The overlay is independent of the status bar toggle (
I); the[1][2]status-bar lights stay as they are.
Phases
Phase 1 — Dsk::drive_lit helper (S)
Add a small query to dsk.rs so the frontend asks one question per drive:
/// Whether drive `index`'s activity light is lit: the motor is running
/// (spin-down included) and this drive is the selected one.
pub fn drive_lit(&self, index: usize, cycles: u64) -> bool {
self.motor_lit(cycles) && self.drive == index
}
Rewire the two motor_lit(...) && active_drive() == n sites in
render_status_bar to use it.
Gate: unit tests in dsk.rs: motor off → neither lit; motor on →
selected drive lit, other not; after $C0E8 both the spin-down window
(lit) and expiry (unlit) via the cycles argument; $C0EA/$C0EB select
switches move the light. Existing tests stay green.
Phase 2 — pure LED strip renderer (S)
New ewm/src/led.rs:
pub const LED_STRIP_WIDTH: usize = 13;
pub const LED_STRIP_HEIGHT: usize = 5;
/// Render the two-drive LED strip: a filled square per drive, red when
/// lit, grey when not, on a transparent background. The caller only
/// renders the strip at all when at least one drive is lit.
pub fn render_led_strip(lit: [bool; 2], layout: PixelLayout) -> Vec<u32>
Gate: unit tests: buffer is 13*5; the gap between the squares has
alpha 0; every pixel of each square is red/grey per lit; both
PixelLayout packings sanity-checked.
Phase 3 — frontend wiring (S)
In two::main:
-
Create
led_texture(streaming, 13×5,BlendMode::Blend,ScaleMode::Nearest) next to the other textures. -
In the render block, after the status bar copy:
let lit = [ two.dsk().drive_lit(0, two.cpu.counter), two.dsk().drive_lit(1, two.cpu.counter), ]; if lit[0] || lit[1] { // update led_texture from render_led_strip(lit, layout) // and copy to the lower-right dst rect }
Gate (manual): boot the DOS 3.3 System Master — drive 1's LED shows red
(drive 2 grey) during boot and both vanish ~1 second after the prompt
appears; CATALOG,D2 lights drive 2 red with drive 1 grey; the LEDs render
above the screen contents, dim under the pause overlay, sit below the
palette, and stay put in fullscreen (letterboxed logical coordinates).
--screenshot output and all golden BMPs are unchanged (cargo test).
Out of scope / future work
- Hard drive LED — a third square for the slot 7 HDD, driven by a last-access cycle stamp (no motor to observe). Trivial to add once the strip renderer exists (make the strip width a function of LED count).
- Palette toggle — a "Drive LEDs: on/off" command palette entry if the overlay ever bothers anyone; not needed for v1 since the overlay is invisible when drives are idle.
- Apple 1 (
one) — no disk hardware, nothing to do.