a2fpga-series — Part 2

Building the Rendering Pipeline

Getting 80 columns of text from VRAM to pixels on a modern HDMI display: the Videx VideoTerm rendering pipeline in SystemVerilog, C8-space ownership, and the Pascal boot hang that required reading a 1983 firmware disassembly to fix.

16 min read advanced
apple-iifpgaemulationretrocomputinga2fpga-seriesdeep-diveverilog

Part 2 of 2. Part 1 covered the Videx hardware architecture, the MC6845 CRTC, and the slot 3 address map. This part covers the rendering pipeline, C8-space ownership, and the Pascal boot hang.


The hard part of emulating a display card is not the register interface. Writing a SystemVerilog model of the MC6845’s 18 registers is tedious but mechanical — you read the datasheet, you implement the fields, you verify read-back behavior. The hard part is getting pixel data out of VRAM and onto the screen at exactly the right time, in the right format, synchronized with signals that are themselves derived from a 54MHz FPGA clock pretending to be a 1MHz Apple II bus.

The Video Chain

The A2FPGA renders video through a chain of modules. The Apple II’s native video generator (VGC) produces a stream of RGB pixels from the Apple II’s text and graphics framebuffers. The SuperSprite card’s TMS9918a emulation can overlay sprite graphics on top of that. The Videx card sits between VGC and SuperSprite — it receives Apple video RGB as input and either passes it through unchanged (40-column mode) or replaces it with Videx-rendered 80-column text.

Apple II CPU/Memory

   VGC (Apple native video)
      ↓ RGB
   Videx (80-col mux)
      ↓ RGB
   SuperSprite (TMS9918a overlay)
      ↓ RGB + HDMI signals
   HDMI output

The Videx module receives the current pixel coordinates (screen_x, screen_y) and outputs RGB. When Videx 80-column mode is active, it computes which character cell that pixel belongs to, looks up the character code from VRAM, fetches the character glyph from the character ROM, and drives the pixel as white (character foreground) or black (background). When 40-column mode is active, it passes the input RGB through unchanged.

The Rendering Pipeline

The rendering pipeline has several stages, each taking one or more clock cycles. Because the FPGA runs at 54MHz and the video pixel clock runs much slower, there are plenty of cycles to work with — but they have to be orchestrated carefully.

Stage 1: Address computation. Given screen_x and screen_y, compute the VRAM address of the character at this position. The Videx VideoTerm displays 80 characters across and 24 rows down. Each character cell is 8 pixels wide and 18 pixels tall (though only 9 scanlines contain glyph data — the other 9 are blank, providing line spacing). The VRAM address is simply (row * 80) + column, and the scan line within the character cell is screen_y mod 18 (or specifically, which of the 9 active rows within the cell).

Stage 2: VRAM read. The computed VRAM address is presented to the dual-port block RAM. The read port operates synchronously, so there is a one-cycle latency before the character code appears.

Stage 3: Character ROM read. The character code from VRAM plus the current scan line within the cell form the character ROM address: {char_code[6:0], scan_line[3:0]}. The ROM returns an 8-bit glyph row — one bit per pixel, 1 = foreground, 0 = background.

Stage 4: Pixel extraction. The specific pixel within the 8-bit glyph row is selected by the column position within the character cell. Inverse video is applied by XORing with bit 7 of the character code. Cursor inversion is applied by XORing with the cursor active signal (cursor position matches current cell and cursor blink is in the on phase).

Stage 5: RGB output. A 1 bit becomes white (8'hE0, 8'hE0, 8'hE0) and a 0 bit becomes black. The color palette is parameterized — the Apple II and IIgs palettes use slightly different white point values — but the default is a warm phosphor white.

The pipeline depth means that the pixel output for coordinate (x, y) is actually computed starting from an earlier (x’, y’) value. The pipeline must be primed with the correct offset so that pixel data arrives exactly as the scan position reaches the corresponding screen coordinate.

40/80-Column Switching

The Videx VideoTerm switches between 40 and 80-column modes using the AN0 soft switch at $C058/$C059. AN0 is one of the Apple II’s four annunciator outputs — general-purpose control signals originally intended for game paddles and analog hardware, repurposed by Videx for display mode control.

When $C058 is accessed (AN0 = 0), the Videx card disables its video output and passes through the Apple II’s native 40-column video. When $C059 is accessed (AN0 = 1), the Videx card enables its 80-column rendering and mutes the native video.

The state is held in a single register (videx_active) that tracks the current AN0 state. The FPGA monitors the Apple II address bus for these soft switch addresses and updates the register accordingly.

There is one additional detail: pressing Ctrl-Reset on the Apple II resets the system soft switches, returning AN0 to 0 and restoring 40-column mode. The Videx emulation handles this through the system reset signal.

always @(posedge clk_logic or negedge system_reset_n) begin
    if (!system_reset_n)
        videx_active <= 1'b0;
    else if (phi1_posedge && !m2sel_n) begin
        case (addr)
            16'hC058: videx_active <= 1'b0;  // AN0 off = 40-col
            16'hC059: videx_active <= 1'b1;  // AN0 on  = 80-col
        endcase
    end
end

C8-Space Ownership

The expansion ROM ownership protocol is where the most subtle state management lives.

The Videx card needs $C800–$CFFF for its expansion ROM (the bulk of the firmware) and for the VRAM window at $CC00–$CDFF. But the Apple II’s $C800–$CFFF space is shared — any card with an expansion ROM can use it, but only one card at a time can respond to reads there. The ownership protocol ensures that the right card drives the bus.

The Videx card claims C8-space ownership when the CPU reads from $C300–$C3FF (the slot ROM). It releases ownership when the CPU accesses $CFFF or when another slot’s $Csxx ROM space is accessed. The flag:

c8_owned SET   on $C300-$C3FF access
c8_owned CLEAR on $CFFF access (while INTCXROM inactive)
c8_owned CLEAR on another slot's $C1xx-$C7xx ROM access
c8_owned CLEAR on system reset

The active flag is rom_c8_active = c8_owned && !INTCXROM. When active, the card responds to $C800–$CFFF. When not active, it ignores those addresses entirely.

A critical subtlety: the implementation uses rom_c8_active && phi0 rather than the io_strobe_n signal provided by the slot framework. This is necessary because on the Apple ][+, io_strobe_n is blocked by INTC8ROM after any slot 3 access — which is exactly the scenario the Videx firmware navigates continuously. Using phi0-qualified address detection directly sidesteps the io_strobe_n gating and matches real hardware behavior.

The $CFFF access must also be phi0-qualified. Bus transceiver glitches during the phi0→phi1 transition can produce spurious address patterns that look like $CFFF to a naive implementation. Qualifying the detection with phi0 filters out the glitches:

always @(posedge clk_logic or negedge system_reset_n) begin
    if (!system_reset_n)
        c8_owned <= 1'b0;
    else if (phi0) begin
        if (slot_io_select)          c8_owned <= 1'b1;  // $C300 access
        else if (cfff_access)        c8_owned <= 1'b0;  // $CFFF strobe
        else if (other_slot_c8)      c8_owned <= 1'b0;  // another slot active
    end
end

The Pascal Boot Hang

Apple Pascal 1.3 was one of the primary test cases for the Videx emulation, and for a long time it hung during boot.

The Pascal boot sequence is not simple. It boots from a DOS 3.3 disk, then chains to the Pascal system through several firmware hand-offs. The Pascal kernel probes available hardware early in its initialization, and part of that probe involves calling the Videx firmware through the Pascal Peripheral Card Protocol — a specific calling convention defined for Apple II cards that support Pascal.

The hang occurred at a specific point: after Pascal successfully initialized the Videx card (80-column mode, cursor positioned), the system would lock up attempting to access the expansion ROM. After reading the Videx firmware disassembly closely, the problem became clear.

The Pascal INIT path enters at $C311, which jumps to $C800 — the start of the expansion ROM. This path never touches $CFFF on the way in. The main handler at $C336 (MAIN_HANDLER) does STA $CFFF at every entry as part of its ownership housekeeping, but the JMP $C800 cold entry bypasses MAIN_HANDLER entirely.

The issue: if the C8-space ownership flag was cleared by a previous event before the JMP $C800 executes, the VRAM window at $CC00–$CDFF becomes inaccessible — because VRAM window access requires rom_c8_active. Pascal’s initialization writes to the screen through the VRAM window. Without C8 ownership, those writes go nowhere. Without initial screen state, subsequent Pascal operations produce garbage output and eventually crash.

The fix: ensure that the slot ROM access at $C300 (which triggers C8 ownership) cannot be inadvertently cleared before the JMP $C800 completes. In practice this meant tightening the other_slot_c8 detection to only fire on genuine other-slot accesses, not on bus conditions that look like other-slot accesses due to CDC pipeline timing.

Once fixed, Pascal 1.3 boots cleanly to an 80-column display:

Applesoft BASIC running in 80-column mode, output via HDMI to a modern flat-panel display

That’s not Pascal in the screenshot — but the 80-column text mode is the same pipeline that Pascal uses. The PR# 3 on line 50 is exactly the call that activates the Videx card, the same call Pascal uses, the same firmware entry point.

Testing

The Videx emulation was tested against:

  • VIDEX_DIAG — a custom diagnostic suite that tests every MC6845 register, read-back, VRAM access, and ROM checksum. All tests pass.
  • Apple Pascal 1.3 — full boot to 80-column interactive environment, file operations, program editing.
  • Original Videx VideoTerm Demo disk — the demo software Videx shipped with the original card runs identically on the emulation.
  • CardCat — correctly identifies the card by its firmware ROM signature.
  • Concurrent operation — all five emulated cards (Videx, ThunderClock, SSC, Mockingboard, SuperSprite) running simultaneously, exercising the shared C8-space arbitration.

The hardest part of FPGA emulation is not reproducing the documented behavior. The original MC6845 datasheet is complete and accurate. The Videx firmware is disassembleable. The hard part is reproducing the undocumented timing — the specific moments when the 6845 samples the bus, when the bus transceiver glitches, when phi0 qualification matters and when it doesn’t. Several of those moments turned out to be bugs in the upstream codebase that Videx development exposed for the first time. Those are covered in Five Bugs That Lived in the Dark.