#!/bin/sh
# retroDE_ps2 status block — Ch219 operator helper.
#
# Reads the retroDE ABI v1.0 + ps2-specific diagnostic registers
# exposed by `ps2_hps_bridge` on the hps2fpga bridge and prints a
# one-screen status block. Run from the HPS Linux on the DE25-Nano
# board after `core_loader.sh load /home/terasic/cores/retroDE_ps2.core.rbf`.
#
# Uses `busybox devmem` rather than `devmem2` to avoid a known
# devmem2 access-size quirk on `0x?4`-suffixed offsets (the older
# devmem2 build throws "Bus error" on ABI_VERSION @ 0x40000004 and
# DMA_DONE_COUNT @ 0x40000024 — busybox devmem reads them cleanly).
#
# Usage:
#   ./ps2_status.sh           # one-shot snapshot
#   ./ps2_status.sh --delta   # take two snapshots 500ms apart, show counter deltas
#
# Exits 0 if CORE_ID matches "PS2\0" and CORE_STATUS bit [5]
# (hdmi_i2c_error) is clear, else nonzero. Suitable for automation.

set -u

BASE="${PS2_BRIDGE_BASE:-0x40000000}"
DEVMEM="${DEVMEM:-busybox devmem}"
MODE="${1:-one-shot}"

# Register offsets (matches retroDE ABI v1.0 + ps2 diagnostics; see
# rtl/platform/ps2_hps_bridge.sv and docs/hardware/de25_nano_bringup.md).
OFF_CORE_ID=0x000
OFF_ABI_VERSION=0x004
OFF_CORE_STATUS=0x008
OFF_CORE_CAPS=0x00C
OFF_CORE_CTRL=0x010
OFF_CORE_PULSE=0x014
OFF_FRAME_COUNT=0x020
OFF_DMA_DONE=0x024
OFF_RASTER_OF=0x028
# Ch222 input latches + Ch226 DS2 stub (added to the snapshot in Ch236
# so operators can confirm the input-latch landing on real silicon).
OFF_INPUT_P1=0x040
OFF_INPUT_P2=0x044
OFF_INPUT_P1_RAW=0x048
OFF_DS2_STATUS=0x0F0
OFF_DS2_BUTTONS=0x0F4
# Ch245 — OSD registers (sibling-ABI offsets, now wired to the
# platform osd_overlay + osd_menu_fsm).
OFF_OSD_CTRL=0x100
OFF_OSD_STATUS=0x104
OFF_OSD_TRIGGER=0x108
OFF_OSD_CFG0=0x110
OFF_OSD_CFG1=0x114

read_reg() {
    # $1 = offset (hex string with 0x prefix)
    # Returns the 32-bit value as 0x%08x via stdout.
    local addr=$(printf "0x%08x" $(( BASE + $1 )) )
    $DEVMEM "$addr" w
}

bit_set() {
    # $1 = value, $2 = bit index → echo "1" if bit set, "0" if not.
    local v=$1 b=$2
    if [ $(( (v >> b) & 1 )) -eq 1 ]; then echo "1"; else echo "0"; fi
}

print_status() {
    local label=$1
    local id abi caps status ctrl frame dma raster
    id=$(read_reg $OFF_CORE_ID)
    abi=$(read_reg $OFF_ABI_VERSION)
    caps=$(read_reg $OFF_CORE_CAPS)
    status=$(read_reg $OFF_CORE_STATUS)
    ctrl=$(read_reg $OFF_CORE_CTRL)
    frame=$(read_reg $OFF_FRAME_COUNT)
    dma=$(read_reg $OFF_DMA_DONE)
    raster=$(read_reg $OFF_RASTER_OF)

    printf "retroDE_ps2 core status [%s] @ %s\n" "$label" "$(date)"
    printf "================================================\n"
    printf "  CORE_ID     : %s  " "$id"
    if [ "$id" = "0x50533200" ]; then
        printf '"PS2\\0"  ✓ ps2 fabric loaded\n'
    else
        printf "  ✗ wrong/no fabric loaded (expected 0x50533200)\n"
    fi
    printf "  ABI_VERSION : %s  " "$abi"
    if [ "$abi" = "0x00000100" ]; then
        printf "(v1.0)  ✓ retroDE ABI v1.0\n"
    else
        printf "  ✗ unexpected ABI\n"
    fi
    # Ch246 — CORE_CAPS now advertises OSD geometry per the sibling-ABI
    # bit layout (matches nes_hps_bridge.sv). Decode the fields so
    # operators can spot a misadvertisement at a glance.
    local caps_cols caps_rows caps_2p caps_analog caps_savestates caps_save
    caps_save=$((       caps        & 0x1   ))
    caps_savestates=$(( (caps >> 1) & 0x1   ))
    caps_2p=$((         (caps >> 2) & 0x1   ))
    caps_analog=$((     (caps >> 3) & 0x1   ))
    caps_cols=$((       (caps >> 8) & 0xff  ))
    caps_rows=$((       (caps >> 16) & 0x1f ))
    printf "  CORE_CAPS   : %s  (osd=%dx%d save=%d ss=%d 2p=%d analog=%d)\n" \
        "$caps" "$caps_cols" "$caps_rows" "$caps_save" "$caps_savestates" "$caps_2p" "$caps_analog"
    printf "\n"

    # CORE_STATUS bit decode
    local sv=$((status))
    printf "  CORE_STATUS : %s\n" "$status"
    printf "    [0] loaded            : %s\n" "$(bit_set $sv 0)"
    # Ch251 inverted core_halt semantics: pre-Ch251 the bootlet halted
    # after a single paint (lit = healthy); the animated demo now loops
    # forever (unlit = healthy, lit = EE stuck on an unexpected SYSCALL).
    printf "    [1] core_halt         : %s  " "$(bit_set $sv 1)"
    if [ "$(bit_set $sv 1)" = "0" ]; then
        printf "✓ Ch251 animated demo running (expected)\n"
    else
        printf "✗ EE halted — pre-Ch251 sentinel, or animated loop crashed/SYSCALL'd\n"
    fi
    printf "    [2] dma_done_seen     : %s\n" "$(bit_set $sv 2)"
    printf "    [3] frame_seen        : %s  (lit = PCRTC delivered ≥1 frame)\n" "$(bit_set $sv 3)"
    printf "    [4] hdmi_init_done    : %s  (lit = ADV7513 LUT walk complete)\n" "$(bit_set $sv 4)"
    printf "    [5] hdmi_i2c_error    : %s  " "$(bit_set $sv 5)"
    if [ "$(bit_set $sv 5)" = "0" ]; then printf "✓ no I²C NACKs\n"; else printf "✗ NACK watchdog LATCHED — see runbook triage\n"; fi
    printf "    [6] raster_overflow   : %s  " "$(bit_set $sv 6)"
    if [ "$(bit_set $sv 6)" = "0" ]; then printf "✓ raster healthy\n"; else printf "✗ raster FIFO overflowed — Ch172 backpressure broken?\n"; fi
    printf "\n"

    # CORE_CTRL bit decode
    local cv=$((ctrl))
    printf "  CORE_CTRL   : %s\n" "$ctrl"
    printf "    [0] reset             : %s  (1 = PS2 design held in reset)\n" "$(bit_set $cv 0)"
    printf "    [1] rom_loaded        : %s  (ABI-shape latch, no functional effect yet)\n" "$(bit_set $cv 1)"
    printf "    [2] pause             : %s  (ABI-shape latch, no functional effect yet)\n" "$(bit_set $cv 2)"
    printf "\n"

    printf "  Counters:\n"
    printf "    FRAME_COUNT           : %s  (advances at ~60Hz once PCRTC is alive)\n" "$frame"
    printf "    DMA_DONE_COUNT        : %s\n" "$dma"
    printf "    RASTER_OVERFLOW_COUNT : %s  (should be 0 under Ch172 backpressure)\n" "$raster"
    printf "\n"

    # Ch222 / Ch226 / Ch235 input-latch readbacks. These prove the
    # HPS-to-bridge half of the input path is alive on real silicon.
    # PS2-fabric consumption (Ch234 sio2_input_stub via IOP) is sim-
    # validated by tb_bridge_iop_pad_input but not wired into the
    # synth top yet — so non-zero values here mean "the bridge latch
    # landed" rather than "an in-fabric consumer saw it."
    local p1 p2 p1raw ds2s ds2b
    p1=$(read_reg $OFF_INPUT_P1)
    p2=$(read_reg $OFF_INPUT_P2)
    p1raw=$(read_reg $OFF_INPUT_P1_RAW)
    ds2s=$(read_reg $OFF_DS2_STATUS)
    ds2b=$(read_reg $OFF_DS2_BUTTONS)

    printf "  Input latches (Ch222) + DS2 mirror (Ch226):\n"
    printf "    INPUT_P1              : %s\n" "$p1"
    printf "    INPUT_P2              : %s\n" "$p2"
    printf "    INPUT_P1_RAW          : %s\n" "$p1raw"
    # Ch248 — DS2_STATUS is now driven by the platform ds2_controller
    # (via the bridge's new ds2_connected_i / ds2_error_i input ports).
    # Bit layout: [0]=connected, [1]=error, [2]=1 (PS2-local legacy bit).
    local ds2_conn ds2_err
    ds2_conn=$((    $ds2s        & 0x1 ))
    ds2_err=$((    ($ds2s >> 1)  & 0x1 ))
    printf "    DS2_STATUS            : %s  ([0]=connected=%d [1]=error=%d [2]=reserved=1)\n" \
        "$ds2s" "$ds2_conn" "$ds2_err"
    # DS2_BUTTONS is now the live wired-controller bitmap (Ch248); the
    # Ch226 INPUT_P1 mirror was removed because it blocked retrodesd's
    # ds2_poll_thread from seeing real button state.
    printf "    DS2_BUTTONS           : %s  (live ds2_controller readback — Ch248)\n" "$ds2b"
    if [ "$ds2_conn" = "1" ]; then
        printf "    → controller plugged in; ds2_poll_thread should be writing INPUT_P1/RAW from this.\n"
    else
        printf "    → controller unplugged (DS2_STATUS[0]=0). Plug a DS2 wired pad into\n"
        printf "      the DE25 GPIO header (CLK/CMD/DATA/ATTN = PIN_H16/Y1/C2/P1) to wake the path.\n"
    fi

    # Ch245 — platform OSD register surface. Sibling-ABI layout; the
    # shared retroDE_splash overlay + menu FSM is wired to these.
    local octrl ostatus otrig ocfg0 ocfg1
    octrl=$(read_reg   $OFF_OSD_CTRL)
    ostatus=$(read_reg $OFF_OSD_STATUS)
    otrig=$(read_reg   $OFF_OSD_TRIGGER)
    ocfg0=$(read_reg   $OFF_OSD_CFG0)
    ocfg1=$(read_reg   $OFF_OSD_CFG1)

    printf "\n  OSD registers (Ch245 platform OSD):\n"
    printf "    OSD_CTRL              : %s  ([0]=enable [2]=force_open [3]=force_close,self-clear)\n" "$octrl"
    printf "    OSD_STATUS            : %s  ([0]=osd_active [12:8]=cursor_row)\n" "$ostatus"
    printf "    OSD_TRIGGER           : %s  ([4]=A [5]=B [6]=down [7]=up [16]=open [12:8]=row)\n" "$otrig"
    printf "    OSD_CFG0              : %s  ([5:0]=cols [12:8]=rows [23:16]=origin_x_chars [31:24]=origin_y_chars)\n" "$ocfg0"
    printf "    OSD_CFG1              : %s  ([4:0]=menu_first_row [12:8]=menu_last_row [23:16]=cursor_attr)\n" "$ocfg1"
    # Decode CFG0 fields for human readability.
    local cols rows origx origy origx_pix origy_pix
    cols=$((  ocfg0        & 0x3f ))
    rows=$(( (ocfg0 >> 8 ) & 0x1f ))
    origx=$(((ocfg0 >> 16) & 0xff ))
    origy=$(((ocfg0 >> 24) & 0xff ))
    origx_pix=$(( origx * 16 ))
    origy_pix=$(( origy * 16 ))
    printf "      → cols=%d rows=%d origin=(%d,%d) chars = (%d,%d) px at 2× scale\n" \
        "$cols" "$rows" "$origx" "$origy" "$origx_pix" "$origy_pix"
}

print_delta() {
    # Ch253 — two snapshots 2 s apart so a Ch251.5 ~1 Hz heartbeat
    # toggle is captured reliably regardless of phase (was 500 ms in
    # Ch219 when the bootlet was one-shot and counter Δ didn't matter
    # for blink rate).
    local f1 d1 r1 f2 d2 r2
    f1=$(read_reg $OFF_FRAME_COUNT)
    d1=$(read_reg $OFF_DMA_DONE)
    r1=$(read_reg $OFF_RASTER_OF)
    sleep 2
    f2=$(read_reg $OFF_FRAME_COUNT)
    d2=$(read_reg $OFF_DMA_DONE)
    r2=$(read_reg $OFF_RASTER_OF)
    local df dd dr
    df=$(( f2 - f1 ))
    dd=$(( d2 - d1 ))
    dr=$(( r2 - r1 ))
    printf "\n  Counter Δ over 2 s:\n"
    printf "    FRAME_COUNT           : %s → %s  Δ=%d  (≈ 120 if PCRTC is alive @ 60 Hz)\n" "$f1" "$f2" "$df"
    printf "    DMA_DONE_COUNT        : %s → %s  Δ=%d  (validated band 0–2 per 2 s — liveness cue, not a clock)\n" "$d1" "$d2" "$dd"
    printf "    RASTER_OVERFLOW_COUNT : %s → %s  Δ=%d  (MUST stay 0 — Ch172 backpressure)\n" "$r1" "$r2" "$dr"

    # Ch253 — Ch251+ animated-demo health verdict. Rolls up the
    # individual signals into a single PASS/FAIL block so the
    # operator can run `ps2_status.sh --delta` and know in <3 s
    # whether the field unit is healthy.
    local status_final halt_bit i2c_bit raster_bit ds2_conn_bit
    status_final=$(read_reg $OFF_CORE_STATUS)
    halt_bit=$(bit_set $((status_final)) 1)
    i2c_bit=$(bit_set  $((status_final)) 5)
    raster_bit=$(bit_set $((status_final)) 6)
    ds2_conn_bit=$(( $(read_reg $OFF_DS2_STATUS) & 0x1 ))

    local verdict_ok=1
    printf "\n  Ch251+ animated-demo health verdict:\n"

    if [ "$df" -ge 100 ] && [ "$df" -le 140 ]; then
        printf "    [ ✓ ] PCRTC alive               (FRAME_COUNT Δ=%d ≈ 120 @ 60 Hz)\n" "$df"
    else
        printf "    [ ✗ ] PCRTC stalled/off-rate    (FRAME_COUNT Δ=%d, expected 100–140)\n" "$df"
        verdict_ok=0
    fi

    if [ "$raster_bit" = "0" ] && [ "$dr" -eq 0 ]; then
        printf "    [ ✓ ] Raster healthy            (RASTER_OVERFLOW_COUNT stable + bit[6] clear)\n"
    else
        printf "    [ ✗ ] Raster overflowing        (Δ=%d / status bit[6]=%s — Ch172 broken?)\n" "$dr" "$raster_bit"
        verdict_ok=0
    fi

    # Ch253 / Ch254 — this counter is a LIVENESS CUE, not a precision
    # timer. The bootlet ties DMAC DONE 1:1 to the visible heartbeat
    # toggle, so DMA_DONE Δ over 2 s is a faithful proxy for the blink.
    # Hardware-validated Δ band is {0, 1, 2}; ~1.2 s fixed per-iter
    # overhead (DMAC drain + 17-SPRITE GS rasterize) puts the cadence
    # around 0.5 Hz with natural ±0.5 s jitter — see bake.py comment
    # block and de25_nano_bringup.md Ch254 for the empirical model.
    # Δ=0 is a 2 s phase-miss not a failure; rerun protocol.
    if [ "$dd" -ge 1 ] && [ "$dd" -le 2 ]; then
        printf "    [ ✓ ] DMAC repaint liveness     (DMA_DONE Δ=%d over 2 s; bootlet animating)\n" "$dd"
    elif [ "$dd" -eq 0 ]; then
        printf "    [ ? ] DMAC repaint window miss  (Δ=0 — likely 2 s phase miss; rerun. Persistent Δ=0 = bootlet stuck)\n"
        # Don't fail the verdict on a single Δ=0; rerun is the right
        # protocol. Persistent stuck-state shows up as the operator
        # running --delta twice in a row and getting Δ=0 both times.
    else
        printf "    [ ? ] DMAC repaint off-rate     (Δ=%d, validated band is 0–2 over 2 s — investigate)\n" "$dd"
        verdict_ok=0
    fi

    if [ "$halt_bit" = "0" ]; then
        printf "    [ ✓ ] EE core not halted        (CORE_STATUS[1]=0)\n"
    else
        printf "    [ ✗ ] EE halted                 (CORE_STATUS[1]=1 — animated loop crashed/SYSCALL)\n"
        verdict_ok=0
    fi

    if [ "$i2c_bit" = "0" ]; then
        printf "    [ ✓ ] HDMI I²C clean            (CORE_STATUS[5]=0)\n"
    else
        printf "    [ ✗ ] HDMI I²C NACKed           (CORE_STATUS[5]=1 — see runbook triage)\n"
        verdict_ok=0
    fi

    if [ "$ds2_conn_bit" = "1" ]; then
        printf "    [ ✓ ] DS2 controller plugged    (DS2_STATUS[0]=1)\n"
    else
        printf "    [ — ] DS2 controller unplugged  (informational — Select+Start menu needs a pad)\n"
    fi

    if [ "$verdict_ok" -eq 1 ]; then
        printf "\n    ──> Ch251+ field health: PASS\n"
    else
        printf "\n    ──> Ch251+ field health: FAIL (see flags above)\n"
    fi

    # Build-time profile reminder. The script cannot read VRAM_ENABLE_READ2
    # at runtime (it is baked into the bitstream); the sim-side tripwire
    # in vram_bram_stub.sv catches a bad profile during iverilog/verilator
    # elaboration, but Quartus does not honour it (the guard sits inside
    # `translate_off`). Real synth-side protection is the explicit
    # parameter override in de25_nano_psmct32_raster_demo_top.sv.
    printf "\n  Profile note: hardware build assumes VRAM_ENABLE_READ2=0 (Ch252).\n"
    printf "                Re-enabling read2 at VRAM_BYTES=512 KiB will blow the\n"
    printf "                Agilex 5 M20K budget at fit time. See\n"
    printf "                docs/decisions/0006-vram-roadmap.md for triggers.\n"
}

print_status "snapshot"
if [ "$MODE" = "--delta" ]; then
    print_delta
fi

# Exit status: 0 if CORE_ID + clean I²C, nonzero otherwise.
id_ok=$(read_reg $OFF_CORE_ID)
status_check=$(read_reg $OFF_CORE_STATUS)
i2c_err=$(bit_set $((status_check)) 5)
if [ "$id_ok" = "0x50533200" ] && [ "$i2c_err" = "0" ]; then
    exit 0
else
    exit 1
fi
