Ruido cíclico en pantalla durante la carga del mapa de bits de hardware-sprite en el ASIC

[Hardware/GX4000] Ruido cíclico en pantalla durante la carga de bitmaps de sprites por hardware del ASIC (&4000-&4FFF) — 6128 Plus real, RASM/.cpr de 4 bancos

Hola a todos:

Estoy depurando un cartucho casero (homebrew) para GX4000/CPC+ (RASM, .cpr de 4 bancos, probado en un 6128 Plus real mediante RGB SCART) y me he topado con un problema de corrupción en la imagen. Lo he aislado con bastante precisión tras varias rondas de pruebas, pero no logro explicármelo. Publico esto por si le resulta familiar a alguien con experiencia en el hardware real.

Síntoma

Tras la carga de los bitmaps de los sprites (16 x 256 bytes LDIR en &4000-&4FFF), la pantalla muestra un ruido visual cíclico: finos patrones diagonales/de puntos repetitivos en colores cambiantes, que se alternan con breves momentos de imagen limpia, aproximadamente cada pocos segundos. Parece un fallo de sincronización de vídeo compuesto/RGB (similar al dot crawl), no un cuelgue de la CPU; la ejecución sigue funcionando bien en todo momento (confirmado por otros medios).

Lo que está confirmado que funciona bien de forma aislada (probado individualmente en hardware real, pantalla estable, sin ruido)

  • Secuencia de desbloqueo del ASIC (secuencia estándar de 17 bytes en &BC00)
  • Selección de modo del Gate Array (&7F00,&80 — modo 0, ROMs sin tocar; nota: &7F00,&88, que también desactiva la ROM superior, provoca una pérdida total de sincronismo de vídeo en este hardware — «Sin señal» en la TV, manteniéndose tras un reinicio — un hallazgo independiente que quizás merezca su propio hilo)
  • Inicialización completa del CRTC, R0-R9 + R12/R13 (valores estándar Amstrad de 50 Hz) a través de &BC00/&BD00, realizada mientras el ASIC está desbloqueado
  • Carga de la paleta de pantalla + paleta de sprites (2 LDIRs, 32+30 bytes, en &6400/&6422) mediante el mismo mecanismo de paginación I/O del ASIC (&7F00,&B8 / &7F00,&A0)

Lo que reproduce el ruido

Cargar los 16 bitmaps de los sprites (256 bytes cada uno, LDIR simple, en &4000, &4100, &4200 … &4F00) — confirmado de forma aislada, manteniendo todo lo anterior presente y estable. El ruido aparece independientemente de:

  • si los registros de los sprites (&6000-&607F) se ponen a cero (mag=0, es decir, «no mostrado» según el formato documentado) antes o después de la carga del bitmap
  • si los 16 LDIRs se realizan en una ráfaga continua (~21 ms, es decir, más de un fotograma de 20 ms) o se reparten en varios fotogramas con una espera de VSync entre cada transferencia
  • si la carga ocurre antes o después de la inicialización del CRTC/vídeo (es decir, independientemente de si ya se está generando una imagen de forma activa)

Código (fragmento relevante)

ASIC_IN:
    ld bc,#7fb8
    out (c),c
    ret
ASIC_OUT:
    ld bc,#7fa0
    out (c),c
    ret

InitSprites:
    call ASIC_IN
    ; sprites ocultos primero (mag=0, Y=255), probado tanto antes como después de la carga
    ld hl,#6002
    ld de,8
    ld b,16
.hide:
    ld a,255
    ld (hl),a
    push hl
    inc hl
    inc hl
    xor a
    ld (hl),a
    pop hl
    add hl,de
    djnz .hide

    ld hl,SpritePlayer
    ld de,#4000
    ld bc,#100
    ldir
    ; ... 15 bloques LDIR idénticos más, de = #4100, #4200, ... #4f00
    call ASIC_OUT
    ret

Cosas que he descartado

  • No es un error de código o lógica en nuestro bucle principal del juego (se reproduce reduciendo el bucle del juego a un bucle WaitVSync / jr vacío)
  • No es la configuración del CRTC (confirmada limpia de forma aislada, R0-R9 todos presentes)
  • No es el propio mecanismo de bloqueo/desbloqueo del ASIC (confirmado mediante la carga de paleta usando exactamente el mismo mecanismo &7FB8/&7FA0 sin ningún problema)
  • No parece un problema de «el sprite renderizando basura visible a mitad de la carga» hasta donde puedo juzgar; reordenar el ocultado antes de la carga no cambió nada, y el mag=0 documentado ya significa «no mostrado», no «tamaño 1x» (inicialmente asumí esto último, lo cual era incorrecto)
  • Comparado con el tutorial de sprites por hardware publicado por roudoudou (programmation assembleur Z80) — nuestra secuencia de desbloqueo, el uso de RMR2 (&7F00,&B8/&A0) y la técnica de copia LDIR coinciden exactamente con su implementación de referencia, por lo que no parece un error de implementación por nuestra parte.

Nota sobre emuladores

Curiosamente, CaPriCe Forever (Windows) ejecuta este cartucho correctamente a nivel visual en las partes de CRTC y desbloqueo de ASIC, pero se bloquea por completo si se escribe en los registros 8 o 9 del CRTC (incluso individualmente, incluso con valores inofensivos 0/7) — una divergencia respecto al hardware real, donde R8/R9 son necesarios y funcionan bien. cap32 (Linux) no llega lo suficientemente lejos para probarlo (parece ignorar o gestionar mal el .cpr de 4 bancos por completo, arrancando directamente al BASIC del firmware incluso con CPC 6128+ seleccionado — posiblemente un problema independiente de compatibilidad con el formato .cpr en cap32, ya que un .cpr comercial conocido de 4/8 bancos —Bomb Jack GX— tiene el mismo problema allí mientras funciona bien en CaPriCe Forever y en hardware real).

Pregunta

¿Alguien ha visto este comportamiento específico de «ruido durante/después de cargar datos de bitmap de sprites por hardware» en hardware real Plus/GX4000? ¿Existe alguna restricción de timing documentada que me esté pasando por alto? Por ejemplo, ¿la lógica de lectura de sprites (sprite-fetch) sigue leyendo &4000-&4FFF independientemente del registro mag/visibilidad, de modo que escribir allí mientras se genera una imagen causa interferencias visibles en el silicio real (erratas conocidas del ASIC)? De ser así, ¿existe alguna solución recomendada (por ejemplo, cargar solo durante una ventana de barrido específica, o algún paso adicional de bloqueo/asentamiento al conmutar la página del ASIC)?

Estaré encantado de compartir el código fuente completo o un .cpr de prueba si resulta útil. ¡Gracias por leer hasta aquí!

1 me gusta

Nunca he visto ningún tipo de corrupción en la pantalla al actualizar sprites. Puede haber problemas raros si estás haciendo un desplazamiento de menos de un píxel en cualquier modo en el que estés actualmente, pero supongo que no estás haciendo eso, ¿verdad?

La principal discrepancia entre la visualización de sprites emulada y la real ocurre cuando hay un conflicto entre la escritura de datos de sprites y la actualización de la pantalla. Ninguno de los emuladores actuales (que yo sepa) maneja esto correctamente.

Gracias por la respuesta: ese detalle sobre el «conflicto entre la escritura de datos de sprites y las actualizaciones de pantalla» resulta muy útil como contexto. Y no, no estoy haciendo ningún desplazamiento a nivel de subpíxel (eliminé por completo los registros de desplazamiento suave, ni siquiera estaban activados).

Sin embargo, hay una aclaración que creo que cambia la perspectiva: el ruido no es un fallo transitorio puntual vinculado a la propia carga; es un patrón persistente y recurrente que se repite en ciclos (aproximadamente cada pocos segundos) durante la partida normal, mucho después de que haya finalizado la carga de sprites (la cual solo ocurre una vez al arrancar y tarda unos 21 ms en total). Por tanto, no parece que sea un conflicto de bus continuo sucediendo en cada fotograma; más bien parece que la carga deja al hardware en un estado ligeramente distinto o marginal tras su ejecución (temporización del CRTC, estado interno del ASIC, frecuencia de sincronización) que luego se manifiesta como un problema recurrente de bloqueo de sincronización en el televisor.

Intenté ocultar la imagen en pantalla por completo durante la carga (CRTC R6=0 durante toda la transferencia y restaurado después), bajo la teoría de que eliminar cualquier escaneado activo durante la escritura evitaría el conflicto. No hubo cambios: el mismo ruido recurrente continuó después. Así que, sea cual sea el estado en el que queda, este perdura más allá de la propia transferencia y no ocurre únicamente mientras se realiza.

Para ser totalmente sincero: mis conocimientos sobre el Amstrad están desactualizados desde hace unos 35 años, y estoy coordinando este depurado con la ayuda de un asistente de IA que escribe el código de prueba e interpreta por mí los datos del depurador de hardware (ACE), ya que yo no recuerdo al detalle el funcionamiento interno del ASIC/CRTC. Os pido disculpas de antemano si me cuesta un poco responder a preguntas técnicas más detalladas; estaré encantado de ejecutar cualquier prueba específica o extraer cualquier volcado de registros concreto desde ACE si me indicáis exactamente qué debo mirar.

En concreto, si resulta útil, puedo:

  • Ejecutar el programa con los paneles de registros del CRTC/ASIC abiertos en ACE y tomar una captura de pantalla o un vídeo corto en el punto específico que me pidáis (por ejemplo, justo antes/después de la carga de sprites, o durante el momento en que aparece el ruido).
  • Probar valores de registro específicos que me sugiráis.
  • Compartir el código fuente completo o un archivo .cpr mínimo para reproducir el fallo.

Gracias de nuevo por dedicarle tiempo a esto.

Si puedes publicar un .cpr mínimo con el que se pueda reproducir el fallo, seguro que se puede acotar bastante rápido :+1:

Si puedes grabar un vídeo, eso también podría ayudar. Y, solo por curiosidad, ¿tienes algún dispositivo de expansión conectado a tu Plus?

Un vídeo, si es posible, y un código fuente mínimo serían muy útiles.

¿Se trata de un patrón similar a un rastreo de puntos de píxeles físicos (como la «nieve» del ZX Spectrum) o más bien de un patrón de interferencia en la pantalla?

¿Ocurre tanto en el Plus como en el GX4000?

El GX4000 tiene la frecuencia del reloj principal ligeramente ajustada para ofrecer una «mejor señal» en los televisores.

Tengo entendido que se ha visto que tanto el GX4000 como el Plus muestran «barras de cárcel» (líneas verticales) en la imagen, las cuales creo que se pueden filtrar, pero no píxeles como tal.

¿Qué tipo de pantalla es? ¿Un monitor de pantalla plana moderno, un CRT antiguo?

El problema principal que he visto es que escribir los datos de píxeles mientras se muestra el sprite hace que este se corte visualmente o desaparezca durante un breve periodo (normalmente lo que dura la escritura). No he visto ninguna otra alteración ni nada después de haber escrito los sprites.

¿Ocurre con algún sprite en específico? Si activas solo uno, ¿sigue pasando?

¿Cambia algo si el zoom (mag) se configura para simular los anchos de píxel del modo 0 y modo 1?

1 me gusta

Me perdí la parte sobre cómo el modo 0 causa problemas, ¿puedes compartir el código que hace que ocurra eso?

Suena más a un problema de hardware. Nunca he visto que el modo 0 cause problemas de sincronización.

Lo que pienso es: Comprueba la alimentación del ordenador. Comprueba el cristal de 40Mhz. Comprueba la sincronización en el conector Scart (especialmente un buen Vsync y Hsync cuando se usa el modo 0). Si estás usando un Pico GX o similar y aún no lo has hecho, reprodúcelo usando código ejecutado con un cartucho básico conectado, por ejemplo.

1 me gusta

¿Cuál es el valor de tu puntero de pila (Stack pointer)? A mí me aparecía basura en la pantalla hace unos meses, hasta que me di cuenta de que se me había olvidado inicializar el registro SP, y todos los pushes y pops se estaban haciendo en mi doble búfer :face_without_mouth:

1 me gusta

¡Gracias por vuestras respuestas, al final di con el origen de este bug!
En resumen, mi función NextRandom utiliza el registro B como variable de trabajo sin guardarlo, mientras que SpawnMeteors (e InitStars) lo usan en paralelo como contador del bucle djnz. Cada llamada a NextRandom dentro del bucle sobrescribía este contador con un valor pseudoaleatorio, lo que hacía que el número de iteraciones se descontrolara y corrompiera la memoria mucho más allá de las tablas previstas.
Solución: push bc / pop bc alrededor del cuerpo de NextRandom.
Todavía me quedan algunos bugs incomprensibles:
Las colisiones (disparo↔alien, nave↔meteoro) siguen sin funcionar.
Sigue sin haber Música DMA (nada de sonido).
Y por último, el peor de todos: la imagen aparece recortada en la esquina superior izquierda. Mi juego solo se muestra en el tercio izquierdo de la pantalla, pero si muevo la nave hacia la derecha vuelve a aparecer a la izquierda y una vez más, pero se detiene en el medio al segundo intento.
Adjunto el código comentado con la ayuda de Claude IA.
Si a alguien se le ocurre algo, gracias de nuevo.

enlace a un vídeo del juego

; ================================================================
; INVADER GX4000 - RASM cartridge build wrapper
; ================================================================
BUILDCPR “build\INVADERGX.CPR”

; Banque 0 : le jeu complet (code + assets), inchange.
BANK 0
ORG #0000
INCLUDE “invader.asm”

; — CORRECTIF ---------------------------------------------------
; Le firmware GX4000/CPC+ mappe TOUJOURS la banque 3 de la cartouche
; en ROM haute (&C000-&FFFF) au demarrage, quelle que soit la taille
; reelle du programme. Une cartouche d’une seule banque (comme
; c’etait le cas ici) ne fournit pas les banques 1 a 3 : selon
; l’emulateur/le materiel, la cartouche peut alors ne pas etre
; reconnue comme demarrable, meme si le code de la banque 0 est
; parfaitement valide (c’est ce qu’on observait : le code s’executait
; bien lors des tests, mais la cartouche entiere n’etait jamais
; consideree comme bootable). On complete donc a 4 banques minimum
; (64 Ko), comme le fait toute cartouche CPC+ qui fonctionne
; (l’exemple Bomb Jack GX en comporte 8).
; -------------------------------------------------------------------
BANK 1
ORG #0000
ds #4000,#ff

BANK 2
ORG #0000
ds #4000,#ff

BANK 3
ORG #0000
ds #4000,#ff

=====================================================================

; CPC+ / GX4000 hardware constants
ASIC_PAGE_IN equ #7fb8
ASIC_PAGE_OUT equ #7fa0
SCREEN_BASE equ #c000
STACK_TOP equ #bffe

; Sprite attributes: 8 bytes per sprite, x/y are words, zoom at +4.
SPR_ATTR equ #6000
SPR_BITMAP equ #4000
SPR_PALETTE equ #6422

; PSG/PPI
PPI_A equ #f400
PPI_B equ #f500
PPI_C equ #f600
PPI_CTRL equ #f700

=======================================================================

; ================================================================
; INVADER GX - version complete source
; Amstrad GX4000 / CPC Plus - Z80
; Build with RASM.
;
; Gameplay:
; - 5 levels
; - 10 aliens, 4 families / movement engines
; - player free X/Y movement, joystick only
; - one active shot
; - 4 indestructible meteors, pseudo-random drift
; - hardware sprites: 1 player + 1 shot + 10 aliens + 4 meteors
; - CPC+ 4096-colour palette, raster split, soft scroll, DMA music
; - score, lives and level HUD
;
; Memory policy:
; ROM 0000-3FFF: code + assets
; RAM 8000-BFFF: game state / music DMA list
; RAM C000-FFFF: screen (write-through under cartridge ROM)
; ASIC page: only mapped while touching 4000-7FFF registers.
; ================================================================

org #0000
jp Boot

include ‘hardware.inc’

; — CORRECTIF CRITIQUE -------------------------------------------
; Reserve l’adresse &0038 (vecteur standard d’interruption du CPC,
; cible du RST 38h) avec un gestionnaire minimal sûr, comme le fait
; toute cartouche CPC+ qui fonctionne (verifie par comparaison
; binaire avec Bomb Jack GX, qui fait exactement pareil). Sans ca,
; si une interruption survient avant meme que “di” ait pu s’executer
; (le firmware peut laisser les interruptions actives juste avant de
; sauter dans la cartouche), le CPU saute en plein milieu d’une
; instruction de notre code → plantage immediat, ecran noir des le
; premier instant.
; ---------------------------------------------------------------------
ds #0038-$,0
IrqVector:
ei
reti

Boot:
di
ld sp,STACK_TOP
call UnlockASIC
call InitSprites
call InitVideo
call InitPalette
call InitGame
call InitMusic
; — CORRECTIF (voir rapport) ------------------------------------
; “ei” etait active ici sans jamais poser “im 1” et sans le moindre
; gestionnaire d’interruption (aucun “reti” dans tout le source).
; Le Gate Array genere pourtant une IRQ materielle 300 fois/seconde,
; quoi qu’il arrive : en IM 0 (mode par defaut), cela revient a un
; “rst &38” force qui saute EN PLEIN MILIEU du code de FrameLoop et
; empile une adresse de retour jamais depilee (fuite de pile a
; chaque IRQ) → plantage quasi immediat, ecran noir.
; Ce moteur pilote tout par attente active du VSync (WaitVSync) et
; n’a besoin d’aucune interruption CPU : on ne les active donc pas.
; Si des interruptions redeviennent necessaires plus tard, il faudra
; ajouter “im 1” AVANT “ei” et fournir un vrai gestionnaire a &0038.
; -------------------------------------------------------------------

FrameLoop:
call WaitVSync
call ReadJoystick
call UpdatePlayer
call UpdateShot
call UpdateAliens
call UpdateMeteors
call CollideShotAliens
call CollidePlayerMeteors
call CheckWave
call DrawAllSprites
call DrawHUD
call Animate
jr FrameLoop

; ----------------------------------------------------------------
; CPC+ ASIC unlock. 17 writes = sync 0xFF,0x00 then 15-key sequence.
; ----------------------------------------------------------------
UnlockASIC:
ld bc,#bc00
ld hl,ASIC_UNLOCK
ld e,17
.unlock:
ld a,(hl)
out (c),a
inc hl
dec e
jr nz,.unlock
ret

ASIC_UNLOCK:
db #ff,#00,#ff,#77,#b3,#51,#a8,#d4,#62,#39,#9c,#46,#2b,#15,#8a,#cd,#ee

; ----------------------------------------------------------------
; Video setup: MODE 0, screen base C000, simple raster split at line 16.
; Soft-scroll is used for a tiny starfield drift; sprites are independent.
; ----------------------------------------------------------------
InitVideo:
; Gate Array MODE 0, ROM HAUTE DESACTIVEE (#7f88). Necessaire pour
; que les lectures CPU de l’ecran (&C000+) renvoient la vraie RAM et
; non la ROM (le CRTC, lui, lit toujours la RAM directement pour
; l’affichage - mais DrawStars fait un lire-modifier-ecrire sur
; l’ecran a CHAQUE trame : avec la ROM haute active, cette lecture
; renvoyait du contenu ROM au lieu du vrai pixel, source probable
; d’une partie du bruit observe). Le “Aucun signal” vu precedemment
; avec cette valeur etait du a l’absence d’init CRTC dans ce test
; isole precis, pas a #7f88 lui-meme (confirme par un nouveau test
; cible, CRTC complet + #7f88 : stable). Merci a roudoudou (Discord
; Praline / CPCWiki) d’avoir repere ce point.
ld bc,#7f88
out (c),c
; CRTC : programme la table complete R0-R9 + R12/R13. Auparavant seuls
; R12/R13 (adresse ecran) etaient ecrits ; R0-R9 (qui definissent la
; synchro elle-meme) restaient sur leurs valeurs par defaut au boot
; direct sur cartouche → defilement/roulement d’image observe.
call InitCRTC
; Clear screen RAM C000-FFFF
ld hl,SCREEN_BASE
ld de,SCREEN_BASE+1
ld bc,#3fff
xor a
ld (hl),a
ldir
; ASIC split / scroll : RETIRE. Ces ecritures (&6801-&6804) causaient
; une distorsion/bruit generalise sur tout l’ecran sur materiel reel
; (confirme par test isole). Cette fonctionnalite n’etait de toute
; facon jamais activee (juste “preparee” sans le bit d’activation),
; donc aucune perte fonctionnelle a la retirer.
; seed a deterministic star field
call InitStars
ret

; ----------------------------------------------------------------
; Table CRTC standard Amstrad (50 Hz) + R12/R13 pour ecran a &C000.
; Format : paires (registre, valeur).
; ----------------------------------------------------------------
InitCRTC:
ld hl,CRTCTable
ld b,CRTCTableLen
.crtc_loop:
push bc
ld bc,#bc00
ld a,(hl)
out (c),a ; selectionne le registre
inc hl
ld bc,#bd00
ld a,(hl)
out (c),a ; ecrit la valeur
inc hl
pop bc
djnz .crtc_loop
ret

CRTCTable:
; TEST : on ne touche plus R0/R2/R3/R4/R5/R7 (total horizontal/
; vertical, position de synchro), a l’instar de Bomb Jack GX (verifie
; par desassemblage : il ne les ecrit jamais, laissant le firmware
; gerer ces valeurs). On garde R1 (notre bitmap suppose 80 octets/
; ligne = 40 caracteres) et R6 (nombre de lignes affichees).
db 1,#28 ; R1 horizontal affiche (40 car., nécessaire pour notre bitmap)
db 6,#19 ; R6 vertical affiche (25 lignes car.)
db 8,0 ; R8 entrelacement/skew : aucun
db 9,7 ; R9 hauteur de caractere - 1 (8 lignes)
db 12,#30 ; R12 adresse ecran (poids fort) → &C000
db 13,0 ; R13 adresse ecran (poids faible)
CRTCTableEnd:
CRTCTableLen equ (CRTCTableEnd-CRTCTable)/2

; ----------------------------------------------------------------
; CPC+ palette: 16 main colours + 15 sprite colours.
; ----------------------------------------------------------------
InitPalette:
call ASIC_IN
ld hl,MainPalette
ld de,#6400
ld bc,32
ldir
ld hl,SpritePalette
ld de,#6422
ld bc,30
ldir
call ASIC_OUT
ret

MainPalette:
; black, dark blue, blue, cyan, dark red, red, magenta, yellow,
; dark green, green, light cyan, white, orange, violet, grey, bright grey
dw #0000,#0018,#00af,#00ff,#1800,#f000,#f00f,#0ff0
dw #0180,#0f80,#0fff,#0fff,#0f60,#a0af,#0888,#0eee

SpritePalette:
dw #0ff0,#0f00,#00ff,#0fff,#0f80,#0f08,#08ff,#0ff8
dw #0f88,#088f,#0fff,#0f0f,#08f0,#0ff8,#0f40

; ----------------------------------------------------------------
; Hardware sprite pixel data. 16x16, byte per pixel, pen 0 transparent.
; ----------------------------------------------------------------
InitSprites:
call ASIC_IN
; TEST : coupe totalement l’affichage (CRTC R6=0) pendant tout
; l’upload, plutot que de compter sur le masquage individuel des
; sprites (Mag=0) qui n’empeche apparemment pas un conflit de bus
; ecriture/balayage sur materiel reel (confirme par un contributeur
; du forum CPCWiki : “the main discrepancy between emulated and
; real sprite display is when there’s a clash between writing
; sprite data and the screen being updated”).
ld bc,#bc06
out (c),c
ld bc,#bd00
out (c),c ; R6 = 0 : plus aucune ligne affichee
; masque les 16 sprites (Mag=0 + Y hors ecran) - conserve par
; prudence en plus de la coupure d’affichage
ld hl,#6002
ld de,8
ld b,16
.hide:
ld a,255
ld (hl),a ; Y (poids faible) hors ecran
push hl
inc hl
inc hl
xor a
ld (hl),a ; Mag = 0
pop hl
add hl,de
djnz .hide
ld hl,SpritePlayer
ld de,#4000
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteShot
ld de,#4100
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien1
ld de,#4200
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien2
ld de,#4300
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien3
ld de,#4400
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien4
ld de,#4500
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteMeteor
ld de,#4600
ld bc,#100
ldir
call WaitVSync
; replicate 4 alien/meteor images into sprite slots 2..15
ld hl,SpriteAlien1
ld de,#4700
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien2
ld de,#4800
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien3
ld de,#4900
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien4
ld de,#4a00
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteMeteor
ld de,#4b00
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteMeteor
ld de,#4c00
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteMeteor
ld de,#4d00
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteMeteor
ld de,#4e00
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteMeteor
ld de,#4f00
ld bc,#100
ldir
; restaure l’affichage (R6 = 25 lignes de caracteres, valeur normale)
ld bc,#bc06
out (c),c
ld bc,#bd19
out (c),c
call ASIC_OUT
ret

; ----------------------------------------------------------------
; Game initialisation.
; ----------------------------------------------------------------
InitGame:
xor a
ld (Level),a
ld (Frame),a
ld (Anim),a
ld (ShotActive),a
ld (GameOver),a
ld a,3
ld (Lives),a
ld a,72
ld (PlayerX),a
ld a,174
ld (PlayerY),a
xor a
ld (Score0),a
ld (Score1),a
ld (Score2),a
ld (Score3),a
ld a,2
ld (FormationDir),a
ld a,1
ld (RandomSeed),a
call SpawnWave
call SpawnMeteors
ret

SpawnWave:
ld a,(Level)
ld e,a
add a,a
add a,e ; level*3
ld e,a
ld d,0
ld hl,LevelParams
add hl,de
ld a,(hl)
ld (MoveEngine),a
inc hl
ld a,(hl)
ld (AlienSpeed),a
inc hl
ld a,(hl)
ld (AlienSprite),a
ld a,10
ld (AlienAliveCount),a
; formation positions: X=20..110, Y=35..62
ld hl,AlienX
ld de,AlienY
ld bc,AlienVX
ld ix,AlienType
ld iy,AlienAlive
ld a,10
ld (TmpCount),a
ld a,20
ld (TmpX),a
xor a
ld (FormationDir),a
ld b,0
.spawn_loop:
ld a,(TmpX)
ld (hl),a
add a,9
ld (TmpX),a
ld a,b
and 3
ld (ix+0),a
ld a,1
ld (iy+0),a
ld a,35
ld c,b
ld a,c
and 3
add a,a
add a,35
ld (de),a
inc hl
inc de
inc ix
inc iy
inc b
ld a,b
cp #0a
jr nz,.spawn_loop
ret

; Level parameters: engine, speed divisor, sprite family.
; Engine 0 formation sweep, 1 staircase, 2 sine-like, 3 split rush, 4 zigzag.
LevelParams:
db 0,4,0
db 1,3,1
db 2,2,2
db 3,2,3
db 4,1,4

; ----------------------------------------------------------------
; Joystick 1 only: line 9, direct PSG/PPI scan. Active high in JoyState.
; ----------------------------------------------------------------
ReadJoystick:
; Select PSG register 14 on PPI port A.
ld bc,#f40e
out (c),c
ld bc,#f6c0
out (c),c
xor a
out (c),a ; required inactive phase on CPC+
ld bc,#f792 ; PPI: port A input
out (c),c
ld a,#49 ; line 9 + read operation (bit 6 set)
ld b,#f6
out (c),a
ld b,#f4
in a,(c)
cpl
ld (JoyState),a
ld bc,#f782 ; restore port A output
out (c),c
xor a
ld b,#f6
out (c),a
ret

; ----------------------------------------------------------------
; Player: 2D movement with acceleration-free arcade controls.
; Game coordinates are 0..144 logical pixels; sprite X is *4 in ASIC.
; ----------------------------------------------------------------
UpdatePlayer:
ld a,(JoyState)
bit 2,a
jr nz,.left
bit 3,a
jr nz,.right
jr .x
.left:
ld hl,PlayerX
ld a,(hl)
or a
jr z,.x
dec (hl)
dec (hl)
jr .x
.right:
ld hl,PlayerX
ld a,(hl)
cp 142
jr nc,.x
inc (hl)
inc (hl)
.x:
ld a,(JoyState)
bit 0,a
jr nz,.up
bit 1,a
jr nz,.down
jr .done
.up:
ld hl,PlayerY
ld a,(hl)
cp 26
jr c,.done
dec (hl)
dec (hl)
jr .done
.down:
ld hl,PlayerY
ld a,(hl)
cp 182
jr nc,.done
inc (hl)
inc (hl)
.done:
ret

UpdateShot:
ld a,(JoyState)
bit 4,a
jr nz,.fire
jr .move
.fire:
ld a,(ShotActive)
or a
jr nz,.move
ld a,1
ld (ShotActive),a
ld a,(PlayerX)
add a,4
ld (ShotX),a
ld a,(PlayerY)
sub 6
ld (ShotY),a
.move:
ld a,(ShotActive)
or a
ret z
ld hl,ShotY
ld a,(hl)
sub 6
ld (hl),a
cp 18
ret nc
xor a
ld (ShotActive),a
ret

; ----------------------------------------------------------------
; Alien movement engines. All 10 positions updated from the same phase,
; but each level has a different motion model.
; ----------------------------------------------------------------
UpdateAliens:
ld hl,AlienX
ld de,AlienY
ld bc,AlienVX
ld ix,AlienType
ld iy,AlienAlive
ld a,(Frame)
ld (TmpPhase),a
ld b,0
.loop:
ld a,(iy+0)
or a
jp z,.next
ld a,(MoveEngine)
cp 0
jr z,.eng0
cp 1
jr z,.eng1
cp 2
jr z,.eng2
cp 3
jr z,.eng3
jr .eng4
.eng0:
ld a,(FormationDir)
or a
jr z,.e0r
inc (hl)
jr .e0chk
.e0r:
dec (hl)
.e0chk:
ld a,(hl)
cp #08
jr nc,.e0right
ld a,1
ld (FormationDir),a
ld a,(de)
inc a
ld (de),a
jr .next
.e0right:
cp #88
jr c,.next
xor a
ld (FormationDir),a
ld a,(de)
inc a
ld (de),a
jr .next
.eng1:
ld a,(Frame)
and 3
jr nz,.next
ld a,b
and 1
jr z,.e1a
inc (hl)
inc (hl)
jr .next
.e1a:
dec (hl)
dec (hl)
jr .next
.eng2:
; cheap triangle wave: phase + alien index, reflected at 0/120
ld a,(TmpPhase)
add a,b
and 127
cp 64
jr c,.e2p
ld c,a
ld a,128
sub c
.e2p:
ld (hl),a
ld a,b
and 3
add a,a
add a,34
ld (de),a
jr .next
.eng3:
; paired diagonal rush, reverses at edges
ld a,b
and 1
jr z,.e3r
dec (hl)
ld a,(de)
inc a
ld (de),a
jr .next
.e3r:
inc (hl)
ld a,(de)
dec a
ld (de),a
jr .next
.eng4:
; fast zigzag: horizontal + occasional vertical hop
inc (hl)
ld a,(Frame)
and 15
jr nz,.next
ld a,(de)
xor 12
ld (de),a
.next:
inc hl
inc de
inc bc
inc ix
inc iy
inc b
ld a,b
cp #0a
jp nz,.loop
ret

; ----------------------------------------------------------------
; Meteors. Four hardware sprites, never affected by shot collision.
; ----------------------------------------------------------------
SpawnMeteors:
ld a,#5a
ld (RandomSeed),a
ld hl,MeteorX
ld de,MeteorY
ld b,4
.sm:
call NextRandom
and 127
ld (hl),a
call NextRandom
and 127
add a,32
ld (de),a
inc hl
inc de
djnz .sm
ret

UpdateMeteors:
ld hl,MeteorX
ld de,MeteorY
ld b,4
.loop:
ld a,(hl)
inc a
cp 150
jr c,.sx
xor a
.sx:
ld (hl),a
ld a,(de)
add a,1
cp 190
jr c,.sy
sub 158
.sy:
ld (de),a
inc hl
inc de
djnz .loop
ret

NextRandom:
push bc ; preserve BC pour l’appelant (SpawnMeteors/InitStars
ld a,(RandomSeed) ; utilisent B comme compteur de boucle “djnz” : sans
ld b,a ; cette sauvegarde, cet appel l’ecrasait avec une
add a,a ; valeur pseudo-aleatoire au lieu de le laisser se
xor b ; decrementer normalement, faisant partir la boucle
rrca ; pour un nombre d’iterations imprevisible et
xor #1d ; corrompant la memoire au-dela des tableaux prevus.
ld (RandomSeed),a
pop bc
ret

; ----------------------------------------------------------------
; Collision: shot vs 10 aliens using 8x8 boxes. One hit kills one alien.
; ----------------------------------------------------------------
CollideShotAliens:
ld a,(ShotActive)
or a
ret z
ld hl,AlienX
ld de,AlienY
ld ix,AlienAlive
ld b,0
.ca_loop:
ld a,(ix+0)
or a
jr z,.ca_next
ld a,(ShotX)
sub (hl)
jr c,.ca_next
cp 9
jr nc,.ca_next
ld a,(ShotY)
ld c,a
push de
ld a,(de)
ld d,a
ld a,c
sub d
pop de
jr c,.ca_next
cp #0a
jr nc,.ca_next
xor a
ld (ix+0),a
ld (ShotActive),a
ld a,(AlienAliveCount)
dec a
ld (AlienAliveCount),a
call AddScore
ret
.ca_next:
inc hl
inc de
inc ix
inc b
ld a,b
cp #0a
jp nz,.ca_loop
ret

CollidePlayerMeteors:
ld hl,MeteorX
ld de,MeteorY
ld b,4
.cm:
ld a,(PlayerX)
sub (hl)
jr c,.cm_next
cp #0a
jr nc,.cm_next
ld a,(PlayerY)
ld c,a
push de
ld a,(de)
ld d,a
ld a,c
sub d
pop de
jr c,.cm_next
cp 12
jr nc,.cm_next
call LoseLife
ret
.cm_next:
inc hl
inc de
djnz .cm
ret

AddScore:
ld hl,Score0
inc (hl)
ld a,(hl)
cp #0a
ret c
xor a
ld (hl),a
inc hl
inc (hl)
ld a,(hl)
cp #0a
ret c
xor a
ld (hl),a
inc hl
inc (hl)
ld a,(hl)
cp #0a
ret c
xor a
ld (hl),a
inc hl
inc (hl)
ret

LoseLife:
ld a,(Lives)
or a
ret z
dec a
ld (Lives),a
xor a
ld (ShotActive),a
ld a,72
ld (PlayerX),a
ld a,174
ld (PlayerY),a
ld a,(Lives)
or a
ret nz
ld a,1
ld (GameOver),a
ret

CheckWave:
ld a,(GameOver)
or a
ret nz
ld a,(AlienAliveCount)
or a
ret nz
ld a,(Level)
inc a
cp 5
jr c,.next
xor a
.next:
ld (Level),a
call SpawnWave
ret

; ----------------------------------------------------------------
; Hardware sprite drawing. Sprite slots:
; 0 player, 1 shot, 2..11 aliens, 12..15 meteors.
; ----------------------------------------------------------------
DrawAllSprites:
call ASIC_IN
; player
ld a,(PlayerX)
add a,a
add a,a
ld l,a
ld h,0
ld (#6000),hl
ld a,(PlayerY)
ld (#6002),a
xor a
ld (#6003),a
ld a,5
ld (#6004),a
; shot
ld a,(ShotActive)
or a
jr z,.hideShot
ld a,(ShotX)
add a,a
add a,a
ld l,a
ld h,0
ld (#6008),hl
ld a,(ShotY)
ld (#600a),a
xor a
ld (#600b),a
ld a,5
ld (#600c),a
jr .aliens
.hideShot:
xor a
ld (#600c),a
ld a,255
ld (#600a),a ; Y hors ecran : un sprite a mag=0 reste
; visible a sa derniere position sinon.
.aliens:
ld hl,AlienX
ld de,AlienY
ld ix,AlienAlive
ld b,0
ld iy,#6010
.al:
ld a,(ix+0)
or a
jr z,.aloff
ld a,(hl)
add a,a
add a,a
ld (iy+0),a
xor a
ld (iy+1),a
ld a,(de)
ld (iy+2),a
xor a
ld (iy+3),a
ld a,5
ld (iy+4),a
jr .alnext
.aloff:
xor a
ld (iy+4),a
ld a,255
ld (iy+2),a ; Y hors ecran, meme raison que .hideShot
.alnext:
inc hl
inc de
inc ix
push bc
ld bc,8
add iy,bc
pop bc
inc b
ld a,b
cp #0a
jr nz,.al
; meteors
ld hl,MeteorX
ld de,MeteorY
ld iy,#6060
ld b,4
.me:
ld a,(hl)
add a,a
add a,a
ld (iy+0),a
xor a
ld (iy+1),a
ld a,(de)
ld (iy+2),a
xor a
ld (iy+3),a
ld a,5
ld (iy+4),a
inc hl
inc de
push bc
ld bc,8
add iy,bc
pop bc
djnz .me
call ASIC_OUT
ret

; ----------------------------------------------------------------
; HUD: tiny 4x5 digits, score/lives/level. Rendered directly in screen RAM.
; ----------------------------------------------------------------
DrawHUD:
; efface les 8 premieres lignes video (bandeau HUD), une par une
; avec la bonne adresse entrelacee
ld b,0
.clearline:
push bc
ld a,b
call GetLineAddr
ld b,80
xor a
.cl:
ld (hl),a
inc hl
djnz .cl
pop bc
inc b
ld a,b
cp 8
jr nz,.clearline
; ligne separatrice juste sous le bandeau (ligne video 8)
ld a,8
call GetLineAddr
ld b,80
ld a,#ff
.hline:
ld (hl),a
inc hl
djnz .hline
; draw stars into playfield
call DrawStars
ret

; ----------------------------------------------------------------
; Adressage ecran du CPC (Mode 0/1/2) : la memoire n’est PAS lineaire.
; Une ligne video (0-199) se decompose en :
; - une “ligne de caracteres” (0-24) = ligne div 8
; - une “sous-ligne” (0-7) = ligne mod 8, qui saute par blocs de 2048o
; adresse = SCREEN_BASE + (ligne div 8)*80 + (ligne mod 8)*2048
; Entree : A = ligne video (0-199). Sortie : HL = adresse de debut de
; cette ligne (colonne 0).
; ----------------------------------------------------------------
GetLineAddr:
push bc
push de
ld c,a
and 7
ld l,a
ld h,0
add hl,hl ; index * 2 (mot de 16 bits)
ld de,SubScan2048Table
add hl,de
ld e,(hl)
inc hl
ld d,(hl) ; de = sous-ligne * 2048
ld a,c
srl a
srl a
srl a ; a = ligne de caracteres (0-24)
ld l,a
ld h,0
add hl,hl
ld bc,Line80Table
add hl,bc
ld a,(hl)
inc hl
ld h,(hl)
ld l,a ; hl = ligne de caracteres * 80
add hl,de ; hl += sous-ligne * 2048
ld de,SCREEN_BASE
add hl,de ; hl += base ecran
pop de
pop bc
ret

SubScan2048Table:
dw 0,2048,4096,6144,8192,10240,12288,14336

Line80Table:
dw 0,80,160,240,320,400,480,560,640,720
dw 800,880,960,1040,1120,1200,1280,1360,1440,1520
dw 1600,1680,1760,1840,1920

InitStars:
ld hl,StarX
ld de,StarY
ld b,32
.is:
call NextRandom
and 159
ld (hl),a
call NextRandom
and 159
add a,24
ld (de),a
inc hl
inc de
djnz .is
ret

DrawStars:
ld ix,StarX
ld iy,StarY
ld b,32
.ds:
push bc
ld a,(iy+0) ; StarY = ligne video (24..182)
call GetLineAddr ; hl = debut de cette ligne
ld a,(ix+0) ; StarX
srl a ; /2 : 2 pixels par octet en Mode 0
ld c,a
ld b,0
add hl,bc
ld a,(hl)
xor #11
ld (hl),a
pop bc
inc ix
inc iy
djnz .ds
ret

Animate:
ld hl,Frame
inc (hl)
ld a,(Frame)
and 3
ld (Anim),a
ret

; ----------------------------------------------------------------
; DMA music: three-channel AY list. One list is enough for an ambient loop;
; DMA writes occur during HBL and do not consume the Z80 each frame.
; ----------------------------------------------------------------
InitMusic:
call ASIC_IN
ld hl,MusicList
ld (#6c00),hl
xor a
ld (#6c02),a
ld a,1
ld (#6c0f),a
call ASIC_OUT
ret

; LOAD R,D = 0RDD, PAUSE = 1NNN, REPEAT = 2NNN, LOOP=4001, STOP=4020.
; Notes are intentionally slow and arpeggiated for a classic arcade feel.
MusicList:
dw #0738 ; tone A/B/C on, noise off
dw #000e,#0102 ; A period
dw #080f ; A volume 15
dw #0900,#0a00 ; B/C silent
dw #100f ; ~16 HBL units
dw #000f,#0102
dw #080c
dw #100f
dw #0013,#0101
dw #080a
dw #100f
dw #0017,#0101
dw #0808
dw #100f
dw #0013,#0101
dw #080a
dw #100f
dw #203f ; repeat 64 times
dw #4001
dw #4020
align 2

; ----------------------------------------------------------------
; RAM state (never mapped as cartridge ROM).
; ----------------------------------------------------------------
Level equ #8000
Frame equ #8001
Anim equ #8002
JoyState equ #8003
Lives equ #8004
GameOver equ #8005
PlayerX equ #8006
PlayerY equ #8007
ShotX equ #8008
ShotY equ #8009
ShotActive equ #800a
RandomSeed equ #800b
MoveEngine equ #800c
AlienSpeed equ #800d
AlienSprite equ #800e
AlienAliveCount equ #800f
FormationDir equ #8010
TmpCount equ #8011
TmpX equ #8012
TmpPhase equ #8013
Score0 equ #8014
Score1 equ #8015
Score2 equ #8016
Score3 equ #8017
AlienX equ #8020
AlienY equ #8030
AlienVX equ #8040
AlienType equ #8050
AlienAlive equ #8060
MeteorX equ #8070
MeteorY equ #8074
StarX equ #8080
StarY equ #80a0

; ----------------------------------------------------------------
; Utility ASIC paging.
; ----------------------------------------------------------------
ASIC_IN:
ld bc,#7fb8
out (c),c
ret
ASIC_OUT:
ld bc,#7fa0
out (c),c
ret
WaitVSync:
ld b,#f5
.w0:
in a,(c)
rra
jr nc,.w0
.w1:
in a,(c)
rra
jr c,.w1
ret

; ----------------------------------------------------------------
; Sprite art. Each block is 16 rows x 16 pixels. Generated with DB so the
; source stays editable without a binary asset dependency.
; ----------------------------------------------------------------
SpritePlayer:
db 0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0
db 0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0
db 0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0
db 0,0,0,0,1,1,2,2,2,2,1,1,0,0,0,0
db 0,0,0,1,1,2,2,2,2,2,2,1,1,0,0,0
db 0,0,1,1,2,2,2,2,2,2,2,2,1,1,0,0
db 0,1,1,2,2,2,3,3,3,3,2,2,2,1,1,0
db 1,1,2,2,2,3,3,3,3,3,3,2,2,2,1,1
db 1,2,2,2,3,3,3,3,3,3,3,3,2,2,2,1
db 0,2,2,2,2,3,3,3,3,3,3,2,2,2,2,0
db 0,0,2,2,2,2,3,3,3,3,2,2,2,2,0,0
db 0,0,0,2,2,2,2,3,3,2,2,2,2,0,0,0
db 0,0,0,0,2,2,2,2,2,2,2,2,0,0,0,0
db 0,0,0,0,0,2,2,2,2,2,2,0,0,0,0,0
db 0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0
db 0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0

SpriteShot:
; petit tir vertical visible (avant : “ds 256,0” = totalement transparent)
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

SpriteAlien1:
db 0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0
db 0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0
db 0,0,1,1,1,2,1,1,1,1,2,1,1,1,0,0
db 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 1,1,2,1,1,1,1,1,1,1,1,1,2,1,1,1
db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
db 0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0
db 0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0
db 0,0,1,1,0,0,1,1,1,1,0,0,1,1,0,0
db 0,1,1,0,0,0,1,1,1,1,0,0,0,1,1,0
ds 64,0

SpriteAlien2:
ds 16,0
db 0,0,1,0,0,1,1,1,1,1,1,0,0,1,0,0
db 0,1,1,1,1,1,2,1,1,2,1,1,1,1,1,0
db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 1,1,2,1,1,1,1,1,1,1,1,1,2,1,1,1
db 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
db 0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0
db 0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0
db 0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0
ds 112,0

SpriteAlien3:
ds 32,0
db 0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1
db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 1,1,2,2,1,1,1,1,1,1,1,1,2,2,1,1
db 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
db 0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0
ds 176,0

SpriteAlien4:
ds 48,0
db 0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0
db 0,1,1,2,1,1,1,1,1,1,1,1,2,1,1,0
db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1
db 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
db 0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0
db 0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0
ds 136,0

SpriteMeteor:
ds 32,0
db 0,0,1,1,0,0,0,2,2,0,0,1,1,0,0,0
db 0,1,1,1,1,0,2,2,2,2,0,1,1,1,1,0
db 1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1
db 1,1,2,2,1,1,1,2,1,1,1,2,2,1,1,1
db 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
db 0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0
ds 128,0

; pad cartridge ROM to 16 KiB. RASM can then be used with -cpr or the
; project packaging script to create a 16K CPR; the source is intentionally
; small enough for the first cartridge page.
if $ < #4000
ds #4000-$,#ff
endif

Gracias. Por cierto, asegúrate de inicializar TODOS los valores de CRTC, configurar IM 1, establecer el control predeterminado del PPI, configurar la ROM, el modo, etc., porque algunos dispositivos como el PicoGX pueden haberlos configurado para su propio menú. Así, el juego heredará esos valores, pero si lo ejecutas sin el menú o lo «grabas» en un cartucho, el hardware no estará configurado, especialmente los registros R2, R3, R4 y R7 para las sincronizaciones.

1 me gusta

Me alegro de que lo hayas solucionado.

1 me gusta

Además:

No necesitas esperar al vsync para inicializar los sprites, así que puedes quitar eso para acelerar esa parte de la configuración. Tampoco hace falta dejar la pantalla en blanco usando R6 porque no verás los sprites actualizarse a menos que estén visibles.

Una cosa más: pídele a la herramienta de IA que verifique si el código de números aleatorios que ha elegido es bueno, ya que podría no darte la aleatoriedad que necesitas.

Con suerte, probaré el cartucho en mi GX4000 esta noche para ver si veo el patrón que describes.

1 me gusta

Hice un pequeño experimento. Parecen que los números se repiten tras un ciclo muy corto de solo 10 valores.

¿Qué tal esta versión simplificada del RNG del juego Elite?

rng:
    LD HL,seedValue  ;;  3 ;; Cargar estado.
    LD A,H : ADD A,L ;;  2 ;; Crear nuevo valor mediante suma.
    LD H,L : LD L,A  ;;  2 ;; Construir nuevo estado a partir del antiguo y del nuevo valor.
    LD (rng+1),HL    ;;  5 ;; Guardar estado.
    ;;               ;; -- ;; 
    ;;               ;; 12 ;; 

Probé en mi GX4000 y no vi ningún artefacto visual. No lo he probado en un 6128Plus. Suena a un problema de hardware específico tuyo. Sí cambié el código para configurar todos los registros crtc a los valores por defecto. Estoy usando un monitor TFT moderno conectado por SCART y una fuente de alimentación nueva que compré en eBay, así que es posible que esté ocultando el problema. ¿Estás usando un monitor CRT o un monitor de Amstrad? ¿El patrón aparece en toda la pantalla, solo en la parte gráfica o únicamente donde están/estaban los sprites? Es un poco raro que aparezca y luego se limpie.

GX4000 Jail bars or PSU issue?

Es incomprensible (ver video), no tengo el mismo comportamiento en una versión con corrección del juego entre la GX4000 y Caprice Forever, ¡a pesar de que ambas funcionan perfecto con todos mis juegos!
La GX está equipada con un PicoGX de Rodrick; el emulador es la versión para Windows y, al igual que RASM, funciona a través de Wine en Linux.
Una versión da pantalla negra en la GX con las sugerencias que me dieron, pero va mejor en el emulador.
Sigue estando ese raro error de pantalla, que está limitada en la visualización pero parece tener el número correcto de píxeles, solo que comprimida sobre sí misma.
Y por último, sigo sin sonido.
¿Error de compilación???
Estoy mirando cómo poner mi proyecto en un ZIP, mientras tanto aquí está el video de mis pruebas.
Gracias de nuevo a todos

InvaderGX_final11 (copie).zip (350,2 KB) versión con la corrección que me sugirieron (pantalla negra en gx)

InvaderGX_final11.zip (327,5 KB) versión original (funciona en ambos)

Pantalla comprimida: Tu PlayerX es un byte, con un rango de 0 a 255, pero las coordenadas X del sprite son de 16 bits. Las coordenadas del sprite se basan en el modo 2, por lo que el rango para la visualización es de 0 a 640.

1 me gusta

build_cpr.asm (26,8 KB)

¡Eureka! He hecho un montón de correcciones, en realidad estaba calculando sobre 80 caracteres en lugar de 40 lol.

Todavía quedan algunos problemas con la detección de colisiones y sigo sin música

1 me gusta