diff --git a/examples/maskedsprites.bas b/examples/maskedsprites.bas index a9e0d45b5..dbf80a092 100644 --- a/examples/maskedsprites.bas +++ b/examples/maskedsprites.bas @@ -4,74 +4,215 @@ ' Copyright (C) 2026 Conrado Badenas ' ' Example for -' Print Masked (AND+OR) Sprites, version 2026.03.06 +' Print Masked (AND+OR) Sprites, version 2026.04.05 ' ---------------------------------------------------------------- -#include +' DEFINEs to control the library cb/maskedsprites.bas + #define NumberofMaskedSprites 10 ' Try 50 here, with #define USE_MSFS + #define MaskedSprites_USE_STACK_TRANSFER +'#include "maskedsprites.bas" + #include + +' DEFINEs to control this example program + #define NUMWAITS -1 + #define BORDER_WAIT 0 + #define USE_MSFS + #define INTS di #include -CONST NUMMAXSPRITES as UByte = 10 -CONST NUMSPRITES as UByte = 7 -if NUMSPRITES>NUMMAXSPRITES then stop +CONST NUMSPRITES as UByte = NumberofMaskedSprites -dim i,temp as Integer -dim pointx1,pointx2,pointy1,pointy2 as Integer 'for subtractions +dim i as Byte +dim memoryPaging,numberofscrolls,loops,warnings,modulus as UByte dim x0(0 to NUMSPRITES-1) as UByte dim y0(0 to NUMSPRITES-1) as UByte dim x1(0 to NUMSPRITES-1) as UByte dim y1(0 to NUMSPRITES-1) as UByte +dim back(0 to NUMSPRITES-1) as UInteger +#ifdef USE_MSFS + dim regs(0 to NUMSPRITES-1) as UInteger + dim regHero0,regFoe00,regFoe20,regFoe21,regFoe22,regFoe23 as UInteger +#endif +dim temp as Integer +dim pointx1,pointx2,pointy1,pointy2 as Integer 'for subtractions + +ASM + INTS +END ASM for i=0 to NUMSPRITES-1 y0(i)=255:y1(i)=255:x1(i)=randomLimit(240) next i:x1(0)=120 -SetVisibleScreen(5):if GetVisibleScreen()<>5 then STOP +'DIM Rosquilla_Sprite(31) AS UByte => { _ +' $00,$07,$18,$20,$23,$44,$48,$48, _ +' $48,$48,$44,$23,$20,$18,$07,$00, _ +' $00,$E0,$18,$04,$C4,$22,$12,$12, _ +' $12,$12,$22,$C4,$04,$18,$E0,$00 _ +' } +DIM Rosquilla0(31) AS UByte => { _ + $00,$07,$19,$21,$23,$44,$48,$48, _ + $48,$58,$7C,$3B,$20,$18,$07,$00, _ + $00,$E0,$98,$84,$C4,$22,$12,$12, _ + $12,$1A,$3E,$DC,$04,$18,$E0,$00 _ + } +DIM Rosquilla1(31) AS UByte => { _ + $00,$07,$1C,$2E,$2F,$44,$48,$48, _ + $48,$48,$44,$2F,$2E,$1C,$07,$00, _ + $00,$E0,$18,$04,$C4,$22,$12,$1E, _ + $1E,$12,$22,$C4,$04,$18,$E0,$00 _ + } +DIM Rosquilla2(31) AS UByte => { _ + $00,$07,$18,$20,$3B,$7C,$58,$48, _ + $48,$48,$44,$23,$21,$19,$07,$00, _ + $00,$E0,$18,$04,$DC,$3E,$1A,$12, _ + $12,$12,$22,$C4,$84,$98,$E0,$00 _ + } +DIM Rosquilla3(31) AS UByte => { _ + $00,$07,$18,$20,$23,$44,$48,$78, _ + $78,$48,$44,$23,$20,$18,$07,$00, _ + $00,$E0,$38,$74,$F4,$22,$12,$12, _ + $12,$12,$22,$F4,$74,$38,$E0,$00 _ + } +memoryPaging = CheckMemoryPaging() +if memoryPaging then SetVisibleScreen(5) '128K paper 1:ink 7:border 0:cls -for i=0 to 127 +for i=0 to 126 pointx1=randomLimit(255):pointy1=randomLimit(191) pointx2=randomLimit(255):pointy2=randomLimit(191) plot pointx1,pointy1:draw pointx2-pointx1,pointy2-pointy1 next i -CopyScreen5ToScreen7() -'print at 11,8;"Esto es Screen5" -SetDrawingScreen7() 'Bank7 is set at $c000 -'print at 13,8;"Esto es Screen7" -SetDrawingScreen5() 'Bank7 is still set at $c000 -'print at 15,8;"Esto es Screen5" +#ifdef USE_MSFS + print "Init MSFS at ";InitMaskedSpritesFileSystem() + print "Free Blocks in MSFS = ";GetNumberofFreeBlocksInMSFS() + regHero0 = RegisterSpriteImageInMSFS(@hero0):if regHero0=0 then STOP + print "regHero0 = ";regHero0 + regFoe00 = RegisterSpriteImageInMSFS(@foe00):if regFoe00=0 then STOP + print "regFoe00 = ";regFoe00 + regFoe20 = RegisterSpriteGraphAndMaskInMSFS(@Rosquilla0(0),@Rosquilla_Sprite_Mask):if regFoe20=0 then STOP + print "regFoe20 = ";regFoe20 + regFoe21 = RegisterSpriteGraphAndMaskInMSFS(@Rosquilla1(0),@Rosquilla_Sprite_Mask):if regFoe21=0 then STOP + print "regFoe21 = ";regFoe21 + regFoe22 = RegisterSpriteGraphAndMaskInMSFS(@Rosquilla2(0),@Rosquilla_Sprite_Mask):if regFoe22=0 then STOP + print "regFoe22 = ";regFoe22 + regFoe23 = RegisterSpriteGraphAndMaskInMSFS(@Rosquilla3(0),@Rosquilla_Sprite_Mask):if regFoe23=0 then STOP + print "regFoe23 = ";regFoe23 -ASM - xor a ; 4 Ts reseteamos el contador - ld (23672),a ; 13 Ts de FRAMES a 0 -END ASM + for i=0 to NUMSPRITES-1: modulus = i mod 2 + if i=0 then + regs(0)=regHero0 + elseif modulus=1 then + regs(i)=regFoe00 + else ' modulus=0 with i>0 + regs(i)=regFoe20 + end if + next i +' pause 0 +#endif -' Empezamos con VisibleScreen = DrawingScreen = 5 -bucle: +for i=0 to NUMSPRITES-1 + back(i)=MaskedSpritesBackground(i) +' print back(i) +next i +' print:pause 0 - ' La nueva VisibleScreen está lista para que la ULA la saque por la tele - ' Pero vamos a esperar al principio de un frame para que no haya tearing - ' El 2 de luego significa que obtendremos 25 FPS como máximo - ASM - ld hl,23672 -wait: - ld a,(hl) ; A = contador de FRAMES - cp 2 ; 25 FPS como máximo - jr c,wait ; repetimos si A < 2 - xor a - ld (hl),a ; reseteamos el contador de FRAMES a 0 - END ASM +if memoryPaging then '128K + dim back0(0 to NUMSPRITES-1) as UInteger + ChangeMaskedSpritesBackgroundSet + for i=0 to NUMSPRITES-1 + back0(i)=MaskedSpritesBackground(i) +' print back0(i) + next i + ChangeMaskedSpritesBackgroundSet +' print:print MaskedSpritesFileSystemStart:pause 0 - ToggleVisibleScreen() - ' Ahora se puede modificar la DrawingScreen porque no está Visible + CopyScreen5ToScreen7() +' print at 11,8;"This is Screen5" + SetDrawingScreen7() 'Bank7 is set at $c000 +' print at 13,8;"This is Screen7" + SetDrawingScreen5() 'Bank7 is still set at $c000 +' print at 15,8;"This is Screen5" +end if - ' Restauramos fondo en x0,y0 si está guardado en MaskedSpritesBackground - for i=0 to NUMSPRITES-1 - if y0(i)<>255 then RestoreBackground(x0(i),y0(i),MaskedSpritesBackground(i)) +' We start with VisibleScreen = DrawingScreen = 5 +numberofscrolls=0 +warnings=0 +loops=0 +WaitForNewFrame(0) +do + + ' 128K + if memoryPaging then + ' The brand-new VisibleScreen is ready for the ULA to show it on TV + ' But we wait to the beginning of a frame to avoid tearing + ' (unless wanted time-to-wait has already been spent) + out 254,BORDER_WAIT:WaitForNewFrame(NUMWAITS):out 254,0 +'GetInterruptStatusInBorder() + ToggleVisibleScreen() + ' Now we can modify DrawingScreen because it is not Visible + + ' We restore background in x0,y0 if it is saved in MaskedSpritesBackground + if MaskedSpritesBackgroundSet then + for i=NUMSPRITES-1 to 0 step -1 + if y0(i)<>255 then RestoreBackground(x0(i),y0(i),back0(i)) + next i + else + for i=NUMSPRITES-1 to 0 step -1 + if y0(i)<>255 then RestoreBackground(x0(i),y0(i),back(i)) + next i + end if + end if + + ' After restoring background in a Spectrum 128K, you can use CPU to do something else: + ' + ' 0. Change/scroll background (ONLY NOW!!!) + ' 1. Check collisions + ' 2. Resolve collisions + ' 3. Change images of sprites for: + ' a. Animation of regular motion + ' b. Explosions and other extraordinary events + ' 4. Upgrade the score + ' 5. Play sound/music + ' ... + ' + ' You have plenty of time to do whatever you want in a Spectrum 128K + + ' 0. Change/scroll background (ONLY NOW!!!) + if memoryPaging=1 then '128K + if (in($7ffe) bAND 1)=0 then + if numberofscrolls=1 then + Scroll2() + else + Scroll1() + numberofscrolls=1 + end if + else + if numberofscrolls=1 then + Scroll1() + end if + numberofscrolls=0 + end if + end if + +#ifdef USE_MSFS + ' 3. Change images of sprites for: a. Animation of regular motion + for i=2 to NUMSPRITES-1 step 2: modulus = (i+loops) mod 4 + if modulus=0 then + regs(i)=regFoe20 + elseif modulus=1 then + regs(i)=regFoe21 + elseif modulus=2 then + regs(i)=regFoe22 + else + regs(i)=regFoe23 + end if next i +#endif - ' Actualizamos x0,y0 y calculamos nuevos x1,y1 + ' We update x0,y0 and compute new x1,y1 for i=1 to NUMSPRITES-1 temp=x1(i): x0(i)=temp temp=temp-1 @@ -85,7 +226,7 @@ wait: y1(i)=temp next i - 'Código especial para el prota + 'Special code for the hero temp=x1(0): x0(0)=temp temp=temp+1 -2*(in($dffe) bAND 1) +2*((in($dffe)>>1) bAND 1) '-noP+noO if temp<0 then temp=240 @@ -98,28 +239,109 @@ wait: if temp>176 then temp=176 y1(0)=temp - ' Guardamos fondo en MaskedSpritesBackground y dibujamos sprites en x,y - for i=NUMSPRITES-1 to 1 step -1 - SaveBackgroundAndDrawSprite(x1(i),y1(i),MaskedSpritesBackground(i),@enemigo0) - next i - SaveBackgroundAndDrawSprite(x1(0),y1(0),MaskedSpritesBackground(0),@prota0) + ' 48K We restore background in x0,y0 if it is saved in MaskedSpritesBackground + if not memoryPaging then + for i=NUMSPRITES-1 to 0 step -1 + if y0(i)<>255 then RestoreBackground(x0(i),y0(i),back(i)) + next i + end if - ' Cambiamos el Set de Backgrounds para la siguiente iteración - ChangeMaskedSpritesBackgroundSet() +#ifdef USE_MSFS + ' Print WARNING (if not scrolling) twice if MSFS has run out of free blocks + if not numberofscrolls and warnings<2 and not GetNumberofFreeBlocksInMSFS() then + print at 23,0;"WARNING! MSFS has NO free blocks"; + warnings=warnings+1 + end if +#endif - ' Hemos terminado de dibujar - ' DrawingScreen actual está lista para ser mostrada ahora mismo - ' Cambiamos de DrawingScreen para el siguiente ciclo de dibujo - ToggleDrawingScreen() + ' After restoring background in a Spectrum 48K, you should do NOTHING + ' ... + ' You have ABSOLUTELY NO TIME to do anything in a Spectrum 48K - ' Ahora VisibleScreen = DrawingScreen y - ' lo que se dibuje se verá mientras se dibuja + ' We save background in MaskedSpritesBackground and draw sprites in x,y... +#ifdef USE_MSFS + ' ...using SaveBackgroundAndDrawSpriteRegisteredInMSFS(,,,) + if MaskedSpritesBackgroundSet then + for i=0 to NUMSPRITES-1 + SaveBackgroundAndDrawSpriteRegisteredInMSFS(x1(i),y1(i),back0(i),regs(i)) + next i + else + for i=0 to NUMSPRITES-1 + SaveBackgroundAndDrawSpriteRegisteredInMSFS(x1(i),y1(i),back(i),regs(i)) + next i + end if +#else + ' ...using SaveBackgroundAndDrawSprite(,,,) + if MaskedSpritesBackgroundSet then + SaveBackgroundAndDrawSprite(x1(0),y1(0),back0(0),@hero0) + for i=1 to NUMSPRITES-1 + SaveBackgroundAndDrawSprite(x1(i),y1(i),back0(i),@foe00) + next i + else + SaveBackgroundAndDrawSprite(x1(0),y1(0),back(0),@hero0) + for i=1 to NUMSPRITES-1 + SaveBackgroundAndDrawSprite(x1(i),y1(i),back(i),@foe00) + next i + end if +#endif -goto bucle + ' After drawing sprites, you can use CPU to do something else: + ' + ' 1. Check collisions + ' 2. Resolve collisions + ' 3. Change images of sprites for: + ' a. Animation of regular motion + ' b. Explosions and other extraordinary events + ' 4. Upgrade the score + ' 5. Play sound/music + ' ... + ' + ' You have plenty of time to do whatever you want in ANY Spectrum -stop + ' We change the Set of Backgrounds (128K) for next iteration + if memoryPaging then ChangeMaskedSpritesBackgroundSet() -prota0: + ' We have finished drawing + if memoryPaging then + ' We change DrawingScreen (128K) for next iteration + ToggleDrawingScreen() + else + ' ULA shows DrawingScreen (48K) on TV for some frames before going on + out 254,BORDER_WAIT:WaitForNewFrame(NUMWAITS):out 254,0 + end if +'GetInterruptStatusInBorder() + ' Now, VisibleScreen = DrawingScreen, and + ' we could see anything we draw whilst it is drawn + loops=loops+1 +loop + +' SUB/FUNCTIONs that are not used, are "used" here to check compilation is OK + SetBankPreservingINTs(0) +print GetBankPreservingRegs() +print CheckMemoryPaging() + SetVisibleScreen(0) +print GetVisibleScreen() + ToggleVisibleScreen() + CopyScreen5ToScreen7() + CopyScreen7ToScreen5() + SetDrawingScreen5() +print SetDrawingScreen7() + ToggleDrawingScreen() +print ChangeMaskedSpritesBackgroundSet() + SaveBackgroundAndDrawSprite(0,0,0,0) + RestoreBackground(0,0,0) +print InitMaskedSpritesFileSystem() +print GetNumberofFreeBlocksInMSFS() +print RegisterSpriteImageInMSFS(0) +print RegisterSpriteGraphAndMaskInMSFS(0,0) + SaveBackgroundAndDrawSpriteRegisteredInMSFS(0,0,0,0) +GetInterruptStatusInBorder() +Scroll() +Scroll1() +Scroll2() +' Delete/Comment all these "uses" when used, or compilation checking is not needed + +hero0: ASM defb %11000000,%00000000,%00001111,%00000000;1 defb %10000000,%00011111,%00000000,%11100000;2 @@ -139,7 +361,7 @@ ASM defb %11000000,%00000000,%00001111,%00000000;1 END ASM -enemigo0: +foe00: ASM defb %00000011,%00000000,%11000000,%00000000;1 defb %00000011,%01111000,%11000000,%00011110;2 @@ -159,3 +381,404 @@ ASM defb %00000011,%00000000,%11000000,%00000000;1 END ASM +Rosquilla_Sprite_Mask: +ASM + db $FF,$F8,$E0,$C0,$C0,$83,$87,$87 + db $87,$87,$83,$C0,$C0,$E0,$F8,$FF + db $FF,$1F,$07,$03,$03,$C1,$E1,$E1 + db $E1,$E1,$C1,$03,$03,$07,$1F,$FF +END ASM + + +' ---------------------------------------------------------------- +' READ_IFF2 is a MACRO that reads correctly the IFF2 flip-flop, +' avoiding a "bug" reported in the Z80 User Manual, +' which Pedro Picapiedra aka ProgramadorHedonista kindly pointed +' me to: "If an interrupt occurs during execution of this +' instruction [LD A,I or LD A,R], the Parity flag contains a 0." +' ---------------------------------------------------------------- +#define READ_IFF2 \ + ld a,i ; IFF2=0/1=DI/EI is saved in PF=0/1=Odd/Even \ + jp pe,1f ; if PF=Even=1, it is sure that IFF2=1=EI \ + ld a,i ; read IFF2 again to ensure that IFF2=0=DI \ +1: + + +' ---------------------------------------------------------------- +' Wait for New Frame +' This SUB uses a hack to ensure a good reading of the IFF2 flip-flop. +' Hack was found thanks to Pedro Picapiedra aka ProgramadorHedonista. +' Parameters: +' Byte: if =0, you want just one HALT +' if >0, minimum number of frames spent since last wait +' if <0, return without waiting +' ---------------------------------------------------------------- +SUB FASTCALL WaitForNewFrame(minimumNumberofFramesToWaitSinceLastWait AS Byte) +ASM + PROC + LOCAL wait,temp + and a + ret m ; return if minimumNumberofFramesToWaitSinceLastWait < 0 + ld hl,temp + ld de,23672 + ld c,a ; A = C = minimumNumberofFramesToWaitSinceLastWait + READ_IFF2 + ex af,af' + ei ; interrupts MUST be enabled before HALT + halt +wait: + ld a,(de) + sub (hl) ; A = number of frames since last RET + cp c ; is A >= C? Yes: NoCarry. No: Carry + jr c,wait ; repeat if A < minimumNumberofFramesToWaitSinceLastWait + ld a,(de) + ld (hl),a + ex af,af' + ret pe ; Return with EI if IFF2=1=EI at the beginning + di ; Return with DI if IFF2=0=DI at the beginning + RET +temp: + defb 0 + ENDP +END ASM +END SUB + + +' ---------------------------------------------------------------- +' Get Interrupt Status in Border +' This SUB uses a hack to ensure a good reading of the IFF2 flip-flop. +' Hack was found thanks to Pedro Picapiedra aka ProgramadorHedonista. +' Border results: 2 Red if Interrupts are Disabled IFF2=0 +' 4 Green if Interrupts are Enabled IFF2=1 +' ---------------------------------------------------------------- +SUB FASTCALL GetInterruptStatusInBorder() +ASM + READ_IFF2 + push af + pop bc + ld a,c + and 4 ; A=0/4 iff IFF2=0/1 + rra ; A=0/2 iff IFF2=0/1 + add a,2 ; A=2/4 iff IFF2=0/1 + out (254),a +END ASM +END SUB + + +SUB FASTCALL Scroll() +ASM + PROC + LOCAL loop + ld hl,(.core.SCREEN_ADDR) + ld de,31 + ld b,192 +loop: +; Here, HL = screen address for first column + add hl,de; column 32 + xor a + rl (hl) ; 32 + dec l + rl (hl) ; 31 + dec l + rl (hl) ; 30 + dec l + rl (hl) ; 29 + dec l + rl (hl) ; 28 + dec l + rl (hl) ; 27 + dec l + rl (hl) ; 26 + dec l + rl (hl) ; 25 + dec l + rl (hl) ; 24 + dec l + rl (hl) ; 23 + dec l + rl (hl) ; 22 + dec l + rl (hl) ; 21 + dec l + rl (hl) ; 20 + dec l + rl (hl) ; 19 + dec l + rl (hl) ; 18 + dec l + rl (hl) ; 17 + dec l + rl (hl) ; 16 + dec l + rl (hl) ; 15 + dec l + rl (hl) ; 14 + dec l + rl (hl) ; 13 + dec l + rl (hl) ; 12 + dec l + rl (hl) ; 11 + dec l + rl (hl) ; 10 + dec l + rl (hl) ; 09 + dec l + rl (hl) ; 08 + dec l + rl (hl) ; 07 + dec l + rl (hl) ; 06 + dec l + rl (hl) ; 05 + dec l + rl (hl) ; 04 + dec l + rl (hl) ; 03 + dec l + rl (hl) ; 02 + dec l + rl (hl) ; 01 + rla ; save CF in A + add hl,de; column 32 + or (hl) + ld (hl),a + inc hl ; first column + djnz loop + ENDP +END ASM +END SUB + + +' ---------------------------------------------------------------- +' NEXT_ROW is a MACRO of ASM code, based on code from +' https://zonadepruebas.com/viewtopic.php?f=15&t=8372&start=40#p81507 +' and found by Joaquin Ferrero +' ---------------------------------------------------------------- +#define NEXT_ROW \ + sub 224 ; 7 A was L, now A = L + 32 (SUB 224 is similar to +32) \ + ; CF = 0/1 iff E >=/< 224 iff a third is/isn't crossed \ + ld l,a ; 4 \ + sbc a,a ; 4 A = 0/255 \ + and 248 ; 7 A = 0/248 (248 = -8) \ + add a,h ; 4 A = H/H-8 iff a third is/isn't crossed \ + ld h,a ; 4 += 30 Ts + + +' ---------------------------------------------------------------- +' Scroll 1 pixel up +' ---------------------------------------------------------------- +SUB FASTCALL Scroll1() +ASM + PROC + LOCAL loop1,loop2,exit + push ix + xor a + ld hl,(.core.SCREEN_ADDR); HL = $4000 or $c000 + ld de,buffer0 + call ldi32 ; scanline0 + ; DEC HL is not needed because row = 0 <> 7, 15, 23 + ld ixh,24 ; 24 rows (rows 0-23) +loop1: + ld l,a ; A = low byte of screen addresses for this row +; HL = scanline0 for this row + ld d,h + ld e,l ; DE = scanline0 of this row + inc h ; HL = scanline1 of this row + ld ixl,7 ; first 7 scanlines of one row +loop2: +; HL,DE = origin,destination screen address + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi ; BC-=32 DE+=32 HL+=32 (if E,L was 224, then D,H changes) + dec hl ; 6 in case H changes (because L was 224: rows 7,15,23) + ld l,a ; +4 = 10 Ts < 21 Ts (PUSH+POP) + ld d,h + ld e,l ; scanline_N+0 of this row + inc h ; scanline_N+1 of this row + dec ixl + jp nz,loop2 + dec ixh + jr z,exit ; this JR Z here (instead of an JP NZ later) adds +7 Ts/row = 161 Ts for 23 rows +; last scanline of first 23 rows: DE is OK, HL must change + NEXT_ROW + ld a,l + call ldi32 ; 539 Ts needed for each row except last row + dec hl ; with JR Z,EXIT before, we save 539-161 = 378 Ts for last row + jp loop1 +exit: + ld hl,buffer0 + call ldi32 ; DE = scanline7 of last row + pop ix + ENDP +END ASM +END SUB + + +' ---------------------------------------------------------------- +' Scroll 2 pixels up +' ---------------------------------------------------------------- +SUB FASTCALL Scroll2() +ASM + PROC + LOCAL loop1,loop2,exit + push ix + xor a + ld hl,(.core.SCREEN_ADDR); HL = $4000 or $c000 + ld de,buffer0 + call ldi32 ; scanline0 + ; DEC HL is not needed because row = 0 <> 7, 15, 23 + ld l,a + inc h + call ldi32 ; scanline1 + ; DEC HL is not needed because row = 0 <> 7, 15, 23 + ld ixh,24 ; 24 rows (rows 0-23) +loop1: + ld l,a ; A = low byte of screen addresses for this row +; HL = scanline1 for this row + ld d,h + ld e,l + dec d ; DE = scanline0 of this row + inc h ; HL = scanline2 of this row + ld ixl,6 ; first 6 scanlines of one row +loop2: +; HL,DE = origin,destination screen address + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi ; BC-=32 DE+=32 HL+=32 (if E,L was 224, then D,H changes) + dec hl ; 6 in case H changes (because L was 224: rows 7,15,23) + ld l,a ; +4 = 10 Ts < 21 Ts (PUSH+POP) + ld d,h + ld e,l + dec d ; DE = scanline_N+0 of this row + inc h ; HL = scanline_N+2 of this row + dec ixl + jp nz,loop2 + dec ixh + jr z,exit ; this JR Z here (instead of an JP NZ later) adds +7 Ts/row = 161 Ts for 23 rows +; last scanline of first 23 rows: DE is OK, HL must change + NEXT_ROW + ld c,h ; C=H is larger than 32, so B does not change when BC-=32 + ld b,e ; DE = scanline6 of this row SAVED in B + ld a,l ; HL = scanline0' of next row SAVED in A + call ldi32 ; 539 Ts needed for each row except last row + dec hl + ld l,a + dec de ; LD C,H + LD B,E + LD A,L + DEC HL + LD L,A + DEC DE + LD E,B + ld e,b ; = 4+4+4+6+4+6+4 = 32 Ts < 42 Ts 2*(PUSH+POP) + inc d ; DE = scanline7 of this row + inc h ; HL = scanline1' of next row + call ldi32 ; 539 Ts needed for each row except last row + dec hl ; with JR Z,EXIT before, we save 2*539-161 = 917 Ts for last row + jp loop1 +exit: + ld hl,buffer0 + call ldi32 ; DE = scanline6 of last row + dec de + ld e,a + inc d ; DE = scanline7 of last row + call ldi32 + pop ix + ret + ENDP + +buffer0: + defs 32 + +buffer1: + defs 32 + +ldi32: + ldi ; 32 LDI = 32*16 = 512 Ts + ldi ; CALL + RET = 17+10 = 27 Ts + ldi ; call ldi32 (+ret) = 512+27 = 539 Ts + ldi + ldi ; LD BC,32 + LDIR = 10 + 21*31+16 = 677 Ts + ldi + ldi ; 677 - 539 = 138 Ts + ldi + ldi ; then, "call ldi32 (+ret)" is 138 Ts faster than "ld bc,32 + ldir" + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi + ldi ; BC-=32 DE+=32 HL+=32 (if E,L was >=224, then D,H changes) +END ASM +END SUB + diff --git a/src/lib/arch/zx48k/stdlib/cb/maskedsprites.bas b/src/lib/arch/zx48k/stdlib/cb/maskedsprites.bas index 8a2feb376..27f390869 100644 --- a/src/lib/arch/zx48k/stdlib/cb/maskedsprites.bas +++ b/src/lib/arch/zx48k/stdlib/cb/maskedsprites.bas @@ -4,13 +4,13 @@ ' Copyright (C) 2026 Conrado Badenas ' Ideas taken from ' https://github.com/boriel-basic/zxbasic/blob/main/src/lib/arch/zx48k/stdlib/memorybank.bas -' by Juan Segura (a.k.a. Duefectu), +' by Juan Segura (a.k.a. Duefectu), ' https://github.com/oisee/antique-toy/blob/main/chapters/ch16-sprites/draft.md -' by Alice Vinogradova (a.k.a. oisee), and -' https://youtu.be/nBHXtI1Y-xU?t=434 -' by Benjamín (a.k.a. RetrobenSoft) +' by Alice Vinogradova (a.k.a. oisee), and +' https://youtu.be/nBHXtI1Y-xU?t=434 and https://youtu.be/-AUmmzDiGlE?t=434 +' by Benjamín (a.k.a. RetrobenSoft) ' -' Print Masked (AND+OR) Sprites, version 2026.03.06 +' Print Masked (AND+OR) Sprites, version 2026.04.05 ' ---------------------------------------------------------------- #ifndef __CB_MASKEDSPRITES__ @@ -19,17 +19,119 @@ REM Avoid recursive / multiple inclusion #define __CB_MASKEDSPRITES__ -#include #include #include +' ---------------------------------------------------------------- +' MaskedSprites_OUT_7FFD is a MACRO of ASM code, +' which uses a hack to ensure a good reading of the IFF2 flip-flop. +' Hack was found thanks to Pedro Picapiedra aka ProgramadorHedonista, +' who pointed me to this line of the Z80 User Manual: +' "If an interrupt occurs during execution of this instruction +' [LD A,I or LD A,R], the Parity flag contains a 0." +' ---------------------------------------------------------------- +#define MaskedSprites_OUT_7FFD \ + ld c,a ; save A in C \ + ld a,i ; IFF2=0/1=DI/EI is saved in PF=0/1=Odd/Even \ + jp pe,1f ; if PF=Even=1, it is sure that IFF2=1=EI \ + ld a,i ; read IFF2 again to ensure that IFF2=0=DI \ +1: ld a,c \ + ld bc,$7ffd ; Memory Bank control port \ + di ; Disable interrupts (IFF1,2=0) \ + ld ($5b5c),a ; Update BANKM system variable \ + out (c),a ; Set the bank \ + ret po ; Return with DI if IFF2=0=DI at the beginning \ + ei ; Return with EI if IFF2=1=EI at the beginning + + +' ---------------------------------------------------------------- +' Set a RAM bank in addresses $c000-$ffff, update BANKM, +' and return with INTerrupts preserved (unchanged) +' Only works on 128K and compatible models. +' Parameters: +' Ubyte: bank number 0,1,2,3,4,5,6,7 +' Changes: +' A, B, C +' Preserves: +' D, E, H, L are not used +' ---------------------------------------------------------------- +SUB FASTCALL SetBankPreservingINTs(bankNumber AS UByte) +ASM + and 7 ; make sure bankNumber = 0,1,2,3,4,5,6,7 + ld c,a ; C = A = bankNumber + ld a,($5b5c) ; Read BANKM system variable + and %11111000 ; Reset bank bits + or c ; Set bank bits to bankNumber + MaskedSprites_OUT_7FFD +END ASM +END SUB + + +' ---------------------------------------------------------------- +' Get which RAM bank is set in addresses $c000-$ffff +' Only works on 128K and compatible models. +' Preserves: +' B, C, D, E, H, L are not used +' Returns: +' UByte: bank number 0,1,2,3,4,5,6,7 +' ---------------------------------------------------------------- +#define MaskedSprites_GETBANK \ + ld a,($5b5c) ; Read BANKM system variable \ + and 7 ; Obtain bank bits +FUNCTION FASTCALL GetBankPreservingRegs() AS UByte +ASM + MaskedSprites_GETBANK +END ASM +END FUNCTION + + +' ---------------------------------------------------------------- +' Check whether memory paging works (128,+2,...) or not (16,48) +' Returns: +' UByte: 1 if paging works, 0 if it does not +' ---------------------------------------------------------------- +FUNCTION FASTCALL CheckMemoryPaging() AS UByte + DIM b,i AS UByte + DIM p AS UInteger + CONST address AS UInteger = $fffe + + b = GetBankPreservingRegs() + p = peek(UInteger, address) + for i=0 to 7 + SetBankPreservingINTs(i) + if p<>peek(UInteger, address) then 'there is another bank at address + if i<>b then SetBankPreservingINTs(b) + return 1 + end if + next i +' Here, we are in an unpaged Spectrum (48), or maybe... +' ...all banks hold the same value p at address. + + poke UInteger address, p bXOR $5555 ' we change value at address in RAM7 + SetBankPreservingINTs(0) + if p=peek(UInteger, address) then ' value at address in RAM0 is as before + SetBankPreservingINTs(7) + poke UInteger address, p + if 7<>b then SetBankPreservingINTs(b) + return 1 + end if + poke UInteger address, p ' memory paging does not work, we restore value at address + return 0 +ASM + +END ASM +END FUNCTION + + ' ---------------------------------------------------------------- ' Set the visible screen (either in bank5 or bank7) ' and updates the system variable BANKM. ' Only works on 128K and compatible models. ' Parameters: ' Ubyte: bank number 5 or 7 +' Preserves: +' D, E, H, L are not used ' ---------------------------------------------------------------- SUB FASTCALL SetVisibleScreen(bankNumber AS UByte) ASM @@ -38,15 +140,10 @@ ASM rla rla ; bit3 = 0/1 for bank5/7 ld c,a - ld hl,$5b5c ; BANKM system variable - ld a,(hl) ; Read BANKM + ld a,($5b5c) ; Read BANKM system variable and %11110111 ; Reset bit3 or c ; Set screen in bank5/7 - ld bc,$7ffd ; Memory Bank control port - di ; Disable interrupts - ld (hl),a ; Update BANKM system variable - out (c),a ; Set the screen - ei ; Enable interrupts + MaskedSprites_OUT_7FFD END ASM END SUB @@ -59,7 +156,13 @@ END SUB ' UByte: bank 5 or 7 ' ---------------------------------------------------------------- FUNCTION FASTCALL GetVisibleScreen() AS UByte - RETURN ((PEEK $5b5c bAND %1000)>>2) bOR 5 +ASM + ld a,($5b5c) ; Read BANKM system variable + and %00001000 ; Get bit3 = 0/1 for bank5/7 + rra + rra ; bit1 = 0/1 for bank5/7 + or %00000101 ; A = 5/7 for bank5/7 +END ASM END FUNCTION @@ -78,13 +181,13 @@ END SUB ' Copy contents of screen5 to screen7 (display file + attribs) ' Only works on 128K and compatible models. ' ---------------------------------------------------------------- -SUB CopyScreen5ToScreen7() +SUB FASTCALL CopyScreen5ToScreen7() DIM b AS UByte - b = GetBank() - if b<>7 then SetBank(7) + b = GetBankPreservingRegs() + if b<>7 then SetBankPreservingINTs(7) MemCopy($4000,$c000,$1b00) - if b<>7 then SetBank(b) + if b<>7 then SetBankPreservingINTs(b) END SUB @@ -92,13 +195,13 @@ END SUB ' Copy contents of screen7 to screen5 (display file + attribs) ' Only works on 128K and compatible models. ' ---------------------------------------------------------------- -SUB CopyScreen7ToScreen5() +SUB FASTCALL CopyScreen7ToScreen5() DIM b AS UByte - b = GetBank() - if b<>7 then SetBank(7) + b = GetBankPreservingRegs() + if b<>7 then SetBankPreservingINTs(7) MemCopy($c000,$4000,$1b00) - if b<>7 then SetBank(b) + if b<>7 then SetBankPreservingINTs(b) END SUB @@ -123,8 +226,8 @@ END SUB FUNCTION FASTCALL SetDrawingScreen7() AS UByte DIM b AS UByte - b = GetBank() - if b<>7 then SetBank(7) + b = GetBankPreservingRegs() + if b<>7 then SetBankPreservingINTs(7) SetScreenBufferAddr($c000)' ld (.core.SCREEN_ADDR),hl SetAttrBufferAddr($d800)' ld (.core.SCREEN_ATTR_ADDR),hl RETURN b @@ -146,35 +249,61 @@ END SUB ' ---------------------------------------------------------------- ' MaskedSpritesBackgroundSet = 0 or 1 is the Set of Backgrounds ' -' MaskedSpritesBackground(i) is the Dir where Background i begins +' MaskedSpritesBackground(i) is the address where Background i begins +' +' NumberofMaskedSprites is a MACRO that should be #define-d +' before #include-ing this file +' +' MaskedSprites_USE_STACK_TRANSFER is a MACRO that should be #define-d +' if you want this library to use Stack PUSH+POP instructions to speed-up +' transfer of information between different parts of the RAM +' (this library will disable interrupts before using Stack Transfer) ' ' ChangeMaskedSpritesBackgroundSet() changes the Set of Backgrounds ' Returns: ' Byte: new value of MaskedSpritesBackgroundSet (IYW to use it) ' ---------------------------------------------------------------- -dim MaskedSpritesBackgroundSet as Byte = 0 -#define MaskedSpritesBackground(i) (56064+((i)*2+MaskedSpritesBackgroundSet)*48) -FUNCTION FASTCALL ChangeMaskedSpritesBackgroundSet() as Byte +dim MaskedSpritesBackgroundSet AS UByte = 0 + +#define MaskedSpritesBackground(i) ( $db00+48*CAST(UInteger,i)+48*CAST(UInteger,NumberofMaskedSprites)*MaskedSpritesBackgroundSet ) + +FUNCTION FASTCALL ChangeMaskedSpritesBackgroundSet() AS UByte MaskedSpritesBackgroundSet = MaskedSpritesBackgroundSet bXOR 1 RETURN MaskedSpritesBackgroundSet END FUNCTION +' ---------------------------------------------------------------- +' MaskedSprites_NEXT_ROW is a MACRO of ASM code, based on code from +' https://zonadepruebas.com/viewtopic.php?f=15&t=8372&start=40#p81507 +' and found by Joaquin Ferrero +' ---------------------------------------------------------------- +#define MaskedSprites_NEXT_ROW \ + ld a,e ; 4 A = E \ + sub 224 ; 7 A = E + 32 (SUB 224 is similar to +32) \ + ; CF = 0/1 iff E >=/< 224 iff a third is/isn't crossed \ + ld e,a ; 4 \ + sbc a,a ; 4 A = 0/255 \ + and 248 ; 7 A = 0/248 (248 = -8) \ + add a,d ; 4 A = D/D-8 iff a third is/isn't crossed \ + ld d,a ; 4 += 34 Ts + + ' ---------------------------------------------------------------- ' Save background and Draw sprite in screen ' Parameters: ' UByte: X coordinate (0:left to 240:right) ' UByte: Y coordinate (0:up to 176:down) -' UInteger: Dir where background will be saved -' UInteger: Dir where sprite image begins +' UInteger: Address where background will be saved +' UInteger: Address where sprite image begins ' ---------------------------------------------------------------- -SUB FASTCALL SaveBackgroundAndDrawSprite(X as UByte, Y as UByte, backgroundDir as UInteger, spriteImageDir as UInteger) +SUB FASTCALL SaveBackgroundAndDrawSprite(X AS UByte, Y AS UByte, backgroundAddr AS UInteger, spriteImageAddr AS UInteger) ASM PROC - LOCAL shiftsprite, spriteOK, loop0, loopA, loopB - LOCAL branchA, branchB, nextA, nextB + LOCAL shiftright, shiftleft, noshift + LOCAL loopSR, loopSL, loopNS, loopR, loopL, branchSR, branchSL, branchNS ; A = X - pop de ; returnDir + pop de ; returnAddr exx pop bc ; B = Y ld c,a ; C = X @@ -200,41 +329,44 @@ ASM ; END code from https://skoolkid.github.io/rom/asm/22AA.html ld hl,(.core.SCREEN_ADDR) add hl,de - ex de,hl ; DE = screenDir where drawing will start + ex de,hl ; DE = screenAddr where drawing will start ld a,c; ; A = X and 7 - jr z,spriteOK ; jump if X is a multiple of 8 (unlikely) + jr z,noshift ; jump if X is a multiple of 8 (unlikely) ; continue if sprite must be shifted -shiftsprite: - pop bc ; backgroundDir + cp 4 ; is >= 4 ? + jp nc,shiftleft ; shift left if X MOD 8 = 4,5,6,7 + ; shift right if X MOD 8 = 1,2,3 +shiftright: + pop bc ; backgroundAddr exx - pop hl ; spriteImageDir - push de ; returnDir + pop hl ; spriteImageAddr + push de ; returnAddr push ix ld ixh,16 ; 16 scanlines - ld ixl,a ; IXl = X MOD 8 = 1,2,...,7 -loopB: + ld ixl,a ; IXl = X MOD 8 = 1,2,3,4 +loopSR: ld a,(hl) ; mask1 inc hl ld c,(hl) ; graph1 inc hl ld d,(hl) ; mask2 inc hl - ld e,(hl) ; graph1 + ld e,(hl) ; graph2 inc hl - push hl ; spriteImageDir + push hl ; spriteImageAddr ld hl,$FF00 ; H = 255 , L = 0 ld b,ixl -loop0: - scf - rra ; SCF + RRA injects a 1 in bit7 of A - rr d - rr h - srl c ; ShiftRightLogical injects a 0 in bit7 of C - rr e - rr l - djnz loop0 +loopR: + scf ; 4 + rra ; 4; SCF + RRA injects a 1 in bit7 of A + rr d ; 8 + rr h ; 8 + srl c ; 8; ShiftRightLogical injects a 0 in bit7 of C + rr e ; 8 + rr l ; 8 + djnz loopR ; 4+4+8+8+8+8+8 = 48 Ts ld b,a push hl ; H,L = mask,graph 3rd byte push de ; D,E = mask,graph 2nd byte @@ -272,38 +404,31 @@ loop0: inc d ld a,d and 7 - jr z,branchB ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times) + jr z,branchSR ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times) exx - pop hl ; spriteImageDir + pop hl ; spriteImageAddr dec ixh - jp nz,loopB + jp nz,loopSR pop ix ret -branchB: - ld a,e - add a,32 ; for 1 out of 8 values of D - ld e,a - jr c,nextB ; 7Ts no jump (7/8 times), 12 Ts jump (1/8 times) - ld a,d - sub 8 - ld d,a -nextB: +branchSR: + MaskedSprites_NEXT_ROW exx - pop hl ; spriteImageDir + pop hl ; spriteImageAddr dec ixh - jp nz,loopB + jp nz,loopSR pop ix ret -spriteOK: - pop bc ; backgroundDir - pop hl ; spriteImageDir +noshift: + pop bc ; backgroundAddr + pop hl ; spriteImageAddr exx - push de ; returnDir + push de ; returnAddr exx push ix ld ixh,16 ; 16 scanlines -loopA: +loopNS: ld a,(de) ; screen ld (bc),a; ; save inc bc @@ -327,22 +452,99 @@ loopA: inc d ld a,d and 7 - jr z,branchA ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times) + jr z,branchNS ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times) + dec ixh + jp nz,loopNS + pop ix + ret +branchNS: + MaskedSprites_NEXT_ROW dec ixh - jp nz,loopA + jp nz,loopNS pop ix ret -branchA: - ld a,e - add a,32 ; for 1 out of 8 values of D - ld e,a - jr c,nextA ; 7Ts no jump (7/8 times), 12 Ts jump (1/8 times) + +shiftleft: + pop bc ; backgroundAddr + exx + pop hl ; spriteImageAddr + push de ; returnAddr + push ix + ld ixh,16 ; 16 scanlines + sub 8 + neg ; A = 8 - oldA + ld ixl,a ; IXl = 8 - (X MOD 8) = 8 - 4,5,6,7 = 4,3,2,1 +loopSL: + ld a,(hl) ; mask1 + inc hl + ld c,(hl) ; graph1 + inc hl + ld d,(hl) ; mask2 + inc hl + ld e,(hl) ; graph2 + inc hl + push hl ; spriteImageAddr + + ld hl,$FF00 ; H = 255 , L = 0 + ld b,ixl +loopL: + sll d ; 8; ShiftLeftLogical injects a 1 in bit0 of D + rla ; 4 + rl h ; 8 + sla e ; 8; ShiftLeftArithmetic injects a 0 in bit0 of E + rl c ; 8 + rl l ; 8 + djnz loopL ; 8+4+8+8+8+8 = 44 Ts + ld b,a + push de ; D,E = mask,graph 3rd byte + push bc ; B,D = mask,graph 2nd byte + push hl ; H,L = mask,graph 1st byte + exx + + ld a,(de) ; screen + ld (bc),a ; save + inc bc + pop hl + and h ; mask + or l ; graph + ld (de),a ; 1st byte done + inc e + + ld a,(de) ; screen + ld (bc),a ; save + inc bc + pop hl + and h ; mask + or l ; graph + ld (de),a ; 2nd byte done + inc e + + ld a,(de) ; screen + ld (bc),a ; save + inc bc + pop hl + and h ; mask + or l ; graph + ld (de),a ; 3rd byte done + dec e + dec e + + inc d ld a,d - sub 8 - ld d,a -nextA: + and 7 + jr z,branchSL ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times) + exx + pop hl ; spriteImageAddr + dec ixh + jp nz,loopSL + pop ix + ret +branchSL: + MaskedSprites_NEXT_ROW + exx + pop hl ; spriteImageAddr dec ixh - jp nz,loopA + jp nz,loopSL pop ix ret ENDP @@ -355,14 +557,14 @@ END SUB ' Parameters: ' UByte: X coordinate (0:left to 240:right) ' UByte: Y coordinate (0:up to 176:down) -' UInteger: Dir where saved background begins +' UInteger: Address where saved background begins ' ---------------------------------------------------------------- -SUB FASTCALL RestoreBackground(X as UByte, Y as UByte, backgroundDir as UInteger) +SUB FASTCALL RestoreBackground(X AS UByte, Y AS UByte, backgroundAddr AS UInteger) ASM PROC - LOCAL loopC, loopD, branchC, branchD, nextC, nextD + LOCAL loop2b, loop3b, branch2b, branch3b ; A = X - pop de ; returnDir + pop de ; returnAddr exx pop bc ; B = Y ld c,a ; C = X @@ -388,17 +590,18 @@ ASM ; END code from https://skoolkid.github.io/rom/asm/22AA.html ld hl,(.core.SCREEN_ADDR) add hl,de - ex de,hl ; DE = screenDir where restoring will start - pop hl ; backgroundDir + ex de,hl ; DE = screenAddr where restoring will start + pop hl ; backgroundAddr exx - push de ; returnDir + push de ; returnAddr exx ld a,c; ; A = X ld bc,$10FF ; B = 16, C = 255 (up to 255 LDIs do not change B) and 7 - jr z,loopD ; jump if X is a multiple of 8 (unlikely) + jr z,loop2b ; jump if X is a multiple of 8 (unlikely) ; continue if restoring 3 bytes per scanline -loopC: +; 3bytes per scanline +loop3b: ldi ; 16 Ts vs 7+7+6+4=24 Ts ldi ldi ; 3 bytes background restored to screen @@ -409,22 +612,15 @@ loopC: inc d ld a,d and 7 - jr z,branchC ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times) - djnz loopC + jr z,branch3b ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times) + djnz loop3b ret -branchC: - ld a,e - add a,32 ; for 1 out of 8 values of D - ld e,a - jr c,nextC ; 7Ts no jump (7/8 times), 12 Ts jump (1/8 times) - ld a,d - sub 8 - ld d,a -nextC: - djnz loopC +branch3b: + MaskedSprites_NEXT_ROW + djnz loop3b ret - -loopD: +; 2bytes per scanline +loop2b: ldi ; 16 Ts vs 7+7+6+4=24 Ts ldi ; 2 bytes background restored to screen dec de ; last LDI could have increased D if initially E=254... @@ -433,19 +629,673 @@ loopD: inc d ld a,d and 7 - jr z,branchD ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times) - djnz loopD + jr z,branch2b ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times) + djnz loop2b + ret +branch2b: + MaskedSprites_NEXT_ROW + djnz loop2b + ret + ENDP +END ASM +END SUB + + +' ---------------------------------------------------------------- +' Structure of the Masked Sprites FileSystem (MSFS): +' +' MSFS consists of many blocks of 96 bytes +' MSFS starts at address stored in MaskedSpritesFileSystemStart, e.g., 56736 +' MSFS length is a multiple of 96 bytes, e.g., 8736 = 91*96 bytes +' With that length, MSFS ranges from 56736 to 65471 +' MSFS stores Images (mask+graph) of Masked Sprites +' Blocks used are marked in the FSB (https://en.wikipedia.org/wiki/Free-space_bitmap) +' +' First block of the MSFS (superblock, block number = 0) +' start+0 DEFB number of blocks in MSFS = bits of the FSB, e.g., 91 +' start+1 DEFB number of bytes of the FSB, e.g., 12 (91/8 = 11.4) +' start+2-13 DEFS 12 is the FSB (12 bytes = 96 bits is enough for 91 blocks) +' start+14-15 unused +' +' Block of an unshifted image (block number n = 0,...,90) +' start+n*96 \ +' ... | 16 bytes unused for n>0, superblock for n=0 +' start+n*96+15 / +' start+n*96+16-17 DEFW start+n*96+32 = start of unshifted image +' start+n*96+18-19 DEFW address of block for image shifted 1 pixel, or 0 if not used +' start+n*96+20-21 DEFW address of block for image shifted 2 pixels, or 0 if not used +' ... +' start+n*96+30-31 DEFW address of block for image shifted 7 pixels, or 0 if not used +' start+n*96+32-95 DEFS 64 the unshifted image (mask+graph) +' +' Block number 0 (n=0) is special because it contains +' 16 bytes for the superblock (including 2 unused bytes) + +' 16 bytes for addressing unshifted and shifted versions of the first image + +' 64 bytes for the first unshifted image +' +' Block of a shifted image (block number m = 1,...,90, note m>0) +' start+m*96+0 DEFS 96 the shifted image (mask+graph) +' ---------------------------------------------------------------- +dim MaskedSpritesFileSystemStart AS UInteger = 0 +' ---------------------------------------------------------------- +' MaskedSpritesFileSystemStart = address where Masked Sprites FileSystem starts +' +' InitMaskedSpritesFileSystem() inits the MSFS +' Returns: +' UInteger: value of MaskedSpritesFileSystemStart (IYW to use it) +' ---------------------------------------------------------------- +FUNCTION FASTCALL InitMaskedSpritesFileSystem() AS UInteger + DIM b,c AS UByte + DIM l AS UInteger + + c = CheckMemoryPaging() + if c then + b = GetBankPreservingRegs() + if b<>7 then SetBankPreservingINTs(7) + MaskedSpritesFileSystemStart = $db00+96*NumberofMaskedSprites + else + MaskedSpritesFileSystemStart = $db00+48*NumberofMaskedSprites + end if + l = -MaskedSpritesFileSystemStart + l = Int(l/96) + poke MaskedSpritesFileSystemStart, l ' MSFS blocks = FSB bits + if l=0 then STOP + l = 1+Int((l-1)/8) + poke MaskedSpritesFileSystemStart+1,l ' FSB bytes + if c then + if b<>7 then SetBankPreservingINTs(b) + end if + return MaskedSpritesFileSystemStart + +' Now, some assembly routines needed for next SUB/FUNCTIONs +ASM +; ---------------------------------------------------------------- +; Find memory addres in MSFS for the start of a block +; Parameters: +; L = blocknumber = n = 0,1,2,... (probably less than 200) +; Preserves: +; A, B, C are not used +; Returns: +; HL = start+n*96 +; DE = start +; ---------------------------------------------------------------- +FindMemoryAdressForBlockInMSFS: + PROC + ld h,0 + ld d,h + ld e,l ; HL = DE = n = blocknumber + add hl,de + add hl,de ; HL = DE*3 + add hl,hl + add hl,hl + add hl,hl + add hl,hl + add hl,hl ; HL = DE*3*(2^5) = DE*96 + ld de,(_MaskedSpritesFileSystemStart) + add hl,de; ; HL = start+n*96 + ret + ENDP + +; ---------------------------------------------------------------- +; Find First Unused Block in MSFS and (optionally) Book it +; Parameters: +; CarryFlag = 0/1 don't/do Book it +; Preserves: +; C is not used +; Returns: +; CarryFlag = 0 if found, 1 if not found +; A = FirstUnusedBlock if found +; HL = start+A*96 if found +; ---------------------------------------------------------------- +FindFirstUnusedBlockInMSFS: + PROC + LOCAL loop1, loop2, full, found, loop3, compute + ex af,af' ; saves CarryFlag + ld hl,(_MaskedSpritesFileSystemStart) + ld d,(hl) + ld e,d ; D = E = number of bits in the FSB = N + inc hl + inc hl ; HL points to the first byte in the FSB +loop1: + ld a,(hl) + ld b,8 +loop2: + rrca + jr nc,found + dec e + jr z,full + djnz loop2 + inc hl + jp loop1 +full: ; E = 0 + scf ; CarryFlag=1 = ERROR + ret +found: ; E = N,N-1,N-2,...,1 + ex af,af' + jr nc,compute + ex af,af' + rlca ; undo last RRCA + or 1 ; mark this block as used +loop3: + rrca ; finish the 8-bit rotation to... + djnz loop3 ; leave bits where they were + ld (hl),a ; effective booking +compute: + ld a,d ; A = N + sub e ; A = 0,1,2,...,N-1 = blocknumber + ld l,a + call FindMemoryAdressForBlockInMSFS + and a ; CarryFlag=0 = OK + ret ; A = blocknumber, HL = start+A*96 + ENDP +END ASM + +END FUNCTION + + +' ---------------------------------------------------------------- +' Get Number of Free Blocks in MSFS +' Returns: +' UByte: number of free blocks in MSFS +' ---------------------------------------------------------------- +FUNCTION FASTCALL GetNumberofFreeBlocksInMSFS() AS UByte +ASM + PROC + LOCAL loop1, loop2, exit + call _GetBankPreservingRegs + cp 7 ; ZeroFlag=0 (NZ) iff _GetBankPreservingRegs returns A<>7 + push af ; ZF and original RAM bank when FUNCTION was called + ld a,7 + call nz,_SetBankPreservingINTs ; set RAM7 if _GetBankPreservingRegs returns A<>7 + xor a ; A = 0 = number of reset bits in the FSB + ld hl,(_MaskedSpritesFileSystemStart) + ld d,(hl) ; D = number of bits in the FSB = N + ld e,a ; E = 0 always + inc hl + inc hl ; HL points to the first byte in the FSB +loop1: + ld c,(hl) + ld b,8 +loop2: + rr c + ccf ; CarryFlag = 0/1 = bit in the FSB is set/reset + adc a,e ; A += CarryFlag = number of reset bits in the FSB + dec d + jr z,exit ; return A if all bits in FSB have been checked + djnz loop2 + inc hl + jr loop1 +exit: + ex af,af' ; A' = number of free blocks in MSFS + pop af ; ZF and original RAM bank when FUNCTION was called + call nz,_SetBankPreservingINTs + ex af,af' ; A = number of free blocks in MSFS + ret + ENDP +END ASM +END FUNCTION + + +' ---------------------------------------------------------------- +' Register spriteImage in MSFS +' Parameters: +' UInteger: address where spriteImage begins +' Returns: +' UInteger: registry number in the MSFS = start+n*96+16 if OK +' 0 if not OK +' ---------------------------------------------------------------- +FUNCTION FASTCALL RegisterSpriteImageInMSFS(spriteImageAddr AS UInteger) AS UInteger +ASM + PROC + LOCAL full, exit + call _GetBankPreservingRegs + cp 7 ; ZeroFlag=0 (NZ) iff _GetBankPreservingRegs returns A<>7 + push af ; ZF and original RAM bank when FUNCTION was called + push hl ; spriteImageAddr + ld a,7 + call nz,_SetBankPreservingINTs ; set RAM7 if it was not set + scf + call FindFirstUnusedBlockInMSFS ; and book it (SCF) + jr c,full + ld bc,16 + add hl,bc + push hl ; HL = start+A*96+16 + ld d,h + ld e,l + inc de ; DE = start+A*96+17 + dec bc ; BC = 15 + ld (hl),0 + ldir ; reset RAM from start+A*96+16 to start+A*96+31 (incl.) + pop hl ; HL = start+A*96+16 + ld b,h + ld c,l ; BC = start+A*96+16 + ld (hl),e ; DE = start+A*96+32 (after last LDIR) + inc hl + ld (hl),d ; start+A*96+16-17 DEFW start+A*96+32 + pop hl ; spriteImageAddr + push bc ; BC = start+A*96+16 + ld bc,64 + ldir ; transfer from spriteImageAddr to start+A*96+32 + pop hl ; HL = start+A*96+16 +exit: + pop af ; ZF and original RAM bank when FUNCTION was called + call nz,_SetBankPreservingINTs + ret +full: + pop hl ; spriteImageAddr + ld hl,0 + jr exit + ENDP +END ASM +END FUNCTION + + +' ---------------------------------------------------------------- +' Register spriteGraph and spriteMask in MSFS +' (useful when different Graphs share the same Mask) +' +' Data in spriteGraph and spriteMask MUST be in "putchars format" +' +' Parameters: +' UInteger: address where spriteGraph begins +' UInteger: address where spriteMask begins +' Returns: +' UInteger: registry number in the MSFS = start+n*96+16 if OK +' 0 if not OK +' ---------------------------------------------------------------- +FUNCTION FASTCALL RegisterSpriteGraphAndMaskInMSFS(spriteGraphAddr AS UInteger,spriteMaskAddr AS UInteger) AS UInteger +ASM + PROC + LOCAL full, exit, loop1, loop2 + pop de ; returnAddr + pop bc ; spriteMaskAddr + push de ; returnAddr +; stack is empty. Now we will push data to be preserved + exx ; HL' = spriteGraphAddr, BC' = spriteMaskAddr + call _GetBankPreservingRegs + cp 7 ; ZeroFlag=0 (NZ) iff _GetBankPreservingRegs returns A<>7 + push af ; ZF and original RAM bank when FUNCTION was called + ld a,7 + call nz,_SetBankPreservingINTs ; set RAM7 if it was not set + scf + call FindFirstUnusedBlockInMSFS ; and book it (SCF) + jr c,full + ld bc,16 + add hl,bc + push hl ; HL = start+A*96+16 + ld d,h + ld e,l + inc de ; DE = start+A*96+17 + dec bc ; BC = 15 + ld (hl),0 + ldir ; reset RAM from start+A*96+16 to start+A*96+31 (incl.) + pop hl ; HL = start+A*96+16 + ld (hl),e ; DE = start+A*96+32 (after last LDIR) + inc hl + ld (hl),d ; start+A*96+16-17 DEFW start+A*96+32 + dec hl + push hl ; return value HL = start+A*96+16 + push de ; DE = start+A*96+32 DEFS 64 the unshifted image (mask+graph) + exx ; HL = spriteGraphAddr, BC = spriteMaskAddr + pop de ; DE = DEFS 64 the unshifted image (mask+graph) + push ix + push de ; DE = start+A*96+32 + ld ixl,16 +loop1: + ld a,(bc) ; mask + ld (de),a + inc bc + inc de + ld a,(hl) ; graph + ld (de),a + inc hl + inc de + inc de + inc de + dec ixl + jp nz,loop1 + pop de ; DE = start+A*96+32 + inc de + inc de ; DE = start+A*96+32 +2 + ld ixl,16 +loop2: + ld a,(bc) ; mask + ld (de),a + inc bc + inc de + ld a,(hl) ; graph + ld (de),a + inc hl + inc de + inc de + inc de + dec ixl + jp nz,loop2 + pop ix + pop hl ; return value HL = start+A*96+16 +exit: + pop af ; ZF and original RAM bank when FUNCTION was called + call nz,_SetBankPreservingINTs + ret +full: + ld hl,0 + jr exit + ENDP +END ASM +END FUNCTION + + +' ---------------------------------------------------------------- +' Save background and Draw sprite registered in the MSFS +' Parameters: +' UByte: X coordinate (0:left to 240:right) +' UByte: Y coordinate (0:up to 176:down) +' UInteger: address where background will be saved +' UInteger: registry number in the MSFS for the spriteImage +' ---------------------------------------------------------------- +SUB FASTCALL SaveBackgroundAndDrawSpriteRegisteredInMSFS(X AS UByte, Y AS UByte, backgroundAddr AS UInteger, spriteImageReg AS UInteger) +ASM + PROC + LOCAL full, makeShiftedImage, loopMSI, loopMSI1 + LOCAL useShiftedImage, loopUSI, branchUSI, exitUSI + LOCAL noshift, loopNS, branchNS, exitNS + ; A = X + pop de ; returnAddr + exx + pop bc ; B = Y + ld c,a ; C = X +; BEGIN code from https://skoolkid.github.io/rom/asm/22AA.html + rlca + rlca + rlca ; A = %c4c3c2c1c0c7c6c5 + xor b + and %11000111 + xor b ; A = %c4c3b5b4b3c7c6c5 + rlca + rlca + ld e,a ; E = %b5b4b3c7c6c5c4c3 + ld a,b + and %11111000 + rra + rra + rra ; A = %.0.0.0b7b6b5b4b3 + xor b + and %11111000 + xor b + ld d,a ; D = %.0.0.0b7b6b2b1b0 +; END code from https://skoolkid.github.io/rom/asm/22AA.html + ld hl,(.core.SCREEN_ADDR) + add hl,de + ex de,hl ; DE = screenAddr where drawing will start + ld a,c; ; A = X, keep it secret, keep it safe + pop bc ; BC = backgroundAddr + pop hl ; HL = spriteImageReg + exx + push de ; returnAddr + exx +; stack is empty. Now we will push data to be preserved + push ix + ld ixh,16 ; 16 scanlines + and 7 + jr z,noshift ; jump if X is a multiple of 8 (unlikely) + push de ; DE = screenAddr where drawing will start + push bc ; BC = backgroundAddr + ld b,0 + ld c,a ; BC = A = 1,2,3,4,5,6,7 + add hl,bc + add hl,bc ; HL = start+n*96+16+C*2 DEFW address of block for image shifted C pixels, or 0 if not used + ld e,(hl) + inc hl + ld d,(hl) ; DE = address for image shifted C pixels, or 0 if not used + ld a,d + or e + jp z,makeShiftedImage +;UseShiftedImage (USI) +useShiftedImage: + ex de,hl ; HL = address for image shifted C pixels + pop bc ; BC = backgroundAddr + pop de ; DE = screenAddr where drawing will start +#ifdef MaskedSprites_USE_STACK_TRANSFER + ld a,i ; IFF2=0/1=DI/EI is saved in PF=0/1=Odd/Even + jp pe,1f ; if PF=Even=1, it is sure that IFF2=1=EI + ld a,i ; read IFF2 again to ensure that IFF2=0=DI +1: ex af,af' + di + ld (exitUSI+1),sp + ld sp,hl +#endif +loopUSI: + ld a,(de) ; screen + ld (bc),a; ; save + inc bc +#ifdef MaskedSprites_USE_STACK_TRANSFER + pop hl + and l ; mask + or h ; graph +#else + and (hl); ; mask + inc l + or (hl) ; graph + inc hl +#endif + ld (de),a ; 1st byte done + inc e + + ld a,(de) ; screen + ld (bc),a; ; save + inc bc +#ifdef MaskedSprites_USE_STACK_TRANSFER + pop hl + and l ; mask + or h ; graph +#else + and (hl); ; mask + inc l + or (hl) ; graph + inc hl +#endif + ld (de),a ; 2nd byte done + inc e + + ld a,(de) ; screen + ld (bc),a; ; save + inc bc +#ifdef MaskedSprites_USE_STACK_TRANSFER + pop hl + and l ; mask + or h ; graph +#else + and (hl); ; mask + inc l + or (hl) ; graph + inc hl +#endif + ld (de),a ; 3rd byte done + dec e + dec e + + inc d + ld a,d + and 7 + jr z,branchUSI ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times) + dec ixh + jp nz,loopUSI +exitUSI: +#ifdef MaskedSprites_USE_STACK_TRANSFER + ld sp,$1234 + pop ix + ex af,af' + ret po ; Return with DI if IFF2=0=DI at the beginning + ei ; Return with EI if IFF2=1=EI at the beginning + ret +#else + pop ix ret -branchD: - ld a,e - add a,32 ; for 1 out of 8 values of D - ld e,a - jr c,nextD ; 7Ts no jump (7/8 times), 12 Ts jump (1/8 times) +#endif +branchUSI: + MaskedSprites_NEXT_ROW + dec ixh + jp nz,loopUSI +#ifdef MaskedSprites_USE_STACK_TRANSFER + jp exitUSI +#else + pop ix + ret +#endif +;NoShift (NS) +noshift: + ld a,(hl) ; HL = spriteImageReg + inc hl + ld h,(hl) + ld l,a ; HL = start of unshifted image +#ifdef MaskedSprites_USE_STACK_TRANSFER + ld a,i ; IFF2=0/1=DI/EI is saved in PF=0/1=Odd/Even + jp pe,1f ; if PF=Even=1, it is sure that IFF2=1=EI + ld a,i ; read IFF2 again to ensure that IFF2=0=DI +1: ex af,af' + di + ld (exitNS+1),sp + ld sp,hl +#endif +loopNS: + ld a,(de) ; screen + ld (bc),a; ; save + inc bc +#ifdef MaskedSprites_USE_STACK_TRANSFER + pop hl + and l ; mask + or h ; graph +#else + and (hl); ; mask + inc l + or (hl) ; graph + inc hl +#endif + ld (de),a ; 1st byte done + inc e + + ld a,(de) ; screen + ld (bc),a ; save + inc bc +#ifdef MaskedSprites_USE_STACK_TRANSFER + pop hl + and l ; mask + or h ; graph +#else + and (hl); ; mask + inc l + or (hl) ; graph + inc hl +#endif + ld (de),a ; 2nd byte done + dec e + + inc d ld a,d - sub 8 - ld d,a -nextD: - djnz loopD + and 7 + jr z,branchNS ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times) + dec ixh + jp nz,loopNS +exitNS: +#ifdef MaskedSprites_USE_STACK_TRANSFER + ld sp,$1234 + pop ix + ex af,af' + ret po ; Return with DI if IFF2=0=DI at the beginning + ei ; Return with EI if IFF2=1=EI at the beginning + ret +#else + pop ix + ret +#endif +branchNS: + MaskedSprites_NEXT_ROW + dec ixh + jp nz,loopNS +#ifdef MaskedSprites_USE_STACK_TRANSFER + jp exitNS +#else + pop ix + ret +#endif +;MakeShiftedImage (MSI) +makeShiftedImage: + ld a,8 + sub c ; C = X MOD 8 = 1,2,3,4,5,6,7 to the right + ld ixl,a ; IXl = 8 - C = 7,6,5,4,3,2,1 to the left + push hl ; HL = start+n*96+16+C*2 + 1 + scf + call FindFirstUnusedBlockInMSFS ; and book it (SCF) + jr c,full + ex de,hl ; DE = start+m*96 = address for the shiftedimage-to-be + pop hl ; HL = start+n*96+16+C*2 + 1 + ld (hl),d + dec hl + ld (hl),e; ; HL = start+n*96+16+C*2 DEFW address for the shifted image + ld b,0 + sbc hl,bc + sbc hl,bc + ld a,(hl) ; HL = spriteImageReg + inc hl + ld h,(hl) + ld l,a ; HL = start of unshifted image + push de ; DE = address for image shifted C pixels + push de ; two PUSH because we will POP one just before useShiftedImage + exx + pop hl ; HL' = address for the shiftedimage-to-be + exx +loopMSI: + ld a,(hl) ; mask1 + inc hl + ld c,(hl) ; graph1 + inc hl + ld d,(hl) ; mask2 + inc hl + ld e,(hl) ; graph2 + inc hl + push hl ; spriteImageAddr += 4 + ld hl,$FF00 ; H = 255 , L = 0 + ld b,ixl +loopMSI1: + sll d ; 8; ShiftLeftLogical injects a 1 in bit0 of D + rla ; 4 + rl h ; 8 + sla e ; 8; ShiftLeftArithmetic injects a 0 in bit0 of E + rl c ; 8 + rl l ; 8 + djnz loopMSI1; 8+4+8+8+8+8 = 44 Ts + ld b,a + push de ; D,E = mask,graph 3rd byte + push bc ; B,D = mask,graph 2nd byte + push hl ; H,L = mask,graph 1st byte + exx + pop de ; D',E' = mask,graph 1st byte + ld (hl),d + inc hl + ld (hl),e + inc hl + pop de ; D',E' = mask,graph 2nd byte + ld (hl),d + inc hl + ld (hl),e + inc hl + pop de ; D',E' = mask,graph 3rd byte + ld (hl),d + inc hl + ld (hl),e + inc hl ; HL' += 6 in this loop + exx + pop hl ; spriteImageAddr + dec ixh + jp nz,loopMSI + pop de ; DE = address for image shifted C pixels + ld ixh,16 ; 16 scanlines + jp useShiftedImage +full: + pop hl ; HL = start+n*96+16+C*2 + 1 + pop bc ; BC = backgroundAddr + pop de ; DE = screenAddr where drawing will start + pop ix ret ENDP END ASM