Skip to navigation

Version analysis of LOIN (Part 2 of 7) / LOINQ (Part 2 of 7)

This code appears in the following versions (click to see it in the source code):

Code variations between these versions are shown below.

Code variation 1 of 6A variation in the comments only

Tap on a block to expand it, and tap it again to revert.

Name: LOIN (Part 2 of 7)
Name: LOINQ (Part 2 of 7)
Type: Subroutine Category: Drawing lines Summary: Draw a line: Line has a shallow gradient, step right along x-axis Deep dive: Elite's line-drawing algorithm
This routine draws a line from (X1, Y1) to (X2, Y2). It has multiple stages. If we get here, then: * |delta_y| < |delta_x| * The line is closer to being horizontal than vertical * We are going to step right along the x-axis * We potentially swap coordinates to make sure X1 < X2
.STPX LDX X1 \ Set X = X1 CPX X2 \ If X1 < X2, jump down to LI3, as the coordinates are BCC LI3 \ already in the order that we want DEC SWAP \ Otherwise decrement SWAP from 0 to &FF, to denote that \ we are swapping the coordinates around LDA X2 \ Swap the values of X1 and X2 STA X1 STX X2 TAX \ Set X = X1 LDA Y2 \ Swap the values of Y1 and Y2 LDY Y1 STA Y1 STY Y2 .LI3 \ By this point we know the line is horizontal-ish and \ X1 < X2, so we're going from left to right as we go \ from X1 to X2

Code variation 2 of 6Related to the screen mode

Tap on a block to expand it, and tap it again to revert.

LDA Y1 \ Set A to the y-coordinate in Y1 LSR A \ Set A = A >> 3 LSR A \ = y div 8 LSR A \ \ So A now contains the number of the character row \ that will contain the start of the line ORA #&60 \ As A < 32, this effectively adds &60 to A, which gives \ us the screen address of the character row (as each \ character row takes up 256 bytes, and the first \ character row is at screen address &6000, or page &60) STA SCH \ Store the page number of the character row in SCH, so \ the high byte of SC is set correctly for drawing the \ start of our line
LDY Y1 \ Look up the page number of the character row that LDA ylookup,Y \ contains the pixel with the y-coordinate in Y1, and STA SC+1 \ store it in SC+1, so the high byte of SC is set \ correctly for drawing our line
IF _SNG45 OR _EXECUTIVE OR _SOURCE_DISC_BUILD LDY Y1 \ Look up the page number of the character row that LDA ylookup,Y \ contains the pixel with the y-coordinate in Y1, and STA SC+1 \ store it in SC+1, so the high byte of SC is set \ correctly for drawing our line ELIF _SOURCE_DISC_FILES LDA Y1 \ Fetch the y-coordinate into A LSR A \ Set A = A / 4, clear bit 0 and add &40, so this sets LSR A \ A to &40 + (Y1 / 4) rounded down down to a multiple LSR A \ of 2 ASL A ORA #&40 STA SC+1 \ Store the result in the high byte of SC(1 0) at SC+1, \ which sets SC(1 0) to the the page number of the \ character row that contains the pixel with the \ y-coordinate in Y1, as screen memory starts at &4000 \ and there are two pages (512 bytes) per pixel line in \ the custom screen mode used in the space view ENDIF
\ We now calculate the address of the character block \ containing the pixel (X1, Y1) and put it in SC(1 0), \ as follows: \ \ SC = &5800 + (Y1 div 8 * 256) + (Y1 div 8 * 64) + 32 LDA Y1 \ Set A to the y-coordinate in Y1 LSR A \ Set A = A >> 3 LSR A \ = y div 8 LSR A \ \ So A now contains the number of the character row \ that will contain the start of the line STA SC+1 \ Set SC+1 = A, so (SC+1 0) = A * 256 \ = char row * 256 LSR A \ Set (A SC) = (A SC) / 4 ROR SC \ = (4 * ((char row * 64) + 32)) / 4 LSR A \ = char row * 64 + 32 ROR SC ADC SC+1 \ Set SC(1 0) = (A SC) + (SC+1 0) + &5800 ADC #&58 \ = (char row * 64 + 32) STA SC+1 \ + char row * 256 \ + &5800 \ \ which is what we want, so SC(1 0) contains the address \ of the first visible pixel on the character row \ containing the point (X1, Y1) TXA \ Each character block contains 8 pixel rows, so to get AND #%11111000 \ the address of the first byte in the character block \ that we need to draw into, as an offset from the start \ of the row, we clear bits 0-2 ADC SC \ And add the result to SC(1 0) to get the character STA SC \ block on the row we want BCC P%+4 \ If the addition of the low bytes overflowed, increment INC SC+1 \ the high byte \ So SC(1 0) now contains the address of the first pixel \ in the character block containing the (X1, Y1), taking \ the screen borders into consideration

 LDA Y1                 \ Set Y = Y1 mod 8, which is the pixel row within the
 AND #7                 \ character block at which we want to draw the start of
 TAY                    \ our line (as each character block has 8 rows)

Code variation 3 of 6Related to the screen mode

This variation is blank in the Electron version.

Tap on a block to expand it, and tap it again to revert.

TXA \ Set A = bits 3-7 of X1 AND #%11111000
TXA \ Set A = 2 * bits 2-6 of X1 AND #%11111100 \ ASL A \ and shift bit 7 of X1 into the C flag

Code variation 4 of 6Related to the screen mode

This variation is blank in the Electron version.

STA SC \ Store this value in SC, so SC(1 0) now contains the \ screen address of the far left end (x-coordinate = 0) \ of the horizontal pixel row that we want to draw the \ start of our line on

Code variation 5 of 6Related to the screen mode

Tap on a block to expand it, and tap it again to revert.

TXA \ Set X = X1 mod 8, which is the horizontal pixel number AND #7 \ within the character block where the line starts (as TAX \ each pixel line in the character block is 8 pixels \ wide) LDA TWOS,X \ Fetch a one-pixel byte from TWOS where pixel X is set, STA R \ and store it in R
BCC P%+4 \ If bit 7 of X1 was set, so X1 > 127, increment the INC SC+1 \ high byte of SC(1 0) to point to the second page on \ this screen row, as this page contains the right half \ of the row TXA \ Set R = X1 mod 4, which is the horizontal pixel number AND #3 \ within the character block where the line starts (as STA R \ each pixel line in the character block is 4 pixels \ wide)

Code variation 6 of 6Other (e.g. bug fix, optimisation)

Part 2 of the LOIN routine in the advanced versions uses logarithms to speed up the multiplication, with the Master version omitting half of the logarithm algorithm when compared to the 6502SP version.

Tap on a block to expand it, and tap it again to revert.

\ The following calculates: \ \ Q = Q / P \ = |delta_y| / |delta_x| \ \ using the same shift-and-subtract algorithm that's \ documented in TIS2 LDA Q \ Set A = |delta_y| LDX #%11111110 \ Set Q to have bits 1-7 set, so we can rotate through 7 STX Q \ loop iterations, getting a 1 each time, and then \ getting a 0 on the 8th iteration... and we can also \ use Q to catch our result bits into bit 0 each time .LIL1 ASL A \ Shift A to the left BCS LI4 \ If bit 7 of A was set, then jump straight to the \ subtraction CMP P \ If A < P, skip the following subtraction BCC LI5 .LI4 SBC P \ A >= P, so set A = A - P SEC \ Set the C flag to rotate into the result in Q .LI5 ROL Q \ Rotate the counter in Q to the left, and catch the \ result bit into bit 0 (which will be a 0 if we didn't \ do the subtraction, or 1 if we did) BCS LIL1 \ If we still have set bits in Q, loop back to TIL2 to \ do the next iteration of 7 \ We now have: \ \ Q = A / P \ = |delta_y| / |delta_x| \ \ and the C flag is clear LDX P \ Set X = P + 1 INX \ = |delta_x| + 1 \ \ We add 1 so we can skip the first pixel plot if the \ line is being drawn with swapped coordinates LDA Y2 \ Set A = Y2 - Y1 - 1 (as the C flag is clear following SBC Y1 \ the above division) BCS DOWN \ If Y2 >= Y1 - 1 then jump to DOWN, as we need to draw \ the line to the right and down
\ The following section calculates: \ \ Q = Q / P \ = |delta_y| / |delta_x| \ \ using the log tables at logL and log to calculate: \ \ A = log(Q) - log(P) \ = log(|delta_y|) - log(|delta_x|) \ \ by first subtracting the low bytes of the logarithms \ from the table at LogL, and then subtracting the high \ bytes from the table at log, before applying the \ antilog to get the result of the division and putting \ it in Q LDX Q \ Set X = |delta_y| BEQ LIlog7 \ If |delta_y| = 0, jump to LIlog7 to return 0 as the \ result of the division LDA logL,X \ Set A = log(Q) - log(P) LDX P \ = log(|delta_y|) - log(|delta_x|) SEC \ SBC logL,X \ by first subtracting the low bytes of log(Q) - log(P) LDX Q \ And then subtracting the high bytes of log(Q) - log(P) LDA log,X \ so now A contains the high byte of log(Q) - log(P) LDX P SBC log,X BCS LIlog5 \ If the subtraction fitted into one byte and didn't \ underflow, then log(Q) - log(P) < 256, so we jump to \ LIlog5 to return a result of 255 TAX \ Otherwise we set A to the A-th entry from the antilog LDA alogh,X \ table so the result of the division is now in A JMP LIlog6 \ Jump to LIlog6 to return the result .LIlog5 LDA #255 \ The division is very close to 1, so set A to the BNE LIlog6 \ closest possible answer to 256, i.e. 255, and jump to \ LIlog6 to return the result (this BNE is effectively a \ JMP as A is never zero) .LIlog7 LDA #0 \ The numerator in the division is 0, so set A to 0 .LIlog6 STA Q \ Store the result of the division in Q, so we have: \ \ Q = |delta_y| / |delta_x| LDX P \ Set X = P \ = |delta_x| BEQ LIEXS \ If |delta_x| = 0, return from the subroutine, as LIEXS \ contains a BEQ LIEX instruction, and LIEX contains an \ RTS INX \ Set X = P + 1 \ = |delta_x| + 1 \ \ We add 1 so we can skip the first pixel plot if the \ line is being drawn with swapped coordinates LDA Y2 \ If Y2 < Y1 then skip the following instruction CMP Y1 BCC P%+5 JMP DOWN \ Y2 >= Y1, so jump to DOWN, as we need to draw the line \ to the right and down
IF _SNG45 OR _EXECUTIVE OR _SOURCE_DISC_BUILD \ The following section calculates: \ \ Q = Q / P \ = |delta_y| / |delta_x| \ \ using the log tables at logL and log to calculate: \ \ A = log(Q) - log(P) \ = log(|delta_y|) - log(|delta_x|) \ \ by first subtracting the low bytes of the logarithms \ from the table at LogL, and then subtracting the high \ bytes from the table at log, before applying the \ antilog to get the result of the division and putting \ it in Q LDX Q \ Set X = |delta_y| BEQ LIlog7 \ If |delta_y| = 0, jump to LIlog7 to return 0 as the \ result of the division LDA logL,X \ Set A = log(Q) - log(P) LDX P \ = log(|delta_y|) - log(|delta_x|) SEC \ SBC logL,X \ by first subtracting the low bytes of log(Q) - log(P) BMI LIlog4 \ If A > 127, jump to LIlog4 LDX Q \ And then subtracting the high bytes of log(Q) - log(P) LDA log,X \ so now A contains the high byte of log(Q) - log(P) LDX P SBC log,X BCS LIlog5 \ If the subtraction fitted into one byte and didn't \ underflow, then log(Q) - log(P) < 256, so we jump to \ LIlog5 to return a result of 255 TAX \ Otherwise we set A to the A-th entry from the antilog LDA antilog,X \ table so the result of the division is now in A JMP LIlog6 \ Jump to LIlog6 to return the result .LIlog5 LDA #255 \ The division is very close to 1, so set A to the BNE LIlog6 \ closest possible answer to 256, i.e. 255, and jump to \ LIlog6 to return the result (this BNE is effectively a \ JMP as A is never zero) .LIlog7 LDA #0 \ The numerator in the division is 0, so set A to 0 and BEQ LIlog6 \ jump to LIlog6 to return the result (this BEQ is \ effectively a JMP as A is always zero) .LIlog4 LDX Q \ Subtract the high bytes of log(Q) - log(P) so now A LDA log,X \ contains the high byte of log(Q) - log(P) LDX P SBC log,X BCS LIlog5 \ If the subtraction fitted into one byte and didn't \ underflow, then log(Q) - log(P) < 256, so we jump to \ LIlog5 to return a result of 255 TAX \ Otherwise we set A to the A-th entry from the LDA antilogODD,X \ antilogODD so the result of the division is now in A .LIlog6 STA Q \ Store the result of the division in Q, so we have: \ \ Q = |delta_y| / |delta_x| ELIF _SOURCE_DISC_FILES \ The following section calculates: \ \ Q = Q / P \ = |delta_y| / |delta_x| \ \ using the same shift-and-subtract algorithm that's \ documented in TIS2 LDA Q \ Set A = |delta_y| \ We now repeat the following seven instruction block \ eight times, one for each bit in P. In the BBC Micro \ cassette and disc versions of Elite the following is \ done with a loop, but it is marginally faster to \ unroll the loop and have eight copies of the code, \ though it does take up a bit more memory (though that \ isn't a big concern when you have a 6502 Second \ Processor) ASL A \ Shift A to the left BCS LI4 \ If bit 7 of A was set, then jump straight to the \ subtraction CMP P \ If A < P, skip the following subtraction BCC LI5 .LI4 SBC P \ A >= P, so set A = A - P SEC \ Set the C flag to rotate into the result in Q .LI5 ROL Q \ Rotate the counter in Q to the left, and catch the \ result bit into bit 0 (which will be a 0 if we didn't \ do the subtraction, or 1 if we did) ASL A \ Repeat for the second time BCS P%+6 CMP P BCC P%+5 SBC P SEC ROL Q ASL A \ Repeat for the third time BCS P%+6 CMP P BCC P%+5 SBC P SEC ROL Q ASL A \ Repeat for the fourth time BCS P%+6 CMP P BCC P%+5 SBC P SEC ROL Q ASL A \ Repeat for the fifth time BCS P%+6 CMP P BCC P%+5 SBC P SEC ROL Q ASL A \ Repeat for the sixth time BCS P%+6 CMP P BCC P%+5 SBC P SEC ROL Q ASL A \ Repeat for the seventh time BCS P%+6 CMP P BCC P%+5 SBC P SEC ROL Q ASL A \ Repeat for the eighth time BCS P%+6 CMP P BCC P%+5 SBC P SEC ROL Q ENDIF LDX P \ Set X = P \ = |delta_x| BEQ LIEXS \ If |delta_x| = 0, return from the subroutine, as LIEXS \ contains a BEQ LIEX instruction, and LIEX contains an \ RTS INX \ Set X = P + 1 \ = |delta_x| + 1 \ \ We add 1 so we can skip the first pixel plot if the \ line is being drawn with swapped coordinates LDA Y2 \ If Y2 < Y1 then skip the following instruction CMP Y1 BCC P%+5 JMP DOWN \ Y2 >= Y1, so jump to DOWN, as we need to draw the line \ to the right and down