[{"data":1,"prerenderedAt":2446},["ShallowReactive",2],{"blog:2026:vtx5000-part-5-rendering":3,"blogMore-Technology":2436,"comments-vtx5000-part-5-rendering":2445},{"id":4,"title":5,"body":6,"category":2414,"commentCount":2415,"date":2416,"description":2417,"excerpt":2418,"extension":2419,"filenames":2420,"hidden":2421,"image":2422,"meta":2425,"minutes":416,"navigation":940,"path":2426,"seo":2427,"showCategory":2420,"stem":2428,"tags":2429,"updated":2420,"url":2433,"wordCount":2434,"__hash__":2435},"content\u002Fblog\u002F2026\u002Fvtx5000-part-5-rendering.md","VTX5000: Part 5 - Rendering",{"type":7,"value":8,"toc":2401},"minimark",[9,19,24,47,229,244,247,254,566,570,581,763,766,770,778,786,796,1090,1124,1127,1131,1137,1292,1306,1310,1316,1537,1548,1552,1559,1578,1592,1876,1879,1883,1893,1900,1994,2000,2003,2007,2017,2028,2113,2120,2202,2211,2224,2228,2235,2261,2264,2267,2271,2284,2370,2381,2385,2388,2391,2394,2397],[10,11,12,13,18],"p",{},"In ",[14,15,17],"a",{"href":16},"\u002Fblog\u002F2026\u002Fvtx5000-part-4-comms-routines\u002F","part 4"," we followed data to and from the TI modem through the Intel 8251 USART landing in our buffers.  Now comes the interesting part - Rendering those Viewdata 40×24 frames on what is typically a 32×40 display.",[20,21,23],"h3",{"id":22},"viewdata-control-codes","Viewdata Control Codes",[10,25,26,27,31,32,35,36,39,40,43,44,46],{},"Every byte received passes through ",[28,29,30],"code",{},"PROCESS_RX_CHAR"," at ",[28,33,34],{},"$160E",". Characters ",[28,37,38],{},"$20","-",[28,41,42],{},"$7F"," are printable, but below ",[28,45,38],{}," is a control code and viewdata treats them mostly like ASCII codes:",[48,49,50,66],"table",{},[51,52,53],"thead",{},[54,55,56,60,63],"tr",{},[57,58,59],"th",{},"Code",[57,61,62],{},"Name",[57,64,65],{},"Action",[67,68,69,83,96,109,122,135,148,161,177,190,203,216],"tbody",{},[54,70,71,77,80],{},[72,73,74],"td",{},[28,75,76],{},"$05",[72,78,79],{},"ENQ",[72,81,82],{},"Clear echo mode",[54,84,85,90,93],{},[72,86,87],{},[28,88,89],{},"$08",[72,91,92],{},"BS",[72,94,95],{},"Cursor left",[54,97,98,103,106],{},[72,99,100],{},[28,101,102],{},"$09",[72,104,105],{},"HT",[72,107,108],{},"Cursor right",[54,110,111,116,119],{},[72,112,113],{},[28,114,115],{},"$0A",[72,117,118],{},"LF",[72,120,121],{},"Cursor down",[54,123,124,129,132],{},[72,125,126],{},[28,127,128],{},"$0B",[72,130,131],{},"VT",[72,133,134],{},"Cursor up",[54,136,137,142,145],{},[72,138,139],{},[28,140,141],{},"$0C",[72,143,144],{},"FF",[72,146,147],{},"Clear screen, home cursor",[54,149,150,155,158],{},[72,151,152],{},[28,153,154],{},"$0D",[72,156,157],{},"CR",[72,159,160],{},"Cursor to column 0",[54,162,163,171,174],{},[72,164,165,39,168],{},[28,166,167],{},"$0E",[28,169,170],{},"$0F",[72,172,173],{},"—",[72,175,176],{},"(Shift in\u002Fout, handled elsewhere)",[54,178,179,184,187],{},[72,180,181],{},[28,182,183],{},"$11",[72,185,186],{},"DC1",[72,188,189],{},"Cursor on",[54,191,192,197,200],{},[72,193,194],{},[28,195,196],{},"$14",[72,198,199],{},"DC4",[72,201,202],{},"Cursor off",[54,204,205,210,213],{},[72,206,207],{},[28,208,209],{},"$1B",[72,211,212],{},"ESC",[72,214,215],{},"Set escape flag (next char is a control code)",[54,217,218,223,226],{},[72,219,220],{},[28,221,222],{},"$1E",[72,224,225],{},"RS",[72,227,228],{},"Home cursor (row 0, col 0)",[10,230,231,232,235,236,239,240,243],{},"The ESC mechanism is how Viewdata sends colour and attribute changes. When ESC is received, the flag at bit 0 of ",[28,233,234],{},"IX+$09"," is set. The next printable character has bit 6 cleared, converting it from ASCII into a control code — so ESC followed by ",[28,237,238],{},"A"," ($41) becomes ",[28,241,242],{},"$01"," (red text). The set-based attribute system works like teletext: colour changes apply from the current position to the end of the row and importantly the change itself takes up a space on the screen.",[10,245,246],{},"This ensures that each viewdata frame is exactly the same size regardless of colour changes etc. and also crucially for us avoids colour clash on our 32x24 attribute display.",[10,248,249,250,253],{},"The cursor position is stored as row in L, column in H. After processing any control code, the bounds-checking code at ",[28,251,252],{},".update_cursor"," clamps the position to the 40×24 grid and handles wrapping both across rows and columns:",[255,256,261],"pre",{"className":257,"code":258,"language":259,"meta":260,"style":260},"language-z80 shiki shiki-themes everforest-light dracula",".update_cursor:\n    LD A,$27             ; Max column = 39\n    BIT 7,H              ; Column negative?\n    JR Z,.col_ok\n    LD H,A               ; Wrap to rightmost column\n    DEC L                ; Move up a row\n.col_ok:\n    CP H                 ; Column > 39?\n    JR NC,.col_bounded\n    LD H,$00             ; Wrap to column 0\n    INC L                ; Move to next row\n.col_bounded:\n    LD A,$17             ; Max row = 23\n    BIT 7,L              ; Row negative?\n    JR Z,.row_ok\n    LD L,A               ; Wrap to bottom\n.row_ok:\n    CP L                 ; Row > 23?\n    JR NC,.row_bounded\n    LD L,$00             ; Wrap to top\n.row_bounded:\n    LD (IX+$0C),L        ; Store cursor row\n    LD (IX+$0D),H        ; Store cursor column\n    RET\n","z80","",[28,262,263,275,297,314,323,338,350,358,369,380,395,406,414,429,444,452,466,474,484,494,508,516,540,560],{"__ignoreMap":260},[264,265,268,271],"span",{"class":266,"line":267},"line",1,[264,269,252],{"class":270},"sSKRk",[264,272,274],{"class":273},"s6Vpi",":\n",[264,276,278,282,286,289,293],{"class":266,"line":277},2,[264,279,281],{"class":280},"smiwp","    LD",[264,283,285],{"class":284},"stJs5"," A",[264,287,288],{"class":273},",",[264,290,292],{"class":291},"s3Ipq","$27",[264,294,296],{"class":295},"sSX4p","             ; Max column = 39\n",[264,298,300,303,306,308,311],{"class":266,"line":299},3,[264,301,302],{"class":280},"    BIT",[264,304,305],{"class":291}," 7",[264,307,288],{"class":273},[264,309,310],{"class":284},"H",[264,312,313],{"class":295},"              ; Column negative?\n",[264,315,317,320],{"class":266,"line":316},4,[264,318,319],{"class":280},"    JR",[264,321,322],{"class":273}," Z,.col_ok\n",[264,324,326,328,331,333,335],{"class":266,"line":325},5,[264,327,281],{"class":280},[264,329,330],{"class":284}," H",[264,332,288],{"class":273},[264,334,238],{"class":284},[264,336,337],{"class":295},"               ; Wrap to rightmost column\n",[264,339,341,344,347],{"class":266,"line":340},6,[264,342,343],{"class":280},"    DEC",[264,345,346],{"class":284}," L",[264,348,349],{"class":295},"                ; Move up a row\n",[264,351,353,356],{"class":266,"line":352},7,[264,354,355],{"class":270},".col_ok",[264,357,274],{"class":273},[264,359,361,364,366],{"class":266,"line":360},8,[264,362,363],{"class":280},"    CP",[264,365,330],{"class":284},[264,367,368],{"class":295},"                 ; Column > 39?\n",[264,370,372,374,377],{"class":266,"line":371},9,[264,373,319],{"class":280},[264,375,376],{"class":284}," NC",[264,378,379],{"class":273},",.col_bounded\n",[264,381,383,385,387,389,392],{"class":266,"line":382},10,[264,384,281],{"class":280},[264,386,330],{"class":284},[264,388,288],{"class":273},[264,390,391],{"class":291},"$00",[264,393,394],{"class":295},"             ; Wrap to column 0\n",[264,396,398,401,403],{"class":266,"line":397},11,[264,399,400],{"class":280},"    INC",[264,402,346],{"class":284},[264,404,405],{"class":295},"                ; Move to next row\n",[264,407,409,412],{"class":266,"line":408},12,[264,410,411],{"class":270},".col_bounded",[264,413,274],{"class":273},[264,415,417,419,421,423,426],{"class":266,"line":416},13,[264,418,281],{"class":280},[264,420,285],{"class":284},[264,422,288],{"class":273},[264,424,425],{"class":291},"$17",[264,427,428],{"class":295},"             ; Max row = 23\n",[264,430,432,434,436,438,441],{"class":266,"line":431},14,[264,433,302],{"class":280},[264,435,305],{"class":291},[264,437,288],{"class":273},[264,439,440],{"class":284},"L",[264,442,443],{"class":295},"              ; Row negative?\n",[264,445,447,449],{"class":266,"line":446},15,[264,448,319],{"class":280},[264,450,451],{"class":273}," Z,.row_ok\n",[264,453,455,457,459,461,463],{"class":266,"line":454},16,[264,456,281],{"class":280},[264,458,346],{"class":284},[264,460,288],{"class":273},[264,462,238],{"class":284},[264,464,465],{"class":295},"               ; Wrap to bottom\n",[264,467,469,472],{"class":266,"line":468},17,[264,470,471],{"class":270},".row_ok",[264,473,274],{"class":273},[264,475,477,479,481],{"class":266,"line":476},18,[264,478,363],{"class":280},[264,480,346],{"class":284},[264,482,483],{"class":295},"                 ; Row > 23?\n",[264,485,487,489,491],{"class":266,"line":486},19,[264,488,319],{"class":280},[264,490,376],{"class":284},[264,492,493],{"class":273},",.row_bounded\n",[264,495,497,499,501,503,505],{"class":266,"line":496},20,[264,498,281],{"class":280},[264,500,346],{"class":284},[264,502,288],{"class":273},[264,504,391],{"class":291},[264,506,507],{"class":295},"             ; Wrap to top\n",[264,509,511,514],{"class":266,"line":510},21,[264,512,513],{"class":270},".row_bounded",[264,515,274],{"class":273},[264,517,519,521,524,527,530,532,535,537],{"class":266,"line":518},22,[264,520,281],{"class":280},[264,522,523],{"class":273}," (",[264,525,526],{"class":284},"IX",[264,528,529],{"class":273},"+",[264,531,141],{"class":291},[264,533,534],{"class":273},"),",[264,536,440],{"class":284},[264,538,539],{"class":295},"        ; Store cursor row\n",[264,541,543,545,547,549,551,553,555,557],{"class":266,"line":542},23,[264,544,281],{"class":280},[264,546,523],{"class":273},[264,548,526],{"class":284},[264,550,529],{"class":273},[264,552,154],{"class":291},[264,554,534],{"class":273},[264,556,310],{"class":284},[264,558,559],{"class":295},"        ; Store cursor column\n",[264,561,563],{"class":266,"line":562},24,[264,564,565],{"class":280},"    RET\n",[20,567,569],{"id":568},"the-display-buffer","The Display Buffer",[10,571,572,573,576,577,580],{},"Printable characters don't go directly to the screen. They're stored in a 40×24 display buffer (960 bytes) within the IX state block, then rendered. The ",[28,574,575],{},"CALC_BUF_ADDR"," routine at ",[28,578,579],{},"$1A59"," converts that row=L, column=H position to a buffer address:",[255,582,584],{"className":257,"code":583,"language":259,"meta":260,"style":260},"CALC_BUF_ADDR:\n    PUSH HL\n    LD A,H               ; Column\n    LD H,$00\n    LD D,H\n    LD E,L               ; DE = row\n    ADD HL,HL            ; ×2\n    ADD HL,HL            ; ×4\n    ADD HL,DE            ; ×5\n    ADD HL,HL            ; ×10\n    ADD HL,HL            ; ×20\n    ADD HL,HL            ; ×40\n    LD E,A               ; Add column\n    ADD HL,DE            ; HL = row × 40 + column\n    ; ... offset from IX base ...\n",[28,585,586,592,600,613,624,636,650,666,679,693,706,719,732,745,758],{"__ignoreMap":260},[264,587,588,590],{"class":266,"line":267},[264,589,575],{"class":270},[264,591,274],{"class":273},[264,593,594,597],{"class":266,"line":277},[264,595,596],{"class":280},"    PUSH",[264,598,599],{"class":284}," HL\n",[264,601,602,604,606,608,610],{"class":266,"line":299},[264,603,281],{"class":280},[264,605,285],{"class":284},[264,607,288],{"class":273},[264,609,310],{"class":284},[264,611,612],{"class":295},"               ; Column\n",[264,614,615,617,619,621],{"class":266,"line":316},[264,616,281],{"class":280},[264,618,330],{"class":284},[264,620,288],{"class":273},[264,622,623],{"class":291},"$00\n",[264,625,626,628,631,633],{"class":266,"line":325},[264,627,281],{"class":280},[264,629,630],{"class":284}," D",[264,632,288],{"class":273},[264,634,635],{"class":284},"H\n",[264,637,638,640,643,645,647],{"class":266,"line":340},[264,639,281],{"class":280},[264,641,642],{"class":284}," E",[264,644,288],{"class":273},[264,646,440],{"class":284},[264,648,649],{"class":295},"               ; DE = row\n",[264,651,652,655,658,660,663],{"class":266,"line":352},[264,653,654],{"class":280},"    ADD",[264,656,657],{"class":284}," HL",[264,659,288],{"class":273},[264,661,662],{"class":284},"HL",[264,664,665],{"class":295},"            ; ×2\n",[264,667,668,670,672,674,676],{"class":266,"line":360},[264,669,654],{"class":280},[264,671,657],{"class":284},[264,673,288],{"class":273},[264,675,662],{"class":284},[264,677,678],{"class":295},"            ; ×4\n",[264,680,681,683,685,687,690],{"class":266,"line":371},[264,682,654],{"class":280},[264,684,657],{"class":284},[264,686,288],{"class":273},[264,688,689],{"class":284},"DE",[264,691,692],{"class":295},"            ; ×5\n",[264,694,695,697,699,701,703],{"class":266,"line":382},[264,696,654],{"class":280},[264,698,657],{"class":284},[264,700,288],{"class":273},[264,702,662],{"class":284},[264,704,705],{"class":295},"            ; ×10\n",[264,707,708,710,712,714,716],{"class":266,"line":397},[264,709,654],{"class":280},[264,711,657],{"class":284},[264,713,288],{"class":273},[264,715,662],{"class":284},[264,717,718],{"class":295},"            ; ×20\n",[264,720,721,723,725,727,729],{"class":266,"line":408},[264,722,654],{"class":280},[264,724,657],{"class":284},[264,726,288],{"class":273},[264,728,662],{"class":284},[264,730,731],{"class":295},"            ; ×40\n",[264,733,734,736,738,740,742],{"class":266,"line":416},[264,735,281],{"class":280},[264,737,642],{"class":284},[264,739,288],{"class":273},[264,741,238],{"class":284},[264,743,744],{"class":295},"               ; Add column\n",[264,746,747,749,751,753,755],{"class":266,"line":431},[264,748,654],{"class":280},[264,750,657],{"class":284},[264,752,288],{"class":273},[264,754,689],{"class":284},[264,756,757],{"class":295},"            ; HL = row × 40 + column\n",[264,759,760],{"class":266,"line":446},[264,761,762],{"class":295},"    ; ... offset from IX base ...\n",[10,764,765],{},"The Z80 has no multiplication instruction so that sequence of 6 ADD instructions is a fast, fixed, ×40 on a 16-bit register which works fine if we know we won't wrap.",[20,767,769],{"id":768},"the-font-5-columns-to-8-rows","The Font: 5 Columns to 8 Rows",[10,771,772,777],{},[773,774],"img",{"alt":775,"src":776},"The image shows a list of letters and numbers, including \"abcdefghijklmnopqrstuvwxyz\", \"1234567890\", and \"ABCEFGHIJKLMNOPQRSTUVWXYZ\".","https:\u002F\u002Fimg.damieng.com\u002Ffb7c1a79-d25c-4f74-9288-fda5e9b87484-vtx5000-font.png","The font is almost a pixel-accurate copy of the one from the Mullard SAA5050 but with a couple of odd subtle changes. Specifically the y and the g have been vertically compressed to not use the bottom row - odd given that 'j' is using it just fine - and one extra pixel top right of the m which makes it look more square than the rest of the font.",[10,779,780,781,785],{},"Aesthetics aside, this is where the VTX5000 ROM starts shaving some extra bytes.  The Viewdata character set uses a 5×8 pixel grid, and the font is stored in the ROM as ",[782,783,784],"strong",{},"5 column bytes"," per character with each byte representing one vertical column of 8 pixels.  This is in contrast to the ROM where the 8x8 fonts it supports are stored on byte per horizontal row which better matches the Spectrums screen RAM.",[10,787,788,789,576,792,795],{},"Given that every character needs to be transposed from column format to row format before it can be written to screen and the ",[28,790,791],{},"SETUP_FONT_RENDER",[28,793,794],{},"$19BB"," does this transposition in a tight loop:",[255,797,799],{"className":257,"code":798,"language":259,"meta":260,"style":260},"; Load 5 column bytes from font table\n    LD C,(HL)            ; Column 1\n    INC HL\n    LD B,(HL)            ; Column 2 → H\n    INC HL\n    LD E,(HL)            ; Column 3\n    INC HL\n    LD D,(HL)            ; Column 4\n    INC HL\n    LD L,(HL)            ; Column 5\n    LD H,B               ; H = Column 2\n    LD B,$08             ; 8 rows to generate\n\n.transpose_loop:\n    XOR A                ; Clear accumulator\n    RR C                 ; Rotate column 1, bit → carry\n    RLA                  ; Carry → A bit 0\n    RR H                 ; Rotate column 2\n    RLA                  ; → A bit 1\n    RR E                 ; Rotate column 3\n    RLA                  ; → A bit 2\n    RR D                 ; Rotate column 4\n    RLA                  ; → A bit 3\n    RR L                 ; Rotate column 5\n    RLA                  ; → A bit 4\n    EXX\n    LD (BC),A            ; Store transposed row byte\n    INC BC\n    EXX\n    DJNZ .transpose_loop ; Repeat for all 8 rows\n",[28,800,801,806,824,830,846,852,867,873,888,894,909,923,936,942,949,959,969,977,986,993,1002,1009,1018,1025,1034,1042,1048,1065,1073,1078],{"__ignoreMap":260},[264,802,803],{"class":266,"line":267},[264,804,805],{"class":295},"; Load 5 column bytes from font table\n",[264,807,808,810,813,816,818,821],{"class":266,"line":277},[264,809,281],{"class":280},[264,811,812],{"class":284}," C",[264,814,815],{"class":273},",(",[264,817,662],{"class":284},[264,819,820],{"class":273},")            ",[264,822,823],{"class":295},"; Column 1\n",[264,825,826,828],{"class":266,"line":299},[264,827,400],{"class":280},[264,829,599],{"class":284},[264,831,832,834,837,839,841,843],{"class":266,"line":316},[264,833,281],{"class":280},[264,835,836],{"class":284}," B",[264,838,815],{"class":273},[264,840,662],{"class":284},[264,842,820],{"class":273},[264,844,845],{"class":295},"; Column 2 → H\n",[264,847,848,850],{"class":266,"line":325},[264,849,400],{"class":280},[264,851,599],{"class":284},[264,853,854,856,858,860,862,864],{"class":266,"line":340},[264,855,281],{"class":280},[264,857,642],{"class":284},[264,859,815],{"class":273},[264,861,662],{"class":284},[264,863,820],{"class":273},[264,865,866],{"class":295},"; Column 3\n",[264,868,869,871],{"class":266,"line":352},[264,870,400],{"class":280},[264,872,599],{"class":284},[264,874,875,877,879,881,883,885],{"class":266,"line":360},[264,876,281],{"class":280},[264,878,630],{"class":284},[264,880,815],{"class":273},[264,882,662],{"class":284},[264,884,820],{"class":273},[264,886,887],{"class":295},"; Column 4\n",[264,889,890,892],{"class":266,"line":371},[264,891,400],{"class":280},[264,893,599],{"class":284},[264,895,896,898,900,902,904,906],{"class":266,"line":382},[264,897,281],{"class":280},[264,899,346],{"class":284},[264,901,815],{"class":273},[264,903,662],{"class":284},[264,905,820],{"class":273},[264,907,908],{"class":295},"; Column 5\n",[264,910,911,913,915,917,920],{"class":266,"line":397},[264,912,281],{"class":280},[264,914,330],{"class":284},[264,916,288],{"class":273},[264,918,919],{"class":284},"B",[264,921,922],{"class":295},"               ; H = Column 2\n",[264,924,925,927,929,931,933],{"class":266,"line":408},[264,926,281],{"class":280},[264,928,836],{"class":284},[264,930,288],{"class":273},[264,932,89],{"class":291},[264,934,935],{"class":295},"             ; 8 rows to generate\n",[264,937,938],{"class":266,"line":416},[264,939,941],{"emptyLinePlaceholder":940},true,"\n",[264,943,944,947],{"class":266,"line":431},[264,945,946],{"class":270},".transpose_loop",[264,948,274],{"class":273},[264,950,951,954,956],{"class":266,"line":446},[264,952,953],{"class":280},"    XOR",[264,955,285],{"class":284},[264,957,958],{"class":295},"                ; Clear accumulator\n",[264,960,961,964,966],{"class":266,"line":454},[264,962,963],{"class":280},"    RR",[264,965,812],{"class":284},[264,967,968],{"class":295},"                 ; Rotate column 1, bit → carry\n",[264,970,971,974],{"class":266,"line":468},[264,972,973],{"class":280},"    RLA",[264,975,976],{"class":295},"                  ; Carry → A bit 0\n",[264,978,979,981,983],{"class":266,"line":476},[264,980,963],{"class":280},[264,982,330],{"class":284},[264,984,985],{"class":295},"                 ; Rotate column 2\n",[264,987,988,990],{"class":266,"line":486},[264,989,973],{"class":280},[264,991,992],{"class":295},"                  ; → A bit 1\n",[264,994,995,997,999],{"class":266,"line":496},[264,996,963],{"class":280},[264,998,642],{"class":284},[264,1000,1001],{"class":295},"                 ; Rotate column 3\n",[264,1003,1004,1006],{"class":266,"line":510},[264,1005,973],{"class":280},[264,1007,1008],{"class":295},"                  ; → A bit 2\n",[264,1010,1011,1013,1015],{"class":266,"line":518},[264,1012,963],{"class":280},[264,1014,630],{"class":284},[264,1016,1017],{"class":295},"                 ; Rotate column 4\n",[264,1019,1020,1022],{"class":266,"line":542},[264,1021,973],{"class":280},[264,1023,1024],{"class":295},"                  ; → A bit 3\n",[264,1026,1027,1029,1031],{"class":266,"line":562},[264,1028,963],{"class":280},[264,1030,346],{"class":284},[264,1032,1033],{"class":295},"                 ; Rotate column 5\n",[264,1035,1037,1039],{"class":266,"line":1036},25,[264,1038,973],{"class":280},[264,1040,1041],{"class":295},"                  ; → A bit 4\n",[264,1043,1045],{"class":266,"line":1044},26,[264,1046,1047],{"class":280},"    EXX\n",[264,1049,1051,1053,1055,1058,1060,1062],{"class":266,"line":1050},27,[264,1052,281],{"class":280},[264,1054,523],{"class":273},[264,1056,1057],{"class":284},"BC",[264,1059,534],{"class":273},[264,1061,238],{"class":284},[264,1063,1064],{"class":295},"            ; Store transposed row byte\n",[264,1066,1068,1070],{"class":266,"line":1067},28,[264,1069,400],{"class":280},[264,1071,1072],{"class":284}," BC\n",[264,1074,1076],{"class":266,"line":1075},29,[264,1077,1047],{"class":280},[264,1079,1081,1084,1087],{"class":266,"line":1080},30,[264,1082,1083],{"class":280},"    DJNZ",[264,1085,1086],{"class":273}," .transpose_loop ",[264,1088,1089],{"class":295},"; Repeat for all 8 rows\n",[10,1091,1092,1093,1096,1097,1099,1100,1102,1103,1105,1106,1109,1110,1113,1114,1116,1117,1120,1121,1123],{},"Each iteration extracts one bit from each of the 5 column registers (using RR to shift each column right, moving the bottom bit into carry) and assembles them into a row byte (using RLA to shift carry into the accumulator). After 8 iterations, the 5 column bytes have been transposed into 8 row bytes. ",[28,1094,1095],{},"EXX"," swaps the main and alternate ",[28,1098,1057],{},", ",[28,1101,689],{},", and ",[28,1104,662],{}," pairs; the listing omits the setup that seeds the write address, so it is easy to misread ",[28,1107,1108],{},"LD (BC),A"," \u002F ",[28,1111,1112],{},"INC BC"," as sharing the same ",[28,1115,919],{}," as the ",[28,1118,1119],{},"DJNZ"," row counter — they do not, because the pointer and the count are kept on opposite sides of each ",[28,1122,1095],{},". The result is stored in a work area within the IX state block.",[10,1125,1126],{},"Storing the font in column format saves ROM space — 5 bytes per character instead of 8 — which adds up to 288 bytes saved across 96 characters. In an 8K ROM where every byte counts, that's a meaningful saving, even at the cost of runtime transposition.",[20,1128,1130],{"id":1129},"double-height-characters","Double-Height Characters",[10,1132,1133,1134,1136],{},"Viewdata supports double-height text for headlines and emphasis. When the double-height flag is set (bit 2 of the attribute state), the ",[28,1135,791],{}," routine stretches the transposed font by duplicating each row:",[255,1138,1140],{"className":257,"code":1139,"language":259,"meta":260,"style":260},".check_double_height:\n    BIT 2,L              ; Double-height flag?\n    JR Z,.font_done\n    ; Stretch: copy 8 rows to 16 by duplicating each\n    DEC BC\n    LD E,C\n    LD D,B\n    LD HL,$0008\n    ADD HL,BC\n    EX DE,HL\n    LD BC,$0010          ; 16 bytes (8 rows × 2)\n.stretch_loop:\n    LDD                  ; Copy byte backward\n    INC HL\n    LDD                  ; Same byte again (doubled)\n    JP PE,.stretch_loop\n",[28,1141,1142,1149,1163,1170,1175,1181,1192,1203,1214,1225,1238,1253,1260,1268,1274,1281],{"__ignoreMap":260},[264,1143,1144,1147],{"class":266,"line":267},[264,1145,1146],{"class":270},".check_double_height",[264,1148,274],{"class":273},[264,1150,1151,1153,1156,1158,1160],{"class":266,"line":277},[264,1152,302],{"class":280},[264,1154,1155],{"class":291}," 2",[264,1157,288],{"class":273},[264,1159,440],{"class":284},[264,1161,1162],{"class":295},"              ; Double-height flag?\n",[264,1164,1165,1167],{"class":266,"line":299},[264,1166,319],{"class":280},[264,1168,1169],{"class":273}," Z,.font_done\n",[264,1171,1172],{"class":266,"line":316},[264,1173,1174],{"class":295},"    ; Stretch: copy 8 rows to 16 by duplicating each\n",[264,1176,1177,1179],{"class":266,"line":325},[264,1178,343],{"class":280},[264,1180,1072],{"class":284},[264,1182,1183,1185,1187,1189],{"class":266,"line":340},[264,1184,281],{"class":280},[264,1186,642],{"class":284},[264,1188,288],{"class":273},[264,1190,1191],{"class":284},"C\n",[264,1193,1194,1196,1198,1200],{"class":266,"line":352},[264,1195,281],{"class":280},[264,1197,630],{"class":284},[264,1199,288],{"class":273},[264,1201,1202],{"class":284},"B\n",[264,1204,1205,1207,1209,1211],{"class":266,"line":360},[264,1206,281],{"class":280},[264,1208,657],{"class":284},[264,1210,288],{"class":273},[264,1212,1213],{"class":291},"$0008\n",[264,1215,1216,1218,1220,1222],{"class":266,"line":371},[264,1217,654],{"class":280},[264,1219,657],{"class":284},[264,1221,288],{"class":273},[264,1223,1224],{"class":284},"BC\n",[264,1226,1227,1230,1233,1235],{"class":266,"line":382},[264,1228,1229],{"class":280},"    EX",[264,1231,1232],{"class":284}," DE",[264,1234,288],{"class":273},[264,1236,1237],{"class":284},"HL\n",[264,1239,1240,1242,1245,1247,1250],{"class":266,"line":397},[264,1241,281],{"class":280},[264,1243,1244],{"class":284}," BC",[264,1246,288],{"class":273},[264,1248,1249],{"class":291},"$0010",[264,1251,1252],{"class":295},"          ; 16 bytes (8 rows × 2)\n",[264,1254,1255,1258],{"class":266,"line":408},[264,1256,1257],{"class":270},".stretch_loop",[264,1259,274],{"class":273},[264,1261,1262,1265],{"class":266,"line":416},[264,1263,1264],{"class":280},"    LDD",[264,1266,1267],{"class":295},"                  ; Copy byte backward\n",[264,1269,1270,1272],{"class":266,"line":431},[264,1271,400],{"class":280},[264,1273,599],{"class":284},[264,1275,1276,1278],{"class":266,"line":446},[264,1277,1264],{"class":280},[264,1279,1280],{"class":295},"                  ; Same byte again (doubled)\n",[264,1282,1283,1286,1289],{"class":266,"line":454},[264,1284,1285],{"class":280},"    JP",[264,1287,1288],{"class":284}," PE",[264,1290,1291],{"class":273},",.stretch_loop\n",[10,1293,1294,1295,1298,1299,1301,1302,1305],{},"This uses LDD (load-and-decrement) going backward through the work area, duplicating each byte. Each ",[28,1296,1297],{},"LDD"," decrements BC; the Zilog manual defines the P\u002FV flag after ",[28,1300,1297],{}," as set when BC is still non-zero after that decrement. ",[28,1303,1304],{},"JP PE"," tests P\u002FV, so the loop continues until BC reaches zero. The result is each pixel row appearing twice, doubling the character's height.",[20,1307,1309],{"id":1308},"mosaic-graphics","Mosaic Graphics",[10,1311,1312,1313,1315],{},"Viewdata's distinctive blocky graphics use mosaic characters — a 2×3 grid where each of the 6 cells can be on or off, encoded in bits 0-5 of the character byte. The ",[28,1314,791],{}," routine handles these and thus avoids having to store another 320 bytes of 64 block-drawing characters glyph data.",[255,1317,1319],{"className":257,"code":1318,"language":259,"meta":260,"style":260},".mosaic_char:\n    LD C,A\n    RRCA\n    OR $1F               ; Set up bit mask\n    AND C\n    LD C,A\n    LD DE,$3807          ; Pixel patterns for cells\n    BIT 0,L              ; Check row flag\n    JR Z,.mosaic_even\n    LD DE,$1803          ; Alternate patterns\n.mosaic_even:\n    LD B,$03             ; 3 pairs of rows\n.mosaic_row_loop:\n    XOR A\n    RR C                 ; Left cell bit → carry\n    JR NC,.left_off\n    OR D                 ; Left cell on: OR in left pattern\n.left_off:\n    RR C                 ; Right cell bit → carry\n    JR NC,.right_off\n    OR E                 ; Right cell on: OR in right pattern\n.right_off:\n    ; ... store row bytes in work area ...\n    DJNZ .mosaic_row_loop\n",[28,1320,1321,1328,1339,1344,1355,1363,1373,1387,1401,1408,1422,1429,1443,1450,1457,1466,1475,1484,1491,1500,1509,1518,1525,1530],{"__ignoreMap":260},[264,1322,1323,1326],{"class":266,"line":267},[264,1324,1325],{"class":270},".mosaic_char",[264,1327,274],{"class":273},[264,1329,1330,1332,1334,1336],{"class":266,"line":277},[264,1331,281],{"class":280},[264,1333,812],{"class":284},[264,1335,288],{"class":273},[264,1337,1338],{"class":284},"A\n",[264,1340,1341],{"class":266,"line":299},[264,1342,1343],{"class":280},"    RRCA\n",[264,1345,1346,1349,1352],{"class":266,"line":316},[264,1347,1348],{"class":280},"    OR",[264,1350,1351],{"class":291}," $1F",[264,1353,1354],{"class":295},"               ; Set up bit mask\n",[264,1356,1357,1360],{"class":266,"line":325},[264,1358,1359],{"class":280},"    AND",[264,1361,1362],{"class":284}," C\n",[264,1364,1365,1367,1369,1371],{"class":266,"line":340},[264,1366,281],{"class":280},[264,1368,812],{"class":284},[264,1370,288],{"class":273},[264,1372,1338],{"class":284},[264,1374,1375,1377,1379,1381,1384],{"class":266,"line":352},[264,1376,281],{"class":280},[264,1378,1232],{"class":284},[264,1380,288],{"class":273},[264,1382,1383],{"class":291},"$3807",[264,1385,1386],{"class":295},"          ; Pixel patterns for cells\n",[264,1388,1389,1391,1394,1396,1398],{"class":266,"line":360},[264,1390,302],{"class":280},[264,1392,1393],{"class":291}," 0",[264,1395,288],{"class":273},[264,1397,440],{"class":284},[264,1399,1400],{"class":295},"              ; Check row flag\n",[264,1402,1403,1405],{"class":266,"line":371},[264,1404,319],{"class":280},[264,1406,1407],{"class":273}," Z,.mosaic_even\n",[264,1409,1410,1412,1414,1416,1419],{"class":266,"line":382},[264,1411,281],{"class":280},[264,1413,1232],{"class":284},[264,1415,288],{"class":273},[264,1417,1418],{"class":291},"$1803",[264,1420,1421],{"class":295},"          ; Alternate patterns\n",[264,1423,1424,1427],{"class":266,"line":397},[264,1425,1426],{"class":270},".mosaic_even",[264,1428,274],{"class":273},[264,1430,1431,1433,1435,1437,1440],{"class":266,"line":408},[264,1432,281],{"class":280},[264,1434,836],{"class":284},[264,1436,288],{"class":273},[264,1438,1439],{"class":291},"$03",[264,1441,1442],{"class":295},"             ; 3 pairs of rows\n",[264,1444,1445,1448],{"class":266,"line":416},[264,1446,1447],{"class":270},".mosaic_row_loop",[264,1449,274],{"class":273},[264,1451,1452,1454],{"class":266,"line":431},[264,1453,953],{"class":280},[264,1455,1456],{"class":284}," A\n",[264,1458,1459,1461,1463],{"class":266,"line":446},[264,1460,963],{"class":280},[264,1462,812],{"class":284},[264,1464,1465],{"class":295},"                 ; Left cell bit → carry\n",[264,1467,1468,1470,1472],{"class":266,"line":454},[264,1469,319],{"class":280},[264,1471,376],{"class":284},[264,1473,1474],{"class":273},",.left_off\n",[264,1476,1477,1479,1481],{"class":266,"line":468},[264,1478,1348],{"class":280},[264,1480,630],{"class":284},[264,1482,1483],{"class":295},"                 ; Left cell on: OR in left pattern\n",[264,1485,1486,1489],{"class":266,"line":476},[264,1487,1488],{"class":270},".left_off",[264,1490,274],{"class":273},[264,1492,1493,1495,1497],{"class":266,"line":486},[264,1494,963],{"class":280},[264,1496,812],{"class":284},[264,1498,1499],{"class":295},"                 ; Right cell bit → carry\n",[264,1501,1502,1504,1506],{"class":266,"line":496},[264,1503,319],{"class":280},[264,1505,376],{"class":284},[264,1507,1508],{"class":273},",.right_off\n",[264,1510,1511,1513,1515],{"class":266,"line":510},[264,1512,1348],{"class":280},[264,1514,642],{"class":284},[264,1516,1517],{"class":295},"                 ; Right cell on: OR in right pattern\n",[264,1519,1520,1523],{"class":266,"line":518},[264,1521,1522],{"class":270},".right_off",[264,1524,274],{"class":273},[264,1526,1527],{"class":266,"line":542},[264,1528,1529],{"class":295},"    ; ... store row bytes in work area ...\n",[264,1531,1532,1534],{"class":266,"line":562},[264,1533,1083],{"class":280},[264,1535,1536],{"class":273}," .mosaic_row_loop\n",[10,1538,1539,1540,1543,1544,1547],{},"Each pair of rows in the 2×3 grid gets the same pixel pattern. The ",[28,1541,1542],{},"$38"," and ",[28,1545,1546],{},"$07"," values are the left and right halves of a character cell — three pixels each. The result is the chunky 2×3 block graphics that give Viewdata pages their distinctive look.",[20,1549,1551],{"id":1550},"screen-address-calculation","Screen Address Calculation",[10,1553,1554,1555,1558],{},"The Spectrum's screen memory layout is, as anyone who's even watched a SCREEN$ load, awkward.  The 6,144 bytes of bitmap data at ",[28,1556,1557],{},"$4000-$57FF"," aren't arranged in simple top-to-bottom order.",[10,1560,1561,1562,1565,1566,1569,1570,1573,1574,1577],{},"Instead, the screen is divided into three thirds (0-7, 8-15, 16-23 character rows), and within each third, pixel lines are interleaved. Row 0 line 0 is at ",[28,1563,1564],{},"$4000",", row 0 line 1 is at ",[28,1567,1568],{},"$4100",", row 0 line 2 is at ",[28,1571,1572],{},"$4200",", and so on — but row 1 line 0 is at ",[28,1575,1576],{},"$4020",".  A nice loading effect but for anyone coding on it a pain normally solved with a 192 or 384 byte Y lookup table.",[10,1579,1580,1581,576,1584,1587,1588,1591],{},"Alas the VTX ROM has no space for such a table and so the ",[28,1582,1583],{},"CALC_SCREEN_ADDR",[28,1585,1586],{},"$1907"," calculates the correct address for any character position on the fly. The attribute area is simple — 768 bytes at ",[28,1589,1590],{},"$5800-$5AFF"," in straightforward row-major order, one byte per character cell.",[255,1593,1595],{"className":257,"code":1594,"language":259,"meta":260,"style":260},"CALC_SCREEN_ADDR:\n    PUSH AF\n    PUSH HL\n    LD A,H                    ; Get row\n    ADD A,A                   ; ×2\n    ADD A,H                   ; ×3\n    LD H,A\n    AND $03                   ; Extract pixel line within character\n    XOR $03                   ; Invert (Spectrum screen is upside-down within chars)\n    LD B,A\n    LD A,H\n    LD H,$00\n    ADD HL,HL                 ; ×2\n    ADD HL,HL                 ; ×4\n    ADD HL,HL                 ; ×8\n    ADD HL,HL                 ; ×16\n    ADD HL,HL                 ; ×32 (32 bytes per character row)\n    LD DE,$5800               ; Attribute file base\n    ADD HL,DE                 ; HL = attribute address\n    RRA                       ; Calculate screen row\n    OR A\n    RRA\n    ADD A,L\n    LD L,A\n    EX DE,HL                  ; DE = attribute address\n    POP HL\n    POP AF\n    RET\n",[28,1596,1597,1603,1610,1616,1629,1642,1655,1665,1675,1684,1694,1704,1714,1727,1740,1753,1766,1779,1793,1806,1814,1820,1825,1836,1846,1859,1866,1872],{"__ignoreMap":260},[264,1598,1599,1601],{"class":266,"line":267},[264,1600,1583],{"class":270},[264,1602,274],{"class":273},[264,1604,1605,1607],{"class":266,"line":277},[264,1606,596],{"class":280},[264,1608,1609],{"class":284}," AF\n",[264,1611,1612,1614],{"class":266,"line":299},[264,1613,596],{"class":280},[264,1615,599],{"class":284},[264,1617,1618,1620,1622,1624,1626],{"class":266,"line":316},[264,1619,281],{"class":280},[264,1621,285],{"class":284},[264,1623,288],{"class":273},[264,1625,310],{"class":284},[264,1627,1628],{"class":295},"                    ; Get row\n",[264,1630,1631,1633,1635,1637,1639],{"class":266,"line":325},[264,1632,654],{"class":280},[264,1634,285],{"class":284},[264,1636,288],{"class":273},[264,1638,238],{"class":284},[264,1640,1641],{"class":295},"                   ; ×2\n",[264,1643,1644,1646,1648,1650,1652],{"class":266,"line":340},[264,1645,654],{"class":280},[264,1647,285],{"class":284},[264,1649,288],{"class":273},[264,1651,310],{"class":284},[264,1653,1654],{"class":295},"                   ; ×3\n",[264,1656,1657,1659,1661,1663],{"class":266,"line":352},[264,1658,281],{"class":280},[264,1660,330],{"class":284},[264,1662,288],{"class":273},[264,1664,1338],{"class":284},[264,1666,1667,1669,1672],{"class":266,"line":360},[264,1668,1359],{"class":280},[264,1670,1671],{"class":291}," $03",[264,1673,1674],{"class":295},"                   ; Extract pixel line within character\n",[264,1676,1677,1679,1681],{"class":266,"line":371},[264,1678,953],{"class":280},[264,1680,1671],{"class":291},[264,1682,1683],{"class":295},"                   ; Invert (Spectrum screen is upside-down within chars)\n",[264,1685,1686,1688,1690,1692],{"class":266,"line":382},[264,1687,281],{"class":280},[264,1689,836],{"class":284},[264,1691,288],{"class":273},[264,1693,1338],{"class":284},[264,1695,1696,1698,1700,1702],{"class":266,"line":397},[264,1697,281],{"class":280},[264,1699,285],{"class":284},[264,1701,288],{"class":273},[264,1703,635],{"class":284},[264,1705,1706,1708,1710,1712],{"class":266,"line":408},[264,1707,281],{"class":280},[264,1709,330],{"class":284},[264,1711,288],{"class":273},[264,1713,623],{"class":291},[264,1715,1716,1718,1720,1722,1724],{"class":266,"line":416},[264,1717,654],{"class":280},[264,1719,657],{"class":284},[264,1721,288],{"class":273},[264,1723,662],{"class":284},[264,1725,1726],{"class":295},"                 ; ×2\n",[264,1728,1729,1731,1733,1735,1737],{"class":266,"line":431},[264,1730,654],{"class":280},[264,1732,657],{"class":284},[264,1734,288],{"class":273},[264,1736,662],{"class":284},[264,1738,1739],{"class":295},"                 ; ×4\n",[264,1741,1742,1744,1746,1748,1750],{"class":266,"line":446},[264,1743,654],{"class":280},[264,1745,657],{"class":284},[264,1747,288],{"class":273},[264,1749,662],{"class":284},[264,1751,1752],{"class":295},"                 ; ×8\n",[264,1754,1755,1757,1759,1761,1763],{"class":266,"line":454},[264,1756,654],{"class":280},[264,1758,657],{"class":284},[264,1760,288],{"class":273},[264,1762,662],{"class":284},[264,1764,1765],{"class":295},"                 ; ×16\n",[264,1767,1768,1770,1772,1774,1776],{"class":266,"line":468},[264,1769,654],{"class":280},[264,1771,657],{"class":284},[264,1773,288],{"class":273},[264,1775,662],{"class":284},[264,1777,1778],{"class":295},"                 ; ×32 (32 bytes per character row)\n",[264,1780,1781,1783,1785,1787,1790],{"class":266,"line":476},[264,1782,281],{"class":280},[264,1784,1232],{"class":284},[264,1786,288],{"class":273},[264,1788,1789],{"class":291},"$5800",[264,1791,1792],{"class":295},"               ; Attribute file base\n",[264,1794,1795,1797,1799,1801,1803],{"class":266,"line":486},[264,1796,654],{"class":280},[264,1798,657],{"class":284},[264,1800,288],{"class":273},[264,1802,689],{"class":284},[264,1804,1805],{"class":295},"                 ; HL = attribute address\n",[264,1807,1808,1811],{"class":266,"line":496},[264,1809,1810],{"class":280},"    RRA",[264,1812,1813],{"class":295},"                       ; Calculate screen row\n",[264,1815,1816,1818],{"class":266,"line":510},[264,1817,1348],{"class":280},[264,1819,1456],{"class":284},[264,1821,1822],{"class":266,"line":518},[264,1823,1824],{"class":280},"    RRA\n",[264,1826,1827,1829,1831,1833],{"class":266,"line":542},[264,1828,654],{"class":280},[264,1830,285],{"class":284},[264,1832,288],{"class":273},[264,1834,1835],{"class":284},"L\n",[264,1837,1838,1840,1842,1844],{"class":266,"line":562},[264,1839,281],{"class":280},[264,1841,346],{"class":284},[264,1843,288],{"class":273},[264,1845,1338],{"class":284},[264,1847,1848,1850,1852,1854,1856],{"class":266,"line":1036},[264,1849,1229],{"class":280},[264,1851,1232],{"class":284},[264,1853,288],{"class":273},[264,1855,662],{"class":284},[264,1857,1858],{"class":295},"                  ; DE = attribute address\n",[264,1860,1861,1864],{"class":266,"line":1044},[264,1862,1863],{"class":280},"    POP",[264,1865,599],{"class":284},[264,1867,1868,1870],{"class":266,"line":1050},[264,1869,1863],{"class":280},[264,1871,1609],{"class":284},[264,1873,1874],{"class":266,"line":1067},[264,1875,565],{"class":280},[10,1877,1878],{},"So taking in H as the ROW from 0-23 we get back the screen address in DE and the attribute address in alternate HL.",[20,1880,1882],{"id":1881},"the-glyph-renderer","The Glyph Renderer",[10,1884,1885,1886,31,1889,1892],{},"Once the font has been transposed and the start screen row address calculated, ",[28,1887,1888],{},"RENDER_GLYPH",[28,1890,1891],{},"$194B"," writes the actual pixels. The font data needs to be positioned at the correct column within the screen byte (since the Spectrum's screen bytes span 8 pixels, and Viewdata characters are only 5 pixels wide at arbitrary positions).",[10,1894,1895,1896,1899],{},"The renderer uses a jump table (",[28,1897,1898],{},"RENDER_TABLE",") indexed by column position. Each entry is a sequence of RLCA instructions that shift the font byte to the correct bit position, followed by masking logic:",[255,1901,1903],{"className":257,"code":1902,"language":259,"meta":260,"style":260},"    LD C,A               ; Save shifted byte\n    AND D                ; Mask with column pattern\n    LD B,A\n    LD A,D\n    CPL                  ; Invert mask\n    AND (HL)             ; Clear old pixels in screen byte\n    OR B                 ; OR in new pixels\n    LD (HL),A            ; Write to screen\n",[28,1904,1905,1918,1927,1937,1948,1956,1970,1979],{"__ignoreMap":260},[264,1906,1907,1909,1911,1913,1915],{"class":266,"line":267},[264,1908,281],{"class":280},[264,1910,812],{"class":284},[264,1912,288],{"class":273},[264,1914,238],{"class":284},[264,1916,1917],{"class":295},"               ; Save shifted byte\n",[264,1919,1920,1922,1924],{"class":266,"line":277},[264,1921,1359],{"class":280},[264,1923,630],{"class":284},[264,1925,1926],{"class":295},"                ; Mask with column pattern\n",[264,1928,1929,1931,1933,1935],{"class":266,"line":299},[264,1930,281],{"class":280},[264,1932,836],{"class":284},[264,1934,288],{"class":273},[264,1936,1338],{"class":284},[264,1938,1939,1941,1943,1945],{"class":266,"line":316},[264,1940,281],{"class":280},[264,1942,285],{"class":284},[264,1944,288],{"class":273},[264,1946,1947],{"class":284},"D\n",[264,1949,1950,1953],{"class":266,"line":325},[264,1951,1952],{"class":280},"    CPL",[264,1954,1955],{"class":295},"                  ; Invert mask\n",[264,1957,1958,1960,1962,1964,1967],{"class":266,"line":340},[264,1959,1359],{"class":280},[264,1961,523],{"class":273},[264,1963,662],{"class":284},[264,1965,1966],{"class":273},")             ",[264,1968,1969],{"class":295},"; Clear old pixels in screen byte\n",[264,1971,1972,1974,1976],{"class":266,"line":352},[264,1973,1348],{"class":280},[264,1975,836],{"class":284},[264,1977,1978],{"class":295},"                 ; OR in new pixels\n",[264,1980,1981,1983,1985,1987,1989,1991],{"class":266,"line":360},[264,1982,281],{"class":280},[264,1984,523],{"class":273},[264,1986,662],{"class":284},[264,1988,534],{"class":273},[264,1990,238],{"class":284},[264,1992,1993],{"class":295},"            ; Write to screen\n",[10,1995,1996,1997,1999],{},"When a character spans two screen bytes (which happens when it doesn't align to a byte boundary), the routine writes the overflow into the adjacent byte using a second mask. The cursor blink also uses this renderer — alternating between the character at the cursor position and a solid block (",[28,1998,42],{},", the full block character) every few frames.",[10,2001,2002],{},"Phew.",[20,2004,2006],{"id":2005},"the-prestel-protocol-parser","The Prestel Protocol Parser",[10,2008,2009,2010,576,2013,2016],{},"The ",[28,2011,2012],{},"PRESTEL_PARSE",[28,2014,2015],{},"$1265"," handles the framing protocol used by the Prestel service for downloading telesoftware and multi-page data. This is separate from the Viewdata display protocol — it's a higher-level protocol for reliable data transfer over the Viewdata connection.",[10,2018,2019,2020,2023,2024,2027],{},"Prestel frames are delimited by pipe characters: ",[28,2021,2022],{},"|A"," marks the start of a frame, ",[28,2025,2026],{},"|Z"," marks the end. Between these markers, the protocol supports:",[48,2029,2030,2040],{},[51,2031,2032],{},[54,2033,2034,2037],{},[57,2035,2036],{},"Sequence",[57,2038,2039],{},"Meaning",[67,2041,2042,2051,2060,2070,2080,2090,2100],{},[54,2043,2044,2048],{},[72,2045,2046],{},[28,2047,2022],{},[72,2049,2050],{},"Frame start",[54,2052,2053,2057],{},[72,2054,2055],{},[28,2056,2026],{},[72,2058,2059],{},"Frame end (followed by 3-digit checksum)",[54,2061,2062,2067],{},[72,2063,2064],{},[28,2065,2066],{},"|F",[72,2068,2069],{},"Frame complete (all data received)",[54,2071,2072,2077],{},[72,2073,2074],{},[28,2075,2076],{},"|L",[72,2078,2079],{},"Length\u002Frepeat count (followed by 3-digit number)",[54,2081,2082,2087],{},[72,2083,2084],{},[28,2085,2086],{},"|I",[72,2088,2089],{},"Ignore toggle (enable\u002Fdisable data storage)",[54,2091,2092,2097],{},[72,2093,2094],{},[28,2095,2096],{},"|E",[72,2098,2099],{},"Escape (literal pipe character)",[54,2101,2102,2110],{},[72,2103,2104,39,2107],{},[28,2105,2106],{},"|0",[28,2108,2109],{},"|5",[72,2111,2112],{},"Shift state (character set switching)",[10,2114,2115,2116,2119],{},"We also have a checksum which is a running XOR of all received bytes, verified against a 3-digit decimal-as-ASCII value XORed with ",[28,2117,2118],{},"$26",":",[255,2121,2123],{"className":257,"code":2122,"language":259,"meta":260,"style":260},".verify_checksum:\n    PUSH BC\n    CALL READ_3_DIGITS   ; Read 3-digit checksum from stream\n    POP BC\n    XOR $26              ; XOR with checksum key\n    CP C                 ; Compare with accumulated checksum\n    JR NZ,PARSE_ERROR    ; Mismatch: error\n    SET 2,E              ; Data is valid\n",[28,2124,2125,2132,2138,2149,2155,2165,2174,2187],{"__ignoreMap":260},[264,2126,2127,2130],{"class":266,"line":267},[264,2128,2129],{"class":270},".verify_checksum",[264,2131,274],{"class":273},[264,2133,2134,2136],{"class":266,"line":277},[264,2135,596],{"class":280},[264,2137,1072],{"class":284},[264,2139,2140,2143,2146],{"class":266,"line":299},[264,2141,2142],{"class":280},"    CALL",[264,2144,2145],{"class":273}," READ_3_DIGITS   ",[264,2147,2148],{"class":295},"; Read 3-digit checksum from stream\n",[264,2150,2151,2153],{"class":266,"line":316},[264,2152,1863],{"class":280},[264,2154,1072],{"class":284},[264,2156,2157,2159,2162],{"class":266,"line":325},[264,2158,953],{"class":280},[264,2160,2161],{"class":291}," $26",[264,2163,2164],{"class":295},"              ; XOR with checksum key\n",[264,2166,2167,2169,2171],{"class":266,"line":340},[264,2168,363],{"class":280},[264,2170,812],{"class":284},[264,2172,2173],{"class":295},"                 ; Compare with accumulated checksum\n",[264,2175,2176,2178,2181,2184],{"class":266,"line":352},[264,2177,319],{"class":280},[264,2179,2180],{"class":284}," NZ",[264,2182,2183],{"class":273},",PARSE_ERROR    ",[264,2185,2186],{"class":295},"; Mismatch: error\n",[264,2188,2189,2192,2194,2196,2199],{"class":266,"line":360},[264,2190,2191],{"class":280},"    SET",[264,2193,1155],{"class":291},[264,2195,288],{"class":273},[264,2197,2198],{"class":284},"E",[264,2200,2201],{"class":295},"              ; Data is valid\n",[10,2203,2009,2204,576,2207,2210],{},[28,2205,2206],{},"READ_3_DIGITS",[28,2208,2209],{},"$1375"," is a tidy multiply-and-accumulate: for each digit, it multiplies the running total by 10 (via shift-and-add: ×2, ×4, +original = ×5, then ×2 again = ×10) and adds the new digit.",[10,2212,2213,2214,31,2217,2220,2221,2223],{},"The frame detector (",[28,2215,2216],{},"PRESTEL_DETECT",[28,2218,2219],{},"$1AE3",") runs on every received character in the main loop, tracking pipe\u002Fescape state to detect frame boundaries. When a complete frame ending with ",[28,2222,2026],{}," is found, it sets return code 5 to signal the BASIC program.",[20,2225,2227],{"id":2226},"the-micronet-800-splash-screen","The MICRONET 800 Splash Screen",[10,2229,2230,2231,2234],{},"The last major data block in the ROM is the splash screen at ",[28,2232,2233],{},"$1D00-$1F82"," — a raw Viewdata frame that displays the MICRONET 800 logo on startup. It uses mosaic graphics characters to build up the logo in multiple colours, with the text \"MICRONET 800 (C)\", \"Ver 3.1\", and \"10\u002F08\u002F83\" on the header line, \"for the\" and \"ZX Spectrum\" in the body, and \"Press any key\" at the bottom.",[10,2236,2237,2238,2240,2241,2243,2244,2246,2247,2250,2251,2253,2254,2256,2257,2260],{},"The frame uses standard Viewdata control codes: ",[28,2239,1439],{}," for yellow text, ",[28,2242,1546],{}," for white, ",[28,2245,242],{}," for red, ",[28,2248,2249],{},"$04"," for blue, ",[28,2252,183],{}," for mosaic red, ",[28,2255,196],{}," for normal display, and ",[28,2258,2259],{},"$1D"," for new background colour.",[10,2262,2263],{},"It's a Viewdata page stored verbatim in the ROM — no compression, no procedural generation, just 643 bytes of raw frame data. Quite wasteful considering all the other savings that had to be made! We could have had a nice Y-address lookup table instead!",[10,2265,2266],{},"During start, this frame is copied to the display buffer and rendered using the same Viewdata engine that handles live Prestel pages.",[20,2268,2270],{"id":2269},"clearing-the-screen-a-real-time-constraint","Clearing the Screen: A Real-Time Constraint",[10,2272,2273,2274,576,2277,2280,2281,2119],{},"One last detail worth mentioning: the ",[28,2275,2276],{},"CLEAR_SCREEN",[28,2278,2279],{},"$1A9A"," doesn't just zero the screen memory in one go. It clears one third at a time (768 bytes each), and ",[782,2282,2283],{},"calls CHECK_RX_DATA between each third",[255,2285,2287],{"className":257,"code":2286,"language":259,"meta":260,"style":260},".clear_third:\n    LD BC,$0300          ; 768 bytes per third\n    LD (HL),$00\n    LDIR                 ; Clear bitmap\n    CALL CHECK_RX_DATA   ; Poll modem — don't miss incoming data!\n    LD A,$58\n    CP H\n    JR NZ,.clear_third   ; Next third\n",[28,2288,2289,2296,2310,2322,2330,2340,2351,2358],{"__ignoreMap":260},[264,2290,2291,2294],{"class":266,"line":267},[264,2292,2293],{"class":270},".clear_third",[264,2295,274],{"class":273},[264,2297,2298,2300,2302,2304,2307],{"class":266,"line":277},[264,2299,281],{"class":280},[264,2301,1244],{"class":284},[264,2303,288],{"class":273},[264,2305,2306],{"class":291},"$0300",[264,2308,2309],{"class":295},"          ; 768 bytes per third\n",[264,2311,2312,2314,2316,2318,2320],{"class":266,"line":299},[264,2313,281],{"class":280},[264,2315,523],{"class":273},[264,2317,662],{"class":284},[264,2319,534],{"class":273},[264,2321,623],{"class":291},[264,2323,2324,2327],{"class":266,"line":316},[264,2325,2326],{"class":280},"    LDIR",[264,2328,2329],{"class":295},"                 ; Clear bitmap\n",[264,2331,2332,2334,2337],{"class":266,"line":325},[264,2333,2142],{"class":280},[264,2335,2336],{"class":273}," CHECK_RX_DATA   ",[264,2338,2339],{"class":295},"; Poll modem — don't miss incoming data!\n",[264,2341,2342,2344,2346,2348],{"class":266,"line":340},[264,2343,281],{"class":280},[264,2345,285],{"class":284},[264,2347,288],{"class":273},[264,2349,2350],{"class":291},"$58\n",[264,2352,2353,2355],{"class":266,"line":352},[264,2354,363],{"class":280},[264,2356,2357],{"class":284}," H\n",[264,2359,2360,2362,2364,2367],{"class":266,"line":360},[264,2361,319],{"class":280},[264,2363,2180],{"class":284},[264,2365,2366],{"class":273},",.clear_third   ",[264,2368,2369],{"class":295},"; Next third\n",[10,2371,2372,2373,2376,2377,2380],{},"At 1200 baud, a character time is on the order of ~8.3 milliseconds (10 bits at 1200 baud). One ",[28,2374,2375],{},"LDIR"," clearing 768 bytes is roughly 768 × 21 T-states per iteration, or about ",[782,2378,2379],{},"4.6 ms"," at 3.5 MHz — still less than a character time, but close enough that polling the modem between thirds is a sensible safeguard. That way, even during screen clears, no incoming data is lost. This kind of attention to real-time constraints throughout the code is what makes 1200 baud work reliably on a machine with no hardware flow control.",[20,2382,2384],{"id":2383},"wrapping-up","Wrapping Up",[10,2386,2387],{},"That's the complete VTX5000 ROM — 8,192 bytes covering ROM paging, a BASIC terminal application, modem I\u002FO with ring buffers, keyboard scanning, a Viewdata rendering engine with font transposition and mosaic graphics, a Prestel protocol parser, and a splash screen. All of it running on a Z80 at 3.5MHz with no operating system, no hardware abstraction, and little room to spare not helped by support for 16KB machines.",[10,2389,2390],{},"The disassembly reveals that the team (RT and DS at Scicon Ltd) understood the constraints with tricks from repurposing RTS for ROM paging to column-format fonts to polling between screen clears show they understood what needed to be done to work with the limited situation they had.",[10,2392,2393],{},"This kind of optimization is kind of a lost art today partly because our systems have more than enough capacity and partly because its hard to get right but it is interesting to think again about how code used to be hand-optimized in this way!",[10,2395,2396],{},"We carry on with rendering in part 5!",[2398,2399,2400],"style",{},"html pre.shiki code .sSKRk, html code.shiki .sSKRk{--shiki-default:#35A77C;--shiki-dark:#F8F8F2}html pre.shiki code .s6Vpi, html code.shiki .s6Vpi{--shiki-default:#5C6A72;--shiki-dark:#F8F8F2}html pre.shiki code .smiwp, html code.shiki .smiwp{--shiki-default:#F85552;--shiki-dark:#FF79C6}html pre.shiki code .stJs5, html code.shiki .stJs5{--shiki-default:#5C6A72;--shiki-default-font-style:inherit;--shiki-dark:#BD93F9;--shiki-dark-font-style:italic}html pre.shiki code .s3Ipq, html code.shiki .s3Ipq{--shiki-default:#DF69BA;--shiki-dark:#BD93F9}html pre.shiki code .sSX4p, html code.shiki .sSX4p{--shiki-default:#939F91;--shiki-default-font-style:italic;--shiki-dark:#6272A4;--shiki-dark-font-style:inherit}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":260,"searchDepth":277,"depth":277,"links":2402},[2403,2404,2405,2406,2407,2408,2409,2410,2411,2412,2413],{"id":22,"depth":299,"text":23},{"id":568,"depth":299,"text":569},{"id":768,"depth":299,"text":769},{"id":1129,"depth":299,"text":1130},{"id":1308,"depth":299,"text":1309},{"id":1550,"depth":299,"text":1551},{"id":1881,"depth":299,"text":1882},{"id":2005,"depth":299,"text":2006},{"id":2226,"depth":299,"text":2227},{"id":2269,"depth":299,"text":2270},{"id":2383,"depth":299,"text":2384},"Technology",0,"2026-05-31T06:23:29.958Z","In part 4 we followed data to and from the TI modem through the Intel 8251 USART landing in our buffers.  Now comes the interesting part - Rendering those Viewdata 40×24 frames on what is typically a 32×40 display.","[object Object]","md",null,false,{"src":2423,"alt":2424},"https:\u002F\u002Fimg.damieng.com\u002F32e5daaa-2402-4878-983d-f12643efbdb9-Micronet800Spectrum.webp","Micronet 800 Splash Screen",{},"\u002Fblog\u002F2026\u002Fvtx5000-part-5-rendering",{"title":5,"description":2417},"blog\u002F2026\u002Fvtx5000-part-5-rendering",[2430,2431,2432],"ZX Spectrum","vintage computing","Z80","\u002Fblog\u002F2026\u002Fvtx5000-part-5-rendering\u002F",2883,"ALZ3g58HKc09VsBE_mT6c1HtobbH12tuAUlFsGEI-O8",[2437,2438,2441],{"title":5,"date":2416,"url":2433},{"title":2439,"date":2440,"url":16},"VTX5000: Part 4 - Communications","2026-05-06T10:19:24.727Z",{"title":2442,"date":2443,"url":2444},"VTX5000: Part 2 - Hardware","2026-03-30T23:00:00.000Z","\u002Fblog\u002F2026\u002Fprism-vtx5000-part-2\u002F",[],1780265895747]