# Scanline Rotation Transform — Engineering Spec
## 0. Project Context: Cloning the 1986 Radius FPD (64/68Hz Board)
This project clones the original 1986 Radius Full Page Display card for the Macintosh SE — specifically the first-generation **64/68Hz board**, not the incompatible second-generation 1987 75Hz board (different signal timing, not interchangeable).
**Confirmed specs:**
- Resolution: **640 wide × 864 high**
- Color depth: **1 bit per pixel** (monochrome, black/white)
Because this is a clone of real vintage hardware (not a new design driven purely by a written spec), the project has an added prerequisite phase: **capturing and characterizing the actual analog signal timing from a real 64/68Hz card**, since the sync/blanking/porch timings are a product of 1986-era hardware design choices, not a standard later-era VESA mode. Guessing at "similar" timings risks producing a signal the original FPD monitor (or a monitor emulating it) won't lock onto.
### Phase 0 — Signal capture & characterization (new, precedes Phase 1)
**Decision: single fixed timing mode.** The clone targets **Crystal 1 (55.000 MHz)** only:
- Horizontal scan rate: 68.75 kHz
- Vertical refresh: 67.139 Hz
- Total pixels/line: 800 (640 active + 160 blanking)
- Total lines/frame: ~1024 (864 active + ~160 blanking)
This crystal is selected because its nominal rates resolve cleanly to round binary-friendly totals, consistent with straightforward TTL counter-based sync generation — the most likely candidate for the board's actual native/primary mode. Crystal 2 (dual-mode support) is out of scope for this clone.
**Action items (single-mode capture, simplified from prior dual-mode plan):**
- Capture real horizontal sync, vertical sync, and pixel clock timing from an actual 64/68Hz card operating in the 55.000 MHz / 68.75kHz / 67.139Hz mode, using a logic analyzer/oscilloscope
- Confirm exact horizontal blanking split (front porch / sync pulse / back porch, out of the 160-pixel blanking total) and vertical blanking split (out of the ~160-line total) — the 800×1024 totals are inferred from nominal rates, but the internal timing breakdown still requires direct measurement
- Document sync pulse widths and polarity (positive vs. negative sync)
- Deliverable: a single timing table (equivalent to one VESA mode entry) for this specific fixed mode, to drive the FPGA's sync generator
### Phase 0.5 — Physical tap point & interface identification (new)
Separate from signal *timing* characterization (Phase 0), this phase identifies *where* and *how* to physically divert the page buffer output from the original card to the FPGA, without disturbing the card's own display path.
**Hardware setup:**
- "Tested good" card: reference/baseline, kept intact and unmodified — used for continuity-verified schematic development and as a known-good comparison signal source
- Project-box spare card: used for invasive work (tapping, soldering, probing) since it's expendable
**Signals to identify at the display connector (TTL interface):**
- Serial pixel data (1-bit monochrome — likely single data line)
- Pixel clock (PCLK)
- Horizontal sync (HSYNC)
- Vertical sync (VSYNC)
- Possible blanking/display-enable line (BLANK/DE) — confirm whether this exists as a discrete signal or must be reconstructed from sync timing in the FPGA
**Key considerations:**
- **Logic levels:** confirm TTL voltage (likely 5V). If FPGA I/O is not 5V-tolerant, tapped signals need level-shifting (buffer IC recommended over a resistor divider, to avoid signal degradation)
- **Non-invasive tap:** use a high-impedance buffer (e.g., 74LVC245 or similar) to tap signals without loading the original circuit — needed since the original card should continue driving the FPD monitor unaffected, if that's still a project goal, while simultaneously feeding the FPGA
- **Tap point location:** identify the point after the page buffer's shift-register/serializer (where pixel data is already serial, sync'd to PCLK) rather than tapping earlier in the pipeline where data may still be in a different form
**Deliverable:** an annotated schematic (extending the continuity-tracing work already underway) identifying exact pin/trace locations for each signal, plus a level-shifting/buffering plan for the FPGA interface.
## 1. Problem Statement (updated)
The (cloned) video card outputs a portrait-orientation raster scan (row-major, top-to-bottom, left-to-right within each row) at 640×864, 1-bit monochrome, using the original 1986 Radius 64/68Hz board's native timing. The physical display panel is a landscape-native modern device (e.g., HDMI monitor), driven via the FPGA project board. A compensating 90° rotation must be applied between source and panel so the image displays right-side-up.
## 2. Confirmed Coordinate Transform
Verified against a worked 4×7 (W×H) example, pixel-by-pixel, against both terminal points (top-right and bottom-right source corners).
**Source (portrait):** width `W`, height `H`, pixel addressed `(x, y)`
- `x ∈ [0, W-1]` (column)
- `y ∈ [0, H-1]` (row)
- Scanned row-major, top to bottom
**Output (landscape buffer):** width `H`, height `W` (dimensions swap)
- Pixel addressed `(x', y')`
**Transform (rotation direction: clockwise):**
```
x' = (H - 1) - y
y' = x
```
### Worked example (W=4, H=7)
| Source (x,y) | Pixel # | Output (x',y') | Output # |
|---|---|---|---|
| (0,0) | p01 | (6,0) | x07 |
| (0,6) | p25 | (0,0) | x01 |
| (3,0) | p04 | (6,3) | x28 |
| (3,6) | p28 | (0,3) | x22 |
All four corners confirmed consistent with the formula above.
## 3. Buffering Requirement (Critical)
This is **not** a line-buffer-compatible transform. Output row `y'=0` requires the first pixel of *every* source row — i.e., the entire first source column — which isn't complete until the last source row has been received.
**Implication:** a full source frame must be buffered before any output row can be emitted.
- Minimum: 1 frame buffer (adds ~1 frame time of latency)
- Recommended: 2 frames (ping-pong / double buffer) to avoid tearing while the next frame is being written while the current one is being read out rotated
- Latency estimate: at 60 Hz, ~16.7 ms minimum (single buffer), ~33 ms typical (double buffer)
## 4. Generalization to Production Resolution
The formula is resolution-independent — substitute actual `H` (source height) for production use:
```
x' = (H_actual - 1) - y
y' = x
```
**Action item:** confirm actual production resolution and aspect ratio before implementation. The 4×7 example was a worked proof-of-concept only; do not hardcode `H=7` into production code.
## 5. Production Configuration: 640×864 Portrait → 1024×768 Landscape (Centered)
**Confirmed:** source is W=640 (wide) × H=864 (tall). Target panel is 1024×768, physically mounted rotated so the viewer sees a portrait image. Since the rotated source (864×640) is smaller than the panel's native landscape resolution (1024×768), it must be centered with a border.
### Step 1 — Rotate (CW)
Using the general formula from Section 4 with `H=864`:
```
x' = 863 - y
y' = x
```
Produces a rotated buffer of 864 (wide) × 640 (tall).
### Step 2 — Center in 1024×768
- Horizontal border: `(1024 - 864) / 2 = 80` px, left and right
- Vertical border: `(768 - 640) / 2 = 64` px, top and bottom
### Combined transform — source `(x, y)` → final panel address `(X, Y)`
```
X = 943 - y (= 863 - y + 80)
Y = x + 64
```
Valid for `x ∈ [0,639]`, `y ∈ [0,863]` → `X ∈ [80,943]`, `Y ∈ [64,703]`. Pixels outside this box are border and need an explicit fill value (e.g., black).
### FPGA Implementation Notes
- **Frame buffer sizing (confirmed 1-bit depth):** source frame = 640 × 864 = 552,960 pixels = 69,120 bytes (~68 KB) at 1 bpp. Double-buffered: ~135 KB total — very likely fits on-chip BRAM on most mid-size FPGA boards, avoiding external DRAM/memory controller complexity.
- **Clock domain crossing:** source pixel clock (native 1986 64/68Hz board timing — to be measured in Phase 0) and 1024×768 HDMI output clock (~65 MHz for standard 60 Hz XGA timing) are independent domains. The frame buffer must be implemented as a proper dual-clock RAM/FIFO, not a single-clock design, to avoid metastability.
- **Border fill:** decide and hardcode a fill value (black recommended) for the 80px/64px borders; gate output explicitly during border regions rather than leaving them undefined.
- **HDMI timing:** 1024×768 is a standard VESA mode with well-documented timing tables — confirm target refresh rate (60 Hz assumed) since it sets the pixel clock and output-side buffer read rate.
## 7. Target Display Selection
**Primary target (this spec): 1024×768** — 15" DVI panel already in hand, HDMI→DVI converted. This is the configuration detailed in Section 5 (centered, no scaling required).
**Considered alternative: 1280×1024** — inexpensive, widely available LCD panels. Deferred to a future phase for the following reason: centering the 864×640 rotated image alone leaves large unused borders (208px sides, 192px top/bottom), so a scaled-up image would be preferable — but scaling a 1-bit monochrome image cleanly requires nearest-neighbor pixel replication (no interpolation possible), and 864×640 doesn't scale by a clean integer factor into 1280×1024. This needs a Bresenham-style pixel-replication accumulator, added frame-buffer read bandwidth, and an extra pipeline stage — nontrivial additional scope.
**Recommendation:** build and validate the 1024×768 centered (no-scaling) pipeline first as the primary deliverable. Treat 1280×1024 (with scaling) as a follow-on enhancement once the core rotation/timing pipeline is proven, not a parallel initial target.
## 8. Open Items Before Implementation
- [x] Confirm production source resolution — **confirmed: 640×864 (W×H), 1986 Radius 64/68Hz board**
- [x] Confirm color depth — **confirmed: 1-bit monochrome**
- [x] Single vs. dual timing mode — **confirmed: single fixed timing, targeting the 55.000MHz/68.75kHz/67.139Hz crystal 1 mode**
- [x] Target display for initial build — **confirmed: 1024×768 15" panel, centered, no scaling (primary); 1280×1024 scaled deferred to future phase**
- [ ] Capture and document exact horizontal/vertical blanking breakdown (front porch/sync/back porch) for the fixed mode (Phase 0)
- [ ] Identify exact tap point for pixel data, PCLK, HSYNC, VSYNC (and BLANK/DE if present) on the display connector (Phase 0.5)
- [ ] Confirm TTL logic voltage and FPGA I/O compatibility; select level-shifting/buffering approach
- [ ] Decide whether original card must continue driving its own FPD monitor simultaneously (affects whether tap must be non-loading/high-impedance) or whether the original display path can be fully diverted
- [ ] Confirm target refresh rate for 1024×768 output (60 Hz assumed)
- [ ] Confirm frame buffer implementation: on-chip BRAM (now very likely sufficient at 1bpp) vs. external memory
- [ ] Confirm latency budget tolerates ~1–2 frames of added delay — flag if this is a touch/interactive/real-time application
- [ ] Verify rotation direction (CW, as derived here) against actual physical mount orientation on real hardware
- [ ] Decide and implement border fill value/logic for the 80px/64px letterbox regions
- [ ] Clarify what "64" and "86" refer to in the board's name (vertical refresh? horizontal scan rate? two distinct modes?) — relevant to Phase 0 timing capture