From a0016da3a70580e9b4ff1e26e53c77dd9f658660 Mon Sep 17 00:00:00 2001 From: Joris van Zwieten Date: Tue, 18 Aug 2026 23:14:27 +0200 Subject: [PATCH] rain: rewrite matrix mode text pixel pipeline The rotate+stretch shifter that expands font bytes into the 16-tick character cells has three defects, visible as rendering artifacts in matrix mode: 1. the 2x pixel-stretch phase free-runs across raster lines; with an odd toggle count per line it flips every scanline, displacing alternate scanlines of each glyph row by one pixel (jagged, zig-zag glyph edges); 2. 8 font pixels x 2 ticks never fit the 16-tick cell without a seam: depending on phase, either the last font pixel lingers for a third tick (glyphs grow an extra pixel on the right) or the first font column ghosts at the end of the cell; 3. display order is b0-first: every glyph is shifted one pixel right, with the rightmost font column wrapped around to the cell's left edge (visible on glyphs like 'm' and 'p' whose rows use bit 0). Replace the shifter with a strobe-aligned character reload (exact 16-tick cells) and an indexed, msb-first font-bit select. Verified in a differential RTL simulation harness (synthetic PAL raster, banner text injected through the monitor char handshake, rendered frames dumped as images): all 320 header character cells now match the font ROM pixel-exactly, with no mismatched scanline pairs and no odd-width strokes. Also verified on hardware (matrix mode on an openXC7 build of the core for an xc7a100t board). --- src/vhdl/rain.vhdl | 57 +++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/src/vhdl/rain.vhdl b/src/vhdl/rain.vhdl index e8b41a830..39b55a3e9 100644 --- a/src/vhdl/rain.vhdl +++ b/src/vhdl/rain.vhdl @@ -248,6 +248,8 @@ begin -- rtl process(pixelclock) variable screenram_busy : std_logic := '0'; variable yoffset : integer := 0; + variable char_k : integer range 0 to 7 := 0; + variable char_px : std_logic := '0'; begin if rising_edge(pixelclock) then @@ -670,7 +672,23 @@ begin -- rtl line_screen_address <= line_screen_address + te_line_length; end if; end if; - elsif char_bit_count = 0 then + elsif ((last_xcounter_t1 /= last_xcounter_t2) and (pal_mode='0')) + or ((last_xcounter_t1 /= last_xcounter_in) and (pal_mode='1')) + then + -- One branch per 640H pixel tick: either reload the next character + -- (exactly replacing the 0-decrement, so every cell is 16 ticks) + -- or advance the tick counter. The displayed bit is INDEXED from + -- char_bit_count (see char_px below) instead of rotating char_bits. + -- The old rotate+stretch scheme had three defects: (1) the stretch + -- phase free-ran across raster lines and, with an odd toggle count + -- per line, flipped every scanline - displacing alternate scanlines + -- of each glyph row one 640H pixel sideways (zig-zag glyph edges); + -- (2) it could not fit 8 font pixels x 2 ticks into the 16-tick + -- cell without a seam (an extra pixel on the right of some glyphs, + -- or a ghost of the first column at the cell end); (3) it displayed + -- bit 0 first, shifting every glyph one pixel right and wrapping + -- the rightmost font column to the left edge of the cell. + if char_bit_count = 0 then -- Request next character if xcounter_in >= debug_x and xcounter_in < (debug_x+10) then report @@ -706,22 +724,8 @@ begin -- rtl fetch_next_char <= '1'; char_bit_count <= 15; column_counter <= column_counter + 1; - else - -- rotate bits for terminal chargen every 2 640H pixels - -- Delayed by 2 cycles to match pixel edge with real pixel edge. - -- (Actually we need to tweak the delay for PAL and NTSC differently - -- still for some reason?) - if ((last_xcounter_t1 /= last_xcounter_t2) and (pal_mode='0')) - or ((last_xcounter_t1 /= last_xcounter_in) and (pal_mode='1')) - then - char_bit_stretch <= not char_bit_stretch; - if char_bit_stretch = '1' and char_bit_count /= 1 then - char_bits(7 downto 1) <= char_bits(6 downto 0); - char_bits(0) <= char_bits(7); - end if; - if char_bit_count /= 0 then - char_bit_count <= char_bit_count - 1; - end if; + else + char_bit_count <= char_bit_count - 1; end if; end if; @@ -847,6 +851,13 @@ begin -- rtl feed <= Rain; end if; + -- Select the displayed font bit by indexing the latched character + -- byte with the tick counter: font rows are stored msb-left (see + -- assets/ascii00-7f.png), so display order is b7 down to b0, two + -- ticks per font pixel. Replaces the old rotate-based shifter. + char_k := (15 - char_bit_count) / 2; + char_px := char_bits(7 - char_k); + -- Now that we know what we want to display, actually display it. if xcounter_in >= debug_x and xcounter_in < (debug_x+10) then report @@ -877,7 +888,7 @@ begin -- rtl vgablue_out <= x"00"; end if; elsif (row_counter = 3 or row_counter = 4) then - if secure_mode_flag='0' or char_bits(0)='0' or column_visible='0' or te_blink_state='1' then + if secure_mode_flag='0' or char_px='0' or column_visible='0' or te_blink_state='1' then -- Hide the secure compartment instructions when not in secure mode -- (and otherwise show as solid orange slab when in secure mode -- for the off-phase blink.) @@ -886,9 +897,9 @@ begin -- rtl vgagreen_out(6 downto 0) <= (others => '0'); vgagreen_out(7) <= '0'; else - vgared_out <= (others => te_blink_state xor (char_bits(0) and column_visible)); - vgagreen_out(6 downto 0) <= (others => te_blink_state xor (char_bits(0) and column_visible)); - vgagreen_out(7) <= te_blink_state xor (char_bits(0) and column_visible); + vgared_out <= (others => te_blink_state xor (char_px and column_visible)); + vgagreen_out(6 downto 0) <= (others => te_blink_state xor (char_px and column_visible)); + vgagreen_out(7) <= te_blink_state xor (char_px and column_visible); end if; vgablue_out <= x"00"; else @@ -914,7 +925,7 @@ begin -- rtl elsif row_counter >= te_header_line_count then -- In normal text area - if (char_bits(0) = '1') and column_visible='1' then + if (char_px = '1') and column_visible='1' then if is_cursor='1' and te_blink_state='1' then -- Display visual beep by making background of monitor -- terminal area flash red. @@ -962,7 +973,7 @@ begin -- rtl else -- In header of matrix mode -- Note that cursor is not visible in header area - if char_bits(0) = '0' or (column_visible='0') then + if char_px = '0' or (column_visible='0') then vgared_out <= (others => '0'); vgagreen_out <= (others => '0'); vgablue_out <= (others => '0');