; ==============================================================================
; C128 WATERFALL SPECTRUM ANALYZER - PASS 1J
; Target: Commodore 128 (Native Mode / ACME Assembler)
; ==============================================================================

!to "pass1j.prg", cbm

; ------------------------------------------------------------------------------
; CONSTANTS & REGISTERS
; ------------------------------------------------------------------------------
MMU_CR              = $FF00     ; MMU Configuration Register (visible in all banks)
GRAPHM              = $00D8     ; C128 VIC shadow-control / graphics-mode flag

VIC_CTRL1           = $D011
VIC_RASTER          = $D012
VIC_CTRL2           = $D016
VIC_MEMORY          = $D018
VIC_IRR             = $D019
VIC_IMR             = $D01A
VIC_BORDER          = $D020
VIC_BG0             = $D021
CIA2_PRA            = $DD00

SID_V1_FREQ_LO      = $D400
SID_V1_FREQ_HI      = $D401
SID_V1_CTRL         = $D404
SID_V1_ATDC         = $D405
SID_V1_SUREL        = $D406
SID_VOLUME          = $D418

SCREEN_RAM          = $1000     ; 1000 bytes ($1000-$13E7) Video Attributes for D018=$48
BITMAP_RAM          = $2000     ; 8000 bytes ($2000-$3F3F) Hires Bitmap
HIST_RAM            = $4000     ; 3840 bytes ($4000-$4EFF) 40 bins x 96 rows

WATERFALL_TOP_PIXEL = 16        ; Pixel scanline Y offset for waterfall top
MAX_SIGNALS         = 8
NUM_BINS            = 40
NUM_ROWS            = 96

; Zero-Page Allocation (STRICTLY $FB-$FE ONLY for indirect pointers)
ZP_PTR1             = $FB       ; $FB-$FC
ZP_PTR2             = $FD       ; $FD-$FE

; ------------------------------------------------------------------------------
; BASIC LOADER ($1C01)
; 10 SYS 7181
; ------------------------------------------------------------------------------
* = $1C01
!byte $0B, $1C, $0A, $00, $9E, $37, $31, $38, $31, $00, $00, $00

; ------------------------------------------------------------------------------
; LOW RAM BOOTSTRAP & TRAMPOLINES ($1C0D - LOW RAM BELOW $2000)
; ------------------------------------------------------------------------------
* = $1C0D

Bootstrap:
    sei

    ; Save hardware state
    lda $0314
    sta Saved_CINV
    lda $0315
    sta Saved_CINV+1

    lda MMU_CR
    sta Saved_MMU
    lda GRAPHM
    sta Saved_GRAPHM
    lda VIC_CTRL1
    sta Saved_D011
    lda VIC_RASTER
    sta Saved_D012
    lda VIC_CTRL2
    sta Saved_D016
    lda VIC_MEMORY
    sta Saved_D018
    lda VIC_IMR
    sta Saved_D01A
    lda VIC_BORDER
    sta Saved_D020
    lda VIC_BG0
    sta Saved_D021
    lda CIA2_PRA
    sta Saved_DD00

    ; Set MMU to RAM Bank 0 with I/O active ($D000-$DFFF visible)
    lda #$02
    sta MMU_CR

    ; C128-specific: disable KERNAL VIC shadow-register rewriting.
    ; $D8=$FF tells the C128 screen editor not to re-impose its
    ; text/graphics shadow values over our direct VIC setup each IRQ.
    lda #$FF
    sta GRAPHM

    ; Vector CINV to low RAM RasterISR
    lda #<RasterISR
    sta $0314
    lda #>RasterISR
    sta $0315

    ; Setup VIC Raster Interrupt line
    lda VIC_CTRL1
    and #$7F
    sta VIC_CTRL1
    lda #$F0
    sta VIC_RASTER
    lda #$01
    sta VIC_IMR
    sta VIC_IRR

    cli
    jmp MainInit

; ------------------------------------------------------------------------------
; LOW RAM IRQ & VARIABLES
; ------------------------------------------------------------------------------
IRQ_FrameFlag:      !byte 0
IRQ_FrameCount:     !byte 0

Saved_CINV:         !word 0
Saved_MMU:          !byte 0
Saved_GRAPHM:       !byte 0
Saved_D011:         !byte 0
Saved_D012:         !byte 0
Saved_D016:         !byte 0
Saved_D018:         !byte 0
Saved_D01A:         !byte 0
Saved_D020:         !byte 0
Saved_D021:         !byte 0
Saved_DD00:         !byte 0

RasterISR:
    lda VIC_IRR
    and #$01
    beq .chain

    sta VIC_IRR
    inc IRQ_FrameFlag
    inc IRQ_FrameCount

.chain:
    jmp (Saved_CINV)

ExitTrampoline:
    sei

    ; Disable raster IRQ & acknowledge
    lda #$00
    sta VIC_IMR
    lda #$01
    sta VIC_IRR

    ; Silence SID
    lda #$00
    sta SID_VOLUME

    ; Restore KERNAL vector
    lda Saved_CINV
    sta $0314
    lda Saved_CINV+1
    sta $0315

    ; Restore VIC/CIA registers
    lda Saved_D011
    sta VIC_CTRL1
    lda Saved_D012
    sta VIC_RASTER
    lda Saved_D016
    sta VIC_CTRL2
    lda Saved_D018
    sta VIC_MEMORY
    lda Saved_D020
    sta VIC_BORDER
    lda Saved_D021
    sta VIC_BG0
    lda Saved_DD00
    sta CIA2_PRA
    lda Saved_D01A
    sta VIC_IMR

    ; Restore C128 VIC shadow-control state before returning to BASIC.
    lda Saved_GRAPHM
    sta GRAPHM

    ; Restore Saved MMU last
    lda Saved_MMU
    sta MMU_CR
    cli
    rts

; ------------------------------------------------------------------------------
; MAIN ENGINE ($5000 REGION)
; ------------------------------------------------------------------------------
* = $5000

MainInit:
    ; Set VIC Bank 0 ($0000-$3FFF)
    lda CIA2_PRA
    and #$FC                    ; Clear bits 0-1
    ora #$03                    ; Select Bank 0
    sta CIA2_PRA

    ; Set Hires Bitmap Mode
    lda #$3B                    ; Bitmap on, 25 rows
    sta VIC_CTRL1
    lda #$08                    ; 40 cols, no multi-color
    sta VIC_CTRL2
    lda #$48                    ; Screen Matrix at $1000, Bitmap at $2000
    sta VIC_MEMORY

    lda #$00
    sta VIC_BORDER
    sta VIC_BG0

    ; Clear Screen Matrix at $1000 to $10 (White on Black hires attributes)
    lda #<SCREEN_RAM
    sta ZP_PTR1
    lda #>SCREEN_RAM
    sta ZP_PTR1+1
    ldx #4                      ; 4 * 250 = 1000 bytes
    ldy #0
    lda #$10
.clr_screen:
    sta (ZP_PTR1),Y
    iny
    bne .clr_screen
    inc ZP_PTR1+1
    dex
    bne .clr_screen

    ; Clear Bitmap RAM at $2000
    lda #<BITMAP_RAM
    sta ZP_PTR1
    lda #>BITMAP_RAM
    sta ZP_PTR1+1
    ldx #32                     ; 32 * 256 = 8192 bytes
    ldy #0
    lda #$00
.clr_bitmap:
    sta (ZP_PTR1),Y
    iny
    bne .clr_bitmap
    inc ZP_PTR1+1
    dex
    bne .clr_bitmap

    ; Clear History Buffer at $4000
    lda #<HIST_RAM
    sta ZP_PTR1
    lda #>HIST_RAM
    sta ZP_PTR1+1
    ldx #15                     ; 15 * 256 = 3840 bytes
    ldy #0
    lda #$00
.clr_hist:
    sta (ZP_PTR1),Y
    iny
    bne .clr_hist
    inc ZP_PTR1+1
    dex
    bne .clr_hist

    ; Build System Lookup Tables
    jsr BuildTables
    jsr InitHistoryRows
    jsr InitFontPointers
    jsr InitFreqPointers
    jsr InitSignals
    jsr InitSID

    ; Render Static UI Borders & Headings
    jsr DrawStaticUI

MainLoop:
    ; Check RUN/STOP key clean exit
    jsr $FFE1
    beq .exit_bridge

    ; Synchronize with Raster ISR
    lda IRQ_FrameFlag
    beq MainLoop

    lda #0
    sta IRQ_FrameFlag

    ; Engine Pipeline
    jsr AdvanceHistoryHead
    jsr GenerateNoiseFloor
    jsr UpdateSignals
    jsr OverlaySignalsToHistory
    jsr ProcessSpawning
    jsr UpdateReceiver
    jsr RenderWaterfallPhased
    jsr UpdateUI
    jsr UpdateCursor
    jsr UpdateSID

    jmp MainLoop

.exit_bridge:
    jmp ExitTrampoline

; ------------------------------------------------------------------------------
; TABLE GENERATION & INITIALIZATION
; ------------------------------------------------------------------------------
InitHistoryRows:
    lda #<HIST_RAM
    sta ZP_PTR1
    lda #>HIST_RAM
    sta ZP_PTR1+1
    ldx #0
.histrow_loop:
    lda ZP_PTR1
    sta HistoryRowLo,X
    lda ZP_PTR1+1
    sta HistoryRowHi,X

    clc
    lda ZP_PTR1
    adc #40
    sta ZP_PTR1
    bcc +
    inc ZP_PTR1+1
+
    inx
    cpx #96
    bne .histrow_loop
    rts

InitFontPointers:
    lda #<FontData
    sta ZP_PTR1
    lda #>FontData
    sta ZP_PTR1+1
    ldx #0
.fontptr_loop:
    lda ZP_PTR1
    sta FontPtrLo,X
    lda ZP_PTR1+1
    sta FontPtrHi,X

    clc
    lda ZP_PTR1
    adc #8
    sta ZP_PTR1
    bcc +
    inc ZP_PTR1+1
+
    inx
    cpx #59                     ; Chars $20..$5A
    bne .fontptr_loop
    rts

InitFreqPointers:
    lda #<FreqTable
    sta ZP_PTR1
    lda #>FreqTable
    sta ZP_PTR1+1
    ldx #0
.freqptr_loop:
    lda ZP_PTR1
    sta FreqPtrLo,X
    lda ZP_PTR1+1
    sta FreqPtrHi,X

    clc
    lda ZP_PTR1
    adc #8                      ; 7 chars + null terminator
    sta ZP_PTR1
    bcc +
    inc ZP_PTR1+1
+
    inx
    cpx #40
    bne .freqptr_loop
    rts

BuildTables:
    ; 1. Build BitmapRowLo/Hi for character rows 0..24
    lda #<BITMAP_RAM
    sta ZP_PTR1
    lda #>BITMAP_RAM
    sta ZP_PTR1+1
    ldx #0
.row_loop:
    lda ZP_PTR1
    sta BitmapRowLo,X
    lda ZP_PTR1+1
    sta BitmapRowHi,X

    clc
    lda ZP_PTR1
    adc #<320
    sta ZP_PTR1
    lda ZP_PTR1+1
    adc #>320
    sta ZP_PTR1+1

    inx
    cpx #25
    bne .row_loop

    ; 2. Build BitmapScanLo/Hi for pixel Y scanlines 0..199
    ldx #0
.scan_loop:
    txa
    lsr
    lsr
    lsr                         ; Char Row = Y / 8
    tay

    txa
    and #7                      ; Fine Y = Y & 7
    clc
    adc BitmapRowLo,Y
    sta BitmapScanLo,X
    lda BitmapRowHi,Y
    adc #0
    sta BitmapScanHi,X

    inx
    cpx #200
    bne .scan_loop

    ; 3. Build ColOffsetLo/Hi for columns 0..39 (col * 8)
    ldx #0
.col_loop:
    txa
    asl
    asl
    asl                         ; X * 8
    sta ColOffsetLo,X
    lda #0
    rol
    sta ColOffsetHi,X

    inx
    cpx #40
    bne .col_loop
    rts

InitSID:
    lda #$00
    sta SID_V1_CTRL
    lda #$0F
    sta SID_VOLUME
    lda #$00
    sta SID_V1_ATDC
    lda #$F8
    sta SID_V1_SUREL
    rts

InitSignals:
    ldx #0
    lda #0
.initsig_loop:
    sta Sig_Type,X
    sta Sig_Bin,X
    sta Sig_Strength,X
    sta Sig_Life,X
    sta Sig_Timer,X
    sta Sig_Param1,X
    sta Sig_Param2,X
    inx
    cpx #MAX_SIGNALS
    bne .initsig_loop
    rts

; ------------------------------------------------------------------------------
; SPECTRUM HISTORY & NOISE FLOOR
; ------------------------------------------------------------------------------
AdvanceHistoryHead:
    inc HistHead
    lda HistHead
    cmp #96
    bcc +
    lda #0
    sta HistHead
+   rts

GetRandom:
    lda Seed
    beq .init
    asl
    bcc +
    eor #$1D
+   sta Seed
    rts
.init:
    lda #$7A
    sta Seed
    rts

GenerateNoiseFloor:
    ldx HistHead
    lda HistoryRowLo,X
    sta ZP_PTR1
    lda HistoryRowHi,X
    sta ZP_PTR1+1

    ldy #0
.noise_loop:
    jsr GetRandom
    and #$0F
    cmp #14
    bcs .high
    cmp #10
    bcs .med
    lda Seed
    and #$02                    ; 0..2
    jmp .store
.med:
    lda #3
    jmp .store
.high:
    lda #4
.store:
    sta (ZP_PTR1),Y
    iny
    cpy #40
    bne .noise_loop
    rts

; ------------------------------------------------------------------------------
; SIGNAL SIMULATION & 8 PERSONALITIES
; ------------------------------------------------------------------------------
UpdateSignals:
    ldx #0
.sig_loop:
    stx CurrentSigIdx
    lda Sig_Type,X
    bne .sig_active
    jmp .next

.sig_active:
    ; Dec lifetime
    dec Sig_Life,X
    bne +
    lda #0
    sta Sig_Type,X
    jmp .next
+
    ; Dispatch Personality updates
    lda Sig_Type,X
    cmp #2
    beq .j_drift
    cmp #3
    beq .j_fsk
    cmp #4
    beq .j_pulsar
    cmp #5
    beq .j_chirp
    cmp #7
    beq .j_burst
    cmp #8
    beq .j_broadband
    jmp .next

.j_drift:      jmp .update_drift
.j_fsk:        jmp .update_fsk
.j_pulsar:     jmp .update_pulsar
.j_chirp:      jmp .update_chirp
.j_burst:      jmp .update_burst
.j_broadband:  jmp .update_broadband

.update_drift:
    inc Sig_Timer,X
    lda Sig_Timer,X
    cmp Sig_Param2,X
    bcs .drift_do
    jmp .next
.drift_do:
    lda #0
    sta Sig_Timer,X
    lda Sig_Bin,X
    clc
    adc Sig_Param1,X            ; Direction (+1 or -1)
    jsr ClampBin
    sta Sig_Bin,X
    jmp .next

.update_fsk:
    inc Sig_Timer,X
    lda Sig_Timer,X
    cmp #6
    bcs .fsk_do
    jmp .next
.fsk_do:
    lda #0
    sta Sig_Timer,X
    lda Sig_Bin,X
    ldy Sig_Param1,X
    sta Sig_Param1,X
    tya
    sta Sig_Bin,X
    jmp .next

.update_pulsar:
    inc Sig_Timer,X
    lda Sig_Timer,X
    cmp Sig_Param2,X
    bcs .pulsar_do
    jmp .next
.pulsar_do:
    lda #0
    sta Sig_Timer,X
    jmp .next

.update_chirp:
    inc Sig_Timer,X
    lda Sig_Timer,X
    cmp Sig_Param2,X
    bcs .chirp_do
    jmp .next
.chirp_do:
    lda #0
    sta Sig_Timer,X
    inc Sig_Bin,X
    lda Sig_Bin,X
    cmp #40
    bcc .next
    lda #0
    sta Sig_Bin,X
    jmp .next

.update_burst:
    inc Sig_Timer,X
    lda Sig_Timer,X
    cmp Sig_Param2,X
    bcs .burst_do
    jmp .next
.burst_do:
    lda #0
    sta Sig_Timer,X
    lda Sig_Param1,X
    eor #$01                    ; Toggle active (1) / quiet (0)
    sta Sig_Param1,X
    bne .burst_act
    ; Set long quiet duration (20..51 updates)
    jsr GetRandom
    and #$1F
    clc
    adc #20
    sta Sig_Param2,X
    jmp .next
.burst_act:
    ; Set short active duration (2..5 updates)
    jsr GetRandom
    and #$03
    clc
    adc #2
    sta Sig_Param2,X
    jmp .next

.update_broadband:
    inc Sig_Timer,X
    lda Sig_Timer,X
    cmp Sig_Param2,X
    bcs .bb_do
    jmp .next
.bb_do:
    lda #0
    sta Sig_Timer,X
    ; Toggle bloom
    lda Sig_Param1,X
    eor #$01
    sta Sig_Param1,X
    jmp .next

.next:
    ldx CurrentSigIdx
    inx
    cpx #MAX_SIGNALS
    beq .signals_done
    jmp .sig_loop
.signals_done:
    rts

OverlaySignalsToHistory:
    ldx HistHead
    lda HistoryRowLo,X
    sta ZP_PTR1
    lda HistoryRowHi,X
    sta ZP_PTR1+1

    ldx #0
.overlay_loop:
    stx CurrentSigIdx
    lda Sig_Type,X
    bne .overlay_active
    jmp .next_sig

.overlay_active:
    cmp #1                      ; CARRIER
    beq .render_single
    cmp #2                      ; DRIFT
    beq .render_single
    cmp #3                      ; FSK
    beq .render_single
    cmp #4                      ; PULSAR
    beq .render_pulsar
    cmp #5                      ; CHIRP
    beq .render_single
    cmp #6                      ; DUAL
    beq .render_dual
    cmp #7                      ; BURST
    beq .render_burst
    cmp #8                      ; BROADBAND
    beq .render_broadband
    jmp .next_sig

.render_single:
    ldy Sig_Bin,X
    lda Sig_Strength,X
    jsr AddEnergyToBin
    jmp .next_sig

.render_pulsar:
    lda Sig_Timer,X
    cmp #4
    bcs .next_sig
    ldy Sig_Bin,X
    lda Sig_Strength,X
    jsr AddEnergyToBin
    jmp .next_sig

.render_dual:
    ldy Sig_Bin,X
    lda Sig_Strength,X
    jsr AddEnergyToBin

    ; Second bin
    lda Sig_Bin,X
    clc
    adc Sig_Param1,X
    jsr ClampBin
    tay
    lda Sig_Strength,X
    lsr                         ; 50% strength for dual slave
    jsr AddEnergyToBin
    jmp .next_sig

.render_burst:
    lda Sig_Param1,X
    beq .next_sig               ; Quiet phase
    ldy Sig_Bin,X
    lda Sig_Strength,X
    jsr AddEnergyToBin
    jmp .next_sig

.render_broadband:
    lda Sig_Param1,X
    beq .next_sig               ; Quiet phase between blooms
    lda #3                      ; Bloom half-width
    sta TempWidth
    lda Sig_Bin,X
    sec
    sbc TempWidth
    jsr ClampBin
    sta TempBin
.bb_loop:
    ldy TempBin
    lda Sig_Strength,X
    lsr                         ; Broad energy spread
    jsr AddEnergyToBin
    inc TempBin
    lda Sig_Bin,X
    clc
    adc TempWidth
    jsr ClampBin
    cmp TempBin
    bcs .bb_loop

.next_sig:
    ldx CurrentSigIdx
    inx
    cpx #MAX_SIGNALS
    beq +
    jmp .overlay_loop
+   rts

AddEnergyToBin:
    ; Inputs: Y = bin (0..39), A = strength
    sta TempAdd
    lda (ZP_PTR1),Y
    clc
    adc TempAdd
    bcc +
    lda #$FF
+   sta (ZP_PTR1),Y
    rts

ClampBin:
    cmp #250
    bcs .zero
    cmp #40
    bcc .clamp_done
    lda #39
    rts
.zero:
    lda #0
.clamp_done:
    rts

; ------------------------------------------------------------------------------
; RATE-LIMITED & DENSITY-AWARE SIGNAL SPAWNING
; ------------------------------------------------------------------------------
ProcessSpawning:
    ; Rate limit spawn evaluation (Every 16 frames)
    inc SpawnDivider
    lda SpawnDivider
    and #$0F
    beq +
    rts
+
    ; Count active signals
    ldx #0
    ldy #0
.count:
    lda Sig_Type,X
    beq +
    iny
+   inx
    cpx #MAX_SIGNALS
    bne .count

    cpy #MAX_SIGNALS
    bcs .spawn_done

    ; Probability threshold lookup
    lda SpawnProbTable,Y
    sta TempThresh
    jsr GetRandom
    cmp TempThresh
    bcs .spawn_done

    ; Find free slot
    ldx #0
.find_slot:
    lda Sig_Type,X
    beq .spawn_at
    inx
    cpx #MAX_SIGNALS
    bne .find_slot
.spawn_done:
    rts

.spawn_at:
    ; Clean state wipe for recycled slot
    lda #0
    sta Sig_Timer,X
    sta Sig_Param1,X
    sta Sig_Param2,X

    jsr GetRandom
    and #$07
    clc
    adc #1                      ; Type 1..8
    sta Sig_Type,X

    jsr GetRandom
    and #$3F
    jsr ClampBin
    sta Sig_Bin,X

    jsr GetRandom
    ora #$80                    ; Strength $80..$FF
    sta Sig_Strength,X

    ; Lifetime 64..191 updates (No wrapping)
    jsr GetRandom
    and #$7F
    clc
    adc #$40
    sta Sig_Life,X

    ; Explicit Type Initialization
    lda Sig_Type,X
    cmp #2                      ; DRIFT
    beq .init_drift
    cmp #3                      ; FSK
    beq .init_fsk
    cmp #4                      ; PULSAR
    beq .init_pulsar
    cmp #5                      ; CHIRP
    beq .init_chirp
    cmp #6                      ; DUAL
    beq .init_dual
    cmp #7                      ; BURST
    beq .init_burst
    cmp #8                      ; BROADBAND
    beq .init_broadband
    rts

.init_drift:
    lda #1
    sta Sig_Param1,X            ; Step +1
    lda #4
    sta Sig_Param2,X            ; Speed
    rts
.init_fsk:
    lda Sig_Bin,X
    clc
    adc #4
    jsr ClampBin
    sta Sig_Param1,X            ; Alt bin
    rts
.init_pulsar:
    lda #8
    sta Sig_Param2,X            ; Period
    rts
.init_chirp:
    lda #2
    sta Sig_Param2,X            ; Speed
    rts
.init_dual:
    lda #5                      ; Offset +5 bins
    sta Sig_Param1,X
    rts
.init_burst:
    lda #0                      ; Start quiet
    sta Sig_Param1,X
    lda #25                     ; Quiet duration
    sta Sig_Param2,X
    rts
.init_broadband:
    lda #0                      ; Start quiet
    sta Sig_Param1,X
    lda #12                     ; Bloom period
    sta Sig_Param2,X
    rts

; ------------------------------------------------------------------------------
; RECEIVER SPECTRUM ANALYSIS & AFC STATE MACHINE
; ------------------------------------------------------------------------------
UpdateReceiver:
    ; 1. Advance Rx_Bin FIRST if in SCAN mode
    lda Rx_State
    bne .no_scan_adv
    lda #0
    sta Rx_AFC
    inc Rx_Bin
    lda Rx_Bin
    cmp #40
    bcc .no_scan_adv
    lda #0
    sta Rx_Bin
.no_scan_adv:

    ; 2. Measure Spectrum at Current Rx_Bin on Newest Logical Row
    ldx HistHead
    lda HistoryRowLo,X
    sta ZP_PTR1
    lda HistoryRowHi,X
    sta ZP_PTR1+1

    ; Measure CENTER
    ldy Rx_Bin
    lda (ZP_PTR1),Y
    sta Rx_CenterEnergy

    ; Measure LEFT
    tya
    beq .left_zero
    dey
    lda (ZP_PTR1),Y
    sta Rx_LeftEnergy
    jmp +
.left_zero:
    lda #0
    sta Rx_LeftEnergy
+
    ; Measure RIGHT
    ldy Rx_Bin
    cpy #39
    beq .right_zero
    iny
    lda (ZP_PTR1),Y
    sta Rx_RightEnergy
    jmp +
.right_zero:
    lda #0
    sta Rx_RightEnergy
+
    ; 3. Dispatch State Machine via Absolute JMP
    lda Rx_State
    cmp #0
    beq .j_scan
    cmp #1
    beq .j_cand
    cmp #2
    beq .j_cent
    cmp #3
    beq .j_trck
    cmp #4
    beq .j_lock
    cmp #5
    beq .j_lost
    rts

.j_scan:  jmp .state_scan
.j_cand:  jmp .state_candidate
.j_cent:  jmp .state_center
.j_trck:  jmp .state_track
.j_lock:  jmp .state_lock
.j_lost:  jmp .state_lost

.state_scan:
    lda Rx_CenterEnergy
    cmp #$40
    bcs .scan_trigger
    jmp .rx_done
.scan_trigger:
    lda #1                      ; Candidate state
    sta Rx_State
    lda #4
    sta Rx_Timer
    rts

.state_candidate:
    lda Rx_CenterEnergy
    cmp #$30
    bcs .cand_hold
    jmp .to_scan
.cand_hold:
    dec Rx_Timer
    beq .candidate_ready
    jmp .rx_done

.candidate_ready:
    lda #2                      ; Center state
    sta Rx_State
    lda #0
    sta Rx_StableCount
    rts

.state_center:
    jsr PerformAFC
    lda Rx_CenterEnergy
    cmp #$50
    bcs .cent_hold
    jmp .to_scan
.cent_hold:
    ; Check Center Stability
    lda Rx_AFC
    beq .center_stable
    lda #0
    sta Rx_StableCount
    jmp .rx_done
.center_stable:
    inc Rx_StableCount
    lda Rx_StableCount
    cmp #4                      ; Require 4 stable frames to transition
    bcs .center_ready
    jmp .rx_done
.center_ready:
    lda #3                      ; Track state
    sta Rx_State
    rts

.state_track:
    jsr PerformAFC
    jsr CalculateQuality
    lda Rx_Quality
    cmp #60
    bcc +
    lda #4                      ; Lock state
    sta Rx_State
    lda #0
    sta Rx_Timer
    rts
+   lda Rx_CenterEnergy
    cmp #$28
    bcs .rx_done
    lda #5                      ; Lost state
    sta Rx_State
    lda #8
    sta Rx_Timer
    rts

.state_lock:
    lda #0                      ; Clear Rx_AFC before potential evaluation
    sta Rx_AFC
    ; Slower AFC in Lock mode (every 4 updates)
    inc Rx_Timer
    lda Rx_Timer
    and #$03
    bne +
    jsr PerformAFC
+   jsr CalculateQuality
    lda Rx_CenterEnergy
    cmp #$20
    bcs .rx_done
    lda #5                      ; Lost state
    sta Rx_State
    lda #12
    sta Rx_Timer
    rts

.state_lost:
    lda #0
    sta Rx_AFC
    lda Rx_CenterEnergy
    cmp #$45
    bcc +
    lda #3                      ; Reacquired -> Track
    sta Rx_State
    rts
+   dec Rx_Timer
    bne .rx_done
.to_scan:
    lda #0                      ; Back to Scan
    sta Rx_State
.rx_done:
    rts

PerformAFC:
    ; Center wins ties (Strictly greater than required)
    lda Rx_LeftEnergy
    cmp Rx_CenterEnergy
    bcc .check_right
    beq .check_right
    cmp Rx_RightEnergy
    bcc .check_right

    ; Left wins
    lda Rx_Bin
    beq .no_afc
    dec Rx_Bin
    lda #$FF                    ; AFC -1
    sta Rx_AFC
    rts

.check_right:
    lda Rx_RightEnergy
    cmp Rx_CenterEnergy
    bcc .no_afc
    beq .no_afc
    lda Rx_RightEnergy
    cmp Rx_LeftEnergy
    bcc .no_afc
    beq .no_afc

    ; Right wins
    lda Rx_Bin
    cmp #39
    bcs .no_afc
    inc Rx_Bin
    lda #$01                    ; AFC +1
    sta Rx_AFC
    rts

.no_afc:
    lda #0
    sta Rx_AFC
    rts

CalculateQuality:
    ; Safe non-overflowing interference estimation: (Left >> 2) + (Right >> 2)
    lda Rx_LeftEnergy
    lsr
    lsr
    sta TempAdj
    lda Rx_RightEnergy
    lsr
    lsr
    clc
    adc TempAdj
    sta TempAdj

    ; Center Energy / 2
    lda Rx_CenterEnergy
    lsr
    sta TempQual

    lda TempQual
    sec
    sbc TempAdj
    bpl +
    lda #0
+
    cmp #100
    bcc +
    lda #100
+   sta Rx_Quality
    rts

; ------------------------------------------------------------------------------
; PHASED WATERFALL RENDERER (SPLIT-LOOP ZERO-PAGE SAFE)
; ------------------------------------------------------------------------------
RenderWaterfallPhased:
    lda RenderPhase
    sta TempPhaseOffset

    ldx #0
.phase_loop:
    stx CurrentPhaseIdx
    lda TempPhaseOffset
    clc
    adc PhaseStepTable,X        ; displayRow = Phase + offset*16
    cmp #96
    bcs .next_row

    jsr RenderSingleWaterfallRow

.next_row:
    ldx CurrentPhaseIdx
    inx
    cpx #6
    bne .phase_loop

    inc RenderPhase
    lda RenderPhase
    and #15
    sta RenderPhase
    rts

RenderSingleWaterfallRow:
    ; Input: A = displayRow (0..95)
    sta TempDisplayRow

    ; historyRow = (HistHead - displayRow + 96) mod 96
    lda HistHead
    sec
    sbc TempDisplayRow
    bpl +
    clc
    adc #96
+   tax

    lda HistoryRowLo,X
    sta ZP_PTR1
    lda HistoryRowHi,X
    sta ZP_PTR1+1

    ; Scanline Y = WATERFALL_TOP_PIXEL + displayRow
    lda TempDisplayRow
    clc
    adc #WATERFALL_TOP_PIXEL
    tax

    lda BitmapScanLo,X
    sta ZP_PTR2
    lda BitmapScanHi,X
    sta ZP_PTR2+1

    ; Render 40 columns using pointer math
    lda #40
    sta TempBin                 ; Column loop counter

.bin_loop:
    ldy #0
    lda (ZP_PTR1),Y
    lsr
    lsr
    lsr
    lsr
    tax
    lda DitherTable,X
    sta (ZP_PTR2),Y

    ; Advance source pointer ZP_PTR1 by 1
    inc ZP_PTR1
    bne +
    inc ZP_PTR1+1
+
    ; Advance destination pointer ZP_PTR2 by 8
    clc
    lda ZP_PTR2
    adc #8
    sta ZP_PTR2
    bcc +
    inc ZP_PTR2+1
+
    dec TempBin
    bne .bin_loop

    rts

; ------------------------------------------------------------------------------
; UI & TEXT RENDER ENGINE
; ------------------------------------------------------------------------------
DrawStaticUI:
    ; Title Bar
    lda #<Txt_Title
    sta ZP_PTR1
    lda #>Txt_Title
    sta ZP_PTR1+1
    lda #0
    sta StrCol
    sta StrRow
    jsr PrintStringAt

    ; Separator
    lda #<Txt_Sep
    sta ZP_PTR1
    lda #>Txt_Sep
    sta ZP_PTR1+1
    lda #0
    sta StrCol
    lda #1
    sta StrRow
    jsr PrintStringAt

    ; Lower UI Line 1
    lda #<Txt_Line1
    sta ZP_PTR1
    lda #>Txt_Line1
    sta ZP_PTR1+1
    lda #0
    sta StrCol
    lda #16
    sta StrRow
    jsr PrintStringAt

    ; Lower UI Line 2
    lda #<Txt_Line2
    sta ZP_PTR1
    lda #>Txt_Line2
    sta ZP_PTR1+1
    lda #0
    sta StrCol
    lda #17
    sta StrRow
    jsr PrintStringAt
    rts

UpdateUI:
    ; 1. FREQ Telemetry (From 40-entry String Table)
    ldx Rx_Bin
    lda FreqPtrLo,X
    sta ZP_PTR1
    lda FreqPtrHi,X
    sta ZP_PTR1+1
    lda #6                      ; Col 6
    sta StrCol
    lda #16
    sta StrRow
    jsr PrintStringAt

    ; 2. QUAL Telemetry
    lda Rx_Quality
    ldx #0
.qual_100:
    cmp #100
    bcc .qual_10
    sec
    sbc #100
    inx
    jmp .qual_100
.qual_10:
    txa
    ora #$30
    sta Txt_BufQual
    ldx #0
.tens_loop:
    lda Rx_Quality
.rem_loop:
    cmp #100
    bcc +
    sec
    sbc #100
    jmp .rem_loop
+   ldx #0
.t_loop:
    cmp #10
    bcc +
    sec
    sbc #10
    inx
    jmp .t_loop
+   ora #$30
    sta Txt_BufQual+2
    txa
    ora #$30
    sta Txt_BufQual+1

    lda #<Txt_BufQual
    sta ZP_PTR1
    lda #>Txt_BufQual
    sta ZP_PTR1+1
    lda #25                     ; Fixed Column 25
    sta StrCol
    lda #16
    sta StrRow
    jsr PrintStringAt

    ; 3. STATE Telemetry
    ldx Rx_State
    lda StatePtrLo,X
    sta ZP_PTR1
    lda StatePtrHi,X
    sta ZP_PTR1+1
    lda #7
    sta StrCol
    lda #17
    sta StrRow
    jsr PrintStringAt

    ; 4. AFC Telemetry
    lda Rx_AFC
    cmp #$FF
    beq .afc_neg
    cmp #$01
    beq .afc_pos
    lda #$20                    ; Space
    sta Txt_BufAFC
    lda #$30                    ; '0'
    sta Txt_BufAFC+1
    lda #$20
    sta Txt_BufAFC+2
    jmp .print_afc
.afc_neg:
    lda #$2D                    ; '-'
    sta Txt_BufAFC
    lda #$31                    ; '1'
    sta Txt_BufAFC+1
    lda #$20
    sta Txt_BufAFC+2
    jmp .print_afc
.afc_pos:
    lda #$2B                    ; '+'
    sta Txt_BufAFC
    lda #$31                    ; '1'
    sta Txt_BufAFC+1
    lda #$20
    sta Txt_BufAFC+2
.print_afc:
    lda #<Txt_BufAFC
    sta ZP_PTR1
    lda #>Txt_BufAFC
    sta ZP_PTR1+1
    lda #24
    sta StrCol
    lda #17
    sta StrRow
    jsr PrintStringAt
    rts

PrintStringAt:
    lda #0
    sta StrIndex
.print_loop:
    ; Guard against accidental text overrun past column 39
    lda StrCol
    cmp #40
    bcs .print_done

    ldy StrIndex
    lda (ZP_PTR1),Y
    beq .print_done
    sta TempChar

    ; DrawChar reuses ZP_PTR1 as its bitmap destination pointer.
    ; Preserve the string source pointer across the call.
    lda ZP_PTR1
    pha
    lda ZP_PTR1+1
    pha

    ldx StrCol
    ldy StrRow
    lda TempChar
    jsr DrawChar

    pla
    sta ZP_PTR1+1
    pla
    sta ZP_PTR1

    inc StrCol
    inc StrIndex
    jmp .print_loop
.print_done:
    rts

DrawChar:
    ; Inputs: A = ASCII ($20..$5A), X = Col (0..39), Y = Row (0..24)
    sec
    sbc #$20
    bcc .dc_invalid
    cmp #59
    bcs .dc_invalid
    tax
    lda FontPtrLo,X
    sta ZP_PTR2
    lda FontPtrHi,X
    sta ZP_PTR2+1
    jmp .dc_draw
.dc_invalid:
    ldx #31                     ; '?' fallback
    lda FontPtrLo,X
    sta ZP_PTR2
    lda FontPtrHi,X
    sta ZP_PTR2+1

.dc_draw:
    ; Calculate Bitmap destination into ZP_PTR1
    ldx StrCol
    ldy StrRow
    lda BitmapRowLo,Y
    clc
    adc ColOffsetLo,X
    sta ZP_PTR1
    lda BitmapRowHi,Y
    adc ColOffsetHi,X
    sta ZP_PTR1+1

    ; Copy 8 glyph bytes using ZP_PTR1 and ZP_PTR2
    ldy #0
.copy:
    lda (ZP_PTR2),Y
    sta (ZP_PTR1),Y
    iny
    cpy #8
    bne .copy
    rts

UpdateCursor:
    ; 1. Erase previous cursor at PrevCursorBin
    ldx #113                    ; Scanline 113 (below waterfall)
    lda BitmapScanLo,X
    sta ZP_PTR1
    lda BitmapScanHi,X
    sta ZP_PTR1+1

    ldx PrevCursorBin
    clc
    lda ZP_PTR1
    adc ColOffsetLo,X
    sta ZP_PTR1
    lda ZP_PTR1+1
    adc ColOffsetHi,X
    sta ZP_PTR1+1

    ldy #0
    lda #$00
    sta (ZP_PTR1),Y

    ; 2. Draw new cursor at Rx_Bin
    ldx #113
    lda BitmapScanLo,X
    sta ZP_PTR1
    lda BitmapScanHi,X
    sta ZP_PTR1+1

    ldx Rx_Bin
    clc
    lda ZP_PTR1
    adc ColOffsetLo,X
    sta ZP_PTR1
    lda ZP_PTR1+1
    adc ColOffsetHi,X
    sta ZP_PTR1+1

    ldy #0
    lda #$FF
    sta (ZP_PTR1),Y

    lda Rx_Bin
    sta PrevCursorBin
    rts

; ------------------------------------------------------------------------------
; SID AUDIO TELEMETRY (CLEAN RE-GATED ENVELOPES)
; ------------------------------------------------------------------------------
UpdateSID:
    lda Rx_State
    cmp #0                      ; SCAN
    beq .sid_scan
    cmp #1                      ; CANDIDATE
    beq .sid_cand
    cmp #2                      ; CENTER
    beq .sid_track
    cmp #3                      ; TRACK
    beq .sid_track
    cmp #4                      ; LOCK
    beq .sid_lock
    cmp #5                      ; LOST
    beq .sid_lost
    rts

.sid_scan:
    lda IRQ_FrameCount
    and #$1F
    bne +
    lda #$80                    ; Gate off
    sta SID_V1_CTRL
    lda #$20
    sta SID_V1_FREQ_LO
    lda #$04
    sta SID_V1_FREQ_HI
    lda #$81                    ; Noise gate on
    sta SID_V1_CTRL
+   rts

.sid_cand:
    lda IRQ_FrameCount
    and #$07
    bne +
    lda #$10                    ; Gate off
    sta SID_V1_CTRL
    lda #$00
    sta SID_V1_FREQ_LO
    lda #$18
    sta SID_V1_FREQ_HI
    lda #$11                    ; Triangle chirp
    sta SID_V1_CTRL
+   rts

.sid_track:
    lda #$11                    ; Triangle tone proportional to Bin
    sta SID_V1_CTRL
    lda Rx_Bin
    asl
    asl
    asl
    asl
    sta SID_V1_FREQ_LO
    lda #$12
    sta SID_V1_FREQ_HI
    rts

.sid_lock:
    lda #$21                    ; Sawtooth tone
    sta SID_V1_CTRL
    lda Rx_Bin
    asl
    asl
    asl
    asl
    sta SID_V1_FREQ_LO
    lda #$20
    sta SID_V1_FREQ_HI
    rts

.sid_lost:
    lda #$10                    ; Gate off
    sta SID_V1_CTRL
    lda #$40
    sta SID_V1_FREQ_LO
    lda #$08
    sta SID_V1_FREQ_HI
    lda #$11
    sta SID_V1_CTRL
    rts

; ------------------------------------------------------------------------------
; SYSTEM DATA & TABLES
; ------------------------------------------------------------------------------
HistHead:           !byte 0
RenderPhase:        !byte 0
Seed:               !byte $5A
PrevCursorBin:      !byte 0
SpawnDivider:       !byte 0

Rx_Bin:             !byte 20
Rx_State:           !byte 0
Rx_Timer:           !byte 0
Rx_StableCount:     !byte 0
Rx_Quality:         !byte 0
Rx_AFC:             !byte 0
Rx_LeftEnergy:      !byte 0
Rx_CenterEnergy:    !byte 0
Rx_RightEnergy:     !byte 0

StrIndex:           !byte 0
StrCol:             !byte 0
StrRow:             !byte 0

CurrentSigIdx:      !byte 0
CurrentPhaseIdx:    !byte 0
TempPhaseOffset:    !byte 0
TempDisplayRow:     !byte 0
TempChar:           !byte 0
TempWidth:          !byte 0
TempBin:            !byte 0
TempAdd:            !byte 0
TempThresh:         !byte 0
TempQual:           !byte 0
TempAdj:            !byte 0

; Signal Allocation Table (8 slots x 7 bytes)
Sig_Type:           !fill MAX_SIGNALS, 0
Sig_Bin:            !fill MAX_SIGNALS, 0
Sig_Strength:       !fill MAX_SIGNALS, 0
Sig_Life:           !fill MAX_SIGNALS, 0
Sig_Timer:          !fill MAX_SIGNALS, 0
Sig_Param1:         !fill MAX_SIGNALS, 0
Sig_Param2:         !fill MAX_SIGNALS, 0

SpawnProbTable:     !byte 80, 35, 15, 8, 4, 2, 1, 0

DitherTable:
    !byte $00, $00, $10, $11, $24, $4A, $55, $55
    !byte $5B, $6D, $B6, $DB, $EE, $F7, $FE, $FF

PhaseStepTable:     !byte 0, 16, 32, 48, 64, 80

; UI Strings
Txt_Title:          !text "C128 WATERFALL SPECTRUM ANALYZER", 0
Txt_Sep:            !text "================================", 0
Txt_Line1:          !text "FREQ: 433.000 MHZ  QUAL: 000%", 0
Txt_Line2:          !text "STATE: SCAN        AFC:  0 ", 0

Txt_BufQual:        !text "000%", 0
Txt_BufAFC:         !text " 0 ", 0

Txt_SCAN:           !text "SCAN    ", 0
Txt_CAND:           !text "CANDIDAT", 0
Txt_CENT:           !text "CENTER  ", 0
Txt_TRCK:           !text "TRACK   ", 0
Txt_LOCK:           !text "LOCK    ", 0
Txt_LOST:           !text "LOST    ", 0

StatePtrLo:         !byte <Txt_SCAN, <Txt_CAND, <Txt_CENT, <Txt_TRCK, <Txt_LOCK, <Txt_LOST
StatePtrHi:         !byte >Txt_SCAN, >Txt_CAND, >Txt_CENT, >Txt_TRCK, >Txt_LOCK, >Txt_LOST

; Frequency Telemetry Strings (Bins 0..39: 433.000 MHz + Bin*0.250 MHz)
FreqTable:
    !text "433.000", 0
    !text "433.250", 0
    !text "433.500", 0
    !text "433.750", 0
    !text "434.000", 0
    !text "434.250", 0
    !text "434.500", 0
    !text "434.750", 0
    !text "435.000", 0
    !text "435.250", 0
    !text "435.500", 0
    !text "435.750", 0
    !text "436.000", 0
    !text "436.250", 0
    !text "436.500", 0
    !text "436.750", 0
    !text "437.000", 0
    !text "437.250", 0
    !text "437.500", 0
    !text "437.750", 0
    !text "438.000", 0
    !text "438.250", 0
    !text "438.500", 0
    !text "438.750", 0
    !text "439.000", 0
    !text "439.250", 0
    !text "439.500", 0
    !text "439.750", 0
    !text "440.000", 0
    !text "440.250", 0
    !text "440.500", 0
    !text "440.750", 0
    !text "441.000", 0
    !text "441.250", 0
    !text "441.500", 0
    !text "441.750", 0
    !text "442.000", 0
    !text "442.250", 0
    !text "442.500", 0
    !text "442.750", 0

FreqPtrLo:          !fill 40, 0
FreqPtrHi:          !fill 40, 0

FontPtrLo:          !fill 59, 0
FontPtrHi:          !fill 59, 0

; Lookup Tables (Dynamically populated at initialization)
HistoryRowLo:       !fill NUM_ROWS, 0
HistoryRowHi:       !fill NUM_ROWS, 0

BitmapScanLo:       !fill 200, 0
BitmapScanHi:       !fill 200, 0

BitmapRowLo:        !fill 25, 0
BitmapRowHi:        !fill 25, 0

ColOffsetLo:        !fill 40, 0
ColOffsetHi:        !fill 40, 0

; Direct ASCII Font ($20..$5A, 8 bytes per character)
FontData:
    !byte $00,$00,$00,$00,$00,$00,$00,$00 ; Space
    !byte $18,$18,$18,$18,$00,$00,$18,$00 ; !
    !byte $66,$66,$66,$00,$00,$00,$00,$00 ; "
    !byte $36,$36,$7F,$36,$7F,$36,$36,$00 ; #
    !byte $0C,$3E,$03,$1E,$30,$1F,$0C,$00 ; $
    !byte $00,$63,$66,$0C,$18,$33,$63,$00 ; %
    !byte $1C,$36,$1C,$0E,$3B,$33,$1E,$00 ; &
    !byte $06,$06,$0C,$00,$00,$00,$00,$00 ; '
    !byte $0C,$18,$30,$30,$30,$18,$0C,$00 ; (
    !byte $30,$18,$0C,$0C,$0C,$18,$30,$00 ; )
    !byte $00,$66,$3C,$FF,$3C,$66,$00,$00 ; *
    !byte $00,$18,$18,$7E,$18,$18,$00,$00 ; +
    !byte $00,$00,$00,$00,$00,$0C,$0C,$18 ; ,
    !byte $00,$00,$00,$7E,$00,$00,$00,$00 ; -
    !byte $00,$00,$00,$00,$00,$18,$18,$00 ; .
    !byte $03,$06,$0C,$18,$30,$60,$40,$00 ; /
    !byte $3E,$63,$6F,$7B,$73,$63,$3E,$00 ; 0
    !byte $0C,$1C,$0C,$0C,$0C,$0C,$3E,$00 ; 1
    !byte $3E,$63,$06,$1C,$30,$63,$7F,$00 ; 2
    !byte $7F,$06,$0C,$1C,$06,$63,$3E,$00 ; 3
    !byte $0C,$1C,$3C,$6C,$7F,$0C,$0C,$00 ; 4
    !byte $7F,$60,$7E,$03,$03,$63,$3E,$00 ; 5
    !byte $1C,$30,$60,$7E,$63,$63,$3E,$00 ; 6
    !byte $7F,$03,$06,$0C,$18,$18,$18,$00 ; 7
    !byte $3E,$63,$63,$3E,$63,$63,$3E,$00 ; 8
    !byte $3E,$63,$63,$3F,$03,$06,$3C,$00 ; 9
    !byte $00,$18,$18,$00,$00,$18,$18,$00 ; :
    !byte $00,$18,$18,$00,$00,$18,$18,$30 ; ;
    !byte $0C,$18,$30,$60,$30,$18,$0C,$00 ; <
    !byte $00,$00,$7E,$00,$7E,$00,$00,$00 ; =
    !byte $30,$18,$0C,$06,$0C,$18,$30,$00 ; >
    !byte $3E,$63,$06,$0C,$18,$00,$18,$00 ; ?
    !byte $3E,$63,$6F,$69,$6F,$60,$3E,$00 ; @
    !byte $18,$3C,$66,$66,$7C,$66,$66,$00 ; A
    !byte $7C,$66,$66,$7C,$66,$66,$7C,$00 ; B
    !byte $3C,$66,$60,$60,$60,$66,$3C,$00 ; C
    !byte $78,$6C,$66,$66,$66,$6C,$78,$00 ; D
    !byte $7E,$60,$60,$78,$60,$60,$7E,$00 ; E
    !byte $7E,$60,$60,$78,$60,$60,$60,$00 ; F
    !byte $3C,$66,$60,$6E,$66,$66,$3E,$00 ; G
    !byte $66,$66,$66,$7E,$66,$66,$66,$00 ; H
    !byte $3C,$18,$18,$18,$18,$18,$3C,$00 ; I
    !byte $1E,$0C,$0C,$0C,$0C,$6C,$38,$00 ; J
    !byte $66,$6C,$78,$70,$78,$6C,$66,$00 ; K
    !byte $60,$60,$60,$60,$60,$60,$7E,$00 ; L
    !byte $63,$77,$7F,$6B,$63,$63,$63,$00 ; M
    !byte $66,$76,$7E,$7E,$6E,$66,$66,$00 ; N
    !byte $3C,$66,$66,$66,$66,$66,$3C,$00 ; O
    !byte $7C,$66,$66,$7C,$60,$60,$60,$00 ; P
    !byte $3C,$66,$66,$66,$66,$3C,$0E,$00 ; Q
    !byte $7C,$66,$66,$7C,$70,$68,$66,$00 ; R
    !byte $3C,$66,$60,$3C,$06,$66,$3C,$00 ; S
    !byte $7E,$18,$18,$18,$18,$18,$18,$00 ; T
    !byte $66,$66,$66,$66,$66,$66,$3C,$00 ; U
    !byte $66,$66,$66,$66,$66,$3C,$18,$00 ; V
    !byte $63,$63,$63,$6B,$7F,$77,$63,$00 ; W
    !byte $66,$66,$3C,$18,$3C,$66,$66,$00 ; X
    !byte $66,$66,$66,$3C,$18,$18,$18,$00 ; Y
    !byte $7E,$06,$0C,$18,$30,$60,$7E,$00 ; Z
