Cyclic screen noise during ASIC hardware-sprite bitmap upload

[Hardware/GX4000] Cyclic screen noise during ASIC hardware-sprite bitmap upload (&4000-&4FFF) — real 6128 Plus, RASM/4-bank .cpr

Hi all,

I’m debugging a homebrew GX4000/CPC+ cartridge (RASM, 4-bank .cpr, tested on a real 6128 Plus over RGB SCART) and have hit a display-corruption issue that I’ve isolated fairly precisely through many rounds of testing, but can’t explain. Posting in case it rings a bell for someone with real hardware experience.

Symptom

After the sprite bitmap upload (16 x 256-byte LDIR into &4000-&4FFF), the screen shows a cyclic visual noise — fine repeating diagonal/dot patterns in shifting colours, alternating with brief moments of a clean picture, roughly every few seconds. It looks like composite/RGB sync stress (dot-crawl-like), not a CPU crash — execution keeps running fine throughout (confirmed via other means).

What’s confirmed fine in isolation (each tested alone on real hardware, stable screen, no noise)

  • ASIC unlock sequence (standard 17-byte &BC00 sequence)
  • Gate Array mode select (&7F00,&80 — mode 0, ROMs untouched; note &7F00,&88, which also disables upper ROM, causes total loss of video sync on this hardware — “No signal” on the TV, persisting through reset — separate finding, possibly worth its own thread)
  • Full CRTC init, R0-R9 + R12/R13 (standard Amstrad 50Hz values) via &BC00/&BD00, done while ASIC is unlocked
  • Screen palette + sprite palette upload (2 LDIRs, 32+30 bytes, to &6400/&6422) via the same ASIC I/O page mechanism (&7F00,&B8 / &7F00,&A0)

What reproduces the noise

Uploading the 16 sprite bitmaps (256 bytes each, plain LDIR, to &4000, &4100, &4200 … &4F00) — confirmed in isolation, with everything else above present and stable. The noise appears regardless of:

  • whether sprite registers (&6000-&607F) are zeroed (mag=0, i.e. “not displayed” per the documented format) before or after the bitmap upload
  • whether the 16 LDIRs are done as one continuous burst (~21ms, i.e. longer than one 20ms frame) or spread across multiple frames with a VSync wait between each transfer
  • whether the upload happens before or after CRTC/video init (i.e., whether a picture is even being actively generated yet)

Code (relevant excerpt)

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

InitSprites:
    call ASIC_IN
    ; sprites hidden first (mag=0, Y=255), tried both before/after upload
    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 more identical LDIR blocks, de = #4100, #4200, ... #4f00
    call ASIC_OUT
    ret

Things I’ve ruled out

  • Not a code/logic bug in our own game loop (reproduces with the game loop reduced to an empty WaitVSync / jr loop)
  • Not the CRTC config (confirmed clean in isolation, R0-R9 all present)
  • Not the ASIC unlock/lock mechanism itself (confirmed via the palette upload using the exact same &7FB8/&7FA0 mechanism, no issue there)
  • Not a “sprite visibly rendering garbage mid-upload” issue as far as I can tell — reordering hide-before-upload changed nothing, and the documented mag=0 already means “not displayed”, not “1x size” (I initially assumed the latter, which was wrong)
  • Cross-checked against roudoudou’s published hard-sprite tutorial (programmation assembleur Z80) — our unlock sequence, RMR2 usage (&7F00,&B8/&A0) and LDIR copy technique all match his reference implementation exactly, so it doesn’t look like an implementation mistake on our end.

Emulator note

Interestingly, CaPriCe Forever (Windows) actually runs this cartridge fine visually for the CRTC/ASIC-unlock parts, but locks up completely if CRTC registers 8 or 9 are written at all (even individually, even with harmless values 0/7) — a divergence from real hardware, where R8/R9 are required and fine. cap32 (Linux) doesn’t get far enough to test (seems to ignore/mishandle the 4-bank .cpr entirely, boots straight to firmware BASIC even with CPC 6128+ selected — possibly a separate .cpr-format-compatibility issue on cap32’s end, since a known-good commercial 4/8-bank .cpr — Bomb Jack GX — has the same problem there while working fine on CaPriCe Forever and on real hardware).

Question

Has anyone seen this specific “noise while/after uploading hardware sprite bitmap data” behaviour on real Plus/GX4000 hardware? Is there a documented timing constraint I’m missing — e.g. does the sprite-fetch logic keep reading &4000-&4FFF regardless of the mag/visibility register, such that writing there while a picture is being generated causes visible interference on real silicon (known ASIC errata), and if so is there a recommended workaround (e.g. upload only during a specific raster window, or some additional lock/settle step around the ASIC page-in)?

Happy to share the full source or a test .cpr if useful. Thanks for reading this far!

1 Like

I’ve never seen any screen corruption whilst updating sprites. There can be weird issues if you’re scrolling less than a pixel in whatever mode you’re currently in, but I assume you aren’t doing that?

The main discrepancy between emulated and real sprite display is when there’s a clash between writing sprite data and the screen being updated. None of the current emulators (to my knowledge) handle this correctly.

Thanks for the reply — that “clash between writing sprite data and screen updates” note is very helpful context, and no, I’m not doing any sub-pixel scrolling (removed the soft-scroll registers entirely, they weren’t even enabled).

One clarification that I think changes the picture, though: the noise isn’t a one-off transient tied to the upload itself — it’s a persistent, recurring pattern that keeps cycling (roughly every few seconds) throughout normal gameplay, long after the sprite upload (which only happens once at boot, ~21ms total) has finished. So it doesn’t look like an ongoing bus clash happening every frame — it looks more like the upload leaves the hardware in a slightly different/marginal state afterwards (CRTC timing, ASIC internal state, sync frequency) that then manifests as a recurring sync-lock issue on the TV.

I tried blanking the display entirely during the upload (CRTC R6=0 for the whole transfer, restored afterwards) on the theory that removing any active scanning during the write would avoid the clash — no change, same recurring noise afterwards. So whatever state is left behind survives past the transfer itself, not just during it.

To be fully transparent: my own Amstrad knowledge is about 35 years out of date at this point, and I’m coordinating this debugging through an AI assistant that writes the test code and interprets the hardware debugger (ACE) output for me, since I no longer remember the fine details of the ASIC/CRTC internals myself. So please forgive me if I’m slow to follow up on more detailed technical questions — I’m happy to run any specific test or grab any specific register dump from ACE if you can spell out exactly what to look at.

Concretely, if useful, I can:

  • Run with ACE’s CRTC/ASIC register panels open and grab a screenshot/short capture at any specific point you want (e.g. right before/after the sprite upload, or during a moment when the noise appears)
  • Try specific register values you’d suggest
  • Share the full source or a minimal repro .cpr

Thanks again for taking the time.

If you can post a minimal .cpr with the bug reproducible, I’m sure it can be narrowed down quite quickly :+1:

If you can capture a video that might also help. And, just out of interest, do you have any expansion devices connected to your Plus?

Video if possible and a minimal source code would be useful.

Is this physical pixel dot like crawl pattern (like ZX Spectrum ‘snow’) or more of an interference pattern in the display?

Both Plus and GX4000?

GX4000 has a slightly adjusted main clock frequency to provide a ‘better signal’ with televisions.

Both GX4000 and Plus, I believe, have been seen to show vertical ‘jail bars’ in the image which I think can be filtered out but not pixels as such.

What is the display? a modern flat screen monitor, an old CRT?

The main issue I’ve seen is that writing the pixel data while the sprite is displayed causes the sprite to be visually cut or missing for a short time (normally duration of write). I’ve not seen any other disruption and nothing after having written the sprites.

Is there a specific sprite it happens on? If you enable just one does that show it?

Does it change if the mag is set to simulate mode 0 and mode 1 pixel widths?

1 Like

I missed the bit about mode 0 causing issues, can you share code that makes it do that?

Feels more like hardware issue. I’ve never seen mode 0 cause sync issues.

My thoughts are: Check the power to the computer. Check the 40Mhz crystal. Check the Sync at the Scart connector (good Vsync and Hsync especially when using mode 0), if you are using a Pico GX or similar and not done already, reproduce using code running with basic cartridge attached for example.

1 Like

What is the value of your Stack pointer? I had some garbage displayed on my screen a few months ago, until I realized I forgot to initialize the SP register, and all push & pops were carried out in my double buffer :face_without_mouth:

1 Like

Thanks for your replies, I finally figured out what was causing this bug!
In short, my NextRandom function uses register B as a scratchpad variable without saving it, while SpawnMeteors (and InitStars) use it at the same time as a djnz loop counter. Each call to NextRandom inside the loop was overwriting this counter with a pseudo-random value, causing the number of iterations to go out of control and corrupting memory far beyond the intended arrays.
Fix: push bc / pop bc around the body of NextRandom.
I still have a few bugs that I can’t quite understand yet:
Collisions (shot↔alien, player↔meteor) still aren’t working.
Still no DMA Music (no sound at all).
And worst of all, the screen image is cropped in the top-left corner. My game only displays on the left third of the screen, but if I move the ship to the right, it reappears on the left and moves across again, stopping in the middle on the second pass.
Attached is the source code commented with the help of Claude AI.
If anyone has an idea, thanks again!

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

; Bank 0: full game (code + assets), unchanged.
BANK 0
ORG #0000
INCLUDE “invader.asm”

; — FIX ---------------------------------------------------------
; The GX4000/CPC+ firmware ALWAYS maps cartridge bank 3
; into upper ROM (&C000-&FFFF) on boot, regardless of the actual
; size of the program. A single-bank cartridge (as was
; the case here) doesn’t provide banks 1 to 3: depending on
; the emulator/hardware, the cartridge might not be recognized
; as bootable, even if the code in bank 0 is perfectly valid
; (which is what we observed: the code ran fine during tests,
; but the whole cartridge was never considered bootable).
; So we pad it out to a minimum of 4 banks (64 KB), just like
; any working CPC+ cartridge does (Bomb Jack GX, for example, uses 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 - complete source version
; 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’

; — CRITICAL FIX -------------------------------------------------
; Reserves address &0038 (standard CPC interrupt vector,
; target of RST 38h) with a minimal safe handler, as done by
; any working CPC+ cartridge (verified via binary comparison
; with Bomb Jack GX, which does the exact same thing). Without this,
; if an interrupt fires before “di” can even execute
; (the firmware can leave interrupts enabled right before jumping
; into the cartridge), the CPU jumps right into the middle of an
; instruction in our code → immediate crash, black screen from the
; very start.
; ---------------------------------------------------------------------
ds #0038-$,0
IrqVector:
ei
reti

Boot:
di
ld sp,STACK_TOP
call UnlockASIC
call InitSprites
call InitVideo
call InitPalette
call InitGame
call InitMusic
; — FIX (see report) --------------------------------------------
; “ei” was enabled here without ever executing “im 1” and without any
; interrupt handler (no “reti” anywhere in the source).
; Yet the Gate Array generates a hardware IRQ 300 times/second,
; no matter what: in IM 0 (default mode), this acts like a forced
; “rst &38” jumping RIGHT IN THE MIDDLE of FrameLoop code and
; pushing a return address that is never popped (stack leak on
; every IRQ) → near-instant crash, black screen.
; This engine runs everything via active polling of VSync (WaitVSync) and
; doesn’t need any CPU interrupts: so we do not enable them.
; If interrupts are needed later on, we must add “im 1” BEFORE “ei”
; and provide a real handler at &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, UPPER ROM DISABLED (#7f88). Required so that
; CPU reads from the screen (&C000+) return actual RAM and
; not ROM (the CRTC itself always reads RAM directly for
; display - but DrawStars performs a read-modify-write on
; the screen on EVERY frame: with upper ROM active, this read
; returned ROM contents instead of the actual pixel, likely causing
; part of the observed noise). The “No signal” issue seen previously
; with this value was due to missing CRTC init in that isolated test,
; not #7f88 itself (confirmed with a targeted test,
; full CRTC + #7f88: stable). Thanks to roudoudou (Discord
; Praline / CPCWiki) for spotting this.
ld bc,#7f88
out (c),c
; CRTC: program full table R0-R9 + R12/R13. Previously only
; R12/R13 (screen address) were written; R0-R9 (which define the
; sync timing itself) remained at their default boot values
; when booting directly from cartridge → observed image rolling/scrolling.
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: REMOVED. These writes (&6801-&6804) caused
; generalized distortion/noise across the whole screen on real hardware
; (confirmed by isolated test). This feature was never actually enabled
; anyway (just “prepared” without setting the enable bit),
; so removing it loses no functionality.
; seed a deterministic star field
call InitStars
ret

; ----------------------------------------------------------------
; Standard Amstrad CRTC table (50 Hz) + R12/R13 for screen at &C000.
; Format: (register, value) pairs.
; ----------------------------------------------------------------
InitCRTC:
ld hl,CRTCTable
ld b,CRTCTableLen
.crtc_loop:
push bc
ld bc,#bc00
ld a,(hl)
out (c),a ; select register
inc hl
ld bc,#bd00
ld a,(hl)
out (c),a ; write value
inc hl
pop bc
djnz .crtc_loop
ret

CRTCTable:
; TEST: no longer touching R0/R2/R3/R4/R5/R7 (total horizontal/
; vertical, sync position), matching Bomb Jack GX (verified
; via disassembly: it never writes them, letting firmware
; handle those values). We keep R1 (our supposed bitmap 80 bytes/
; line = 40 characters) and R6 (number of displayed lines).
db 1,#28 ; R1 horizontal displayed (40 char., required for our bitmap)
db 6,#19 ; R6 vertical displayed (25 char. lines)
db 8,0 ; R8 interlace/skew: none
db 9,7 ; R9 character height - 1 (8 lines)
db 12,#30 ; R12 screen address (high byte) → &C000
db 13,0 ; R13 screen address (low byte)
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: completely turns off display (CRTC R6=0) during the entire
; upload, rather than relying on masking individual sprites (Mag=0)
; which apparently doesn’t prevent write/scan bus conflicts on real
; hardware (confirmed by a CPCWiki forum contributor: “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: no lines displayed
; mask all 16 sprites (Mag=0 + Y offscreen) - kept as a safety measure
; in addition to turning off the display
ld hl,#6002
ld de,8
ld b,16
.hide:
ld a,255
ld (hl),a ; Y (low byte) offscreen
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
; restore display (R6 = 25 character lines, normal value)
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 for the caller (SpawnMeteors/InitStars
ld a,(RandomSeed) ; use B as a “djnz” loop counter: without this
ld b,a ; save, this call was overwriting it with a pseudo-random
add a,a ; value instead of letting it decrement normally,
xor b ; causing the loop to run for an unpredictable number of
rrca ; iterations and corrupting memory beyond the intended arrays.
xor #1d
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 offscreen: a sprite with mag=0 stays
; visible at its last position otherwise.
.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 offscreen, same reason as .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:
; clears the first 8 video lines (HUD banner), one by one
; using the proper interlaced address
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
; divider line right under the banner (video line 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

; ----------------------------------------------------------------
; CPC screen addressing (Mode 0/1/2): memory is NOT linear.
; A video line (0-199) is broken down into:
; - a “character line” (0-24) = line div 8
; - a “sub-scanline” (0-7) = line mod 8, which jumps in 2048-byte blocks
; address = SCREEN_BASE + (line div 8)*80 + (line mod 8)*2048
; Input: A = video line (0-199). Output: HL = start address of
; this line (column 0).
; ----------------------------------------------------------------
GetLineAddr:
push bc
push de
ld c,a
and 7
ld l,a
ld h,0
add hl,hl ; index * 2 (16-bit word)
ld de,SubScan2048Table
add hl,de
ld e,(hl)
inc hl
ld d,(hl) ; de = sub-scanline * 2048
ld a,c
srl a
srl a
srl a ; a = character line (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 = character line * 80
add hl,de ; hl += sub-scanline * 2048
ld de,SCREEN_BASE
add hl,de ; hl += screen base
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 = video line (24..182)
call GetLineAddr ; hl = start of this line
ld a,(ix+0) ; StarX
srl a ; /2 : 2 pixels per byte in 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:
; small visible vertical shot (previously: “ds 256,0” = completely 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

Thank you. BTW, Make sure you are initialising ALL CRTC values, set IM 1, setup default PPI control, set Rom, mode etc because some devices like PicoGX may have set them up for it’s menu and so the game will inherit the values but if you run it without the menu and/or ‘burn’ to a cart then the hardware is not configured, especially the R2, R3, R4, R7 for the syncs.

1 Like

Glad to hear you got it sorted.

1 Like

Also:

You don’t need to wait for vsync to initialise the sprites and that can be taken out to speed that setup part up. Also don’t need to blank the screen using R6 because you’ll not see the sprites update unless they are visible.

One more, ask the AI tool to verify the random number code it’s chosen is good because it may not give the randomness you need.

I’ll test the cart hopefully on my gx4000 tonight and see if I see the pattern you describe.

1 Like

I did a small experiment. It seems, the numbers repeat after a very short cycle of only 10 values.

What about this simplified version of the RNG of the game Elite:

rng:
    LD HL,seedValue  ;;  3 ;; Load state.
    LD A,H : ADD A,L ;;  2 ;; Create new value by addition.
    LD H,L : LD L,A  ;;  2 ;; Construct new state from old one and new value.
    LD (rng+1),HL    ;;  5 ;; Save state.
    ;;               ;; -- ;; 
    ;;               ;; 12 ;; 

I tested on my GX4000 and I didn’t see any visual artifacts. I haven’t tested on a 6128Plus. Feels like a hardware issue specific to you. I did change the code to setup all crtc registers to defaults. I am using a modern TFT monitor connected with scart and a new power supply I bought from Ebay it is possible it might be hiding the issue. Are you using CRT or Amstrad monitor? Is the pattern over the entire screen, over the graphics part, just where the sprites are/were? it is a bit strange it shows and then is clean.

GX4000 Jail bars or PSU issue?

It’s incomprehensible (see video), I don’t get the same behavior on a patched version of the game between the GX4000 and Caprice Forever, even though both work perfectly with all my other games!
The GX is equipped with a PicoGX from Rodrick, and the Windows version of the emulator, just like RASM, runs via Wine on Linux.
With one version, it’s a black screen on the GX using the suggestions I was given, but it runs better on the emulator.
There’s still this weird display bug where the screen seems squeezed into a smaller area, even though it looks like it has the right pixel count.
And finally, still no sound.
Compilation error???
I’m looking into how to zip up my project. In the meantime, here’s the video of my tests.
Thanks again to everyone!

InvaderGX_final11 (copie).zip (350.2 KB) version with the suggested fix (black screen on GX)

InvaderGX_final11.zip (327.5 KB) original version (works on both)

Squeezed screen: Your PlayerX is a byte, range 0-255, but sprite X coords is 16 bits. Sprite coords based on mode 2 so range is 0-640 for displayed.

1 Like

build_cpr.asm (26.8 KB)

Eureka, I made a lot of fixes! I was actually calculating for 80 characters instead of 40 lol.

There are still some collision detection issues and still no music

1 Like