// retroDE_ps2 — tb_top_psmct32_feeder_cube_demo (Ch343)
//
// LOCAL smoke for the AUTHENTIC cube board fixtures (dump-derived; this TB is NOT in the committed
// regression — it needs the local cube .mem from `python3 tools/gs_make_cube_fixture.py`). It runs the
// FULL boot path that the feeder_cube de25 profile fits, so it exercises the cube-specific deltas that
// the Ch342 persp gate (which backdoored a 16x16 texture) did not:
//   * the 64x64 cube texture upload via the QWC=1030 bootlet GIF DMA into a 32 KiB EE RAM, and
//   * the 27 translated perspective triangles rendered through the feeder + S1 perspective path.
//
// Proofs:
//   TEX  — after boot, VRAM at the bound TBP (=64) matches the cube texels byte-for-byte (upload worked).
//   SEQ  — the feeder emits perspective ST (not UV) for all 27 tris: ST==81 (27*3), UV==0.
//   DRAW — the captured frame is non-trivial textured content: enough DE pixels, multiple distinct colors.

`timescale 1ns/1ps

module tb_top_psmct32_feeder_cube_demo;

    localparam int H_ACTIVE = 64, V_ACTIVE = 64;
    localparam int TEXW = 64, TEXH = 64;
    localparam int TBP  = 128;             // relocated past the Z buffer (ZBP=2=word 4096); see tool
    localparam int TEX_W0 = TBP*64;        // VRAM word base = TBP*64 = 8192
    localparam int NTRI = 6;       // Ch343 — one isolated corner-view cube (3 faces x 2 tris), re-fit to fill the frame

    logic clk, rst_n;
    initial clk = 1'b0;
    always #5 clk = ~clk;

    logic        core_go;
    logic [7:0]  r, g, b;
    logic        hsync, vsync, de;
    logic        core_halt, dma_done_seen, frame_seen, raster_overflow;
    logic        frame_toggle, dma_done_toggle;
    logic        feeder_go_tb, feeder_ready_tb;

    top_psmct32_raster_demo_bram #(
        .H_ACTIVE(H_ACTIVE), .V_ACTIVE(V_ACTIVE),
        .VRAM_BYTES(64*1024), .RAM_SIZE_BYTES(32*1024), .PSMCT32_SWIZZLE(1'b0),
        // matches GS_FEEDER_PERSP_DEMO / feeder_cube: ABE=0 S1 perspective path, tiling OFF.
        .COMBINED_TAZ(1'b0), .TILE_LOCAL(1'b0), .TILE_COLS(1), .TILE_ROWS(1),
        .TILE_MULTIPRIM(1'b0), .TILE_PRIM_COUNT(1), .TILE_FIFO_DEPTH(8),
        .BIN_BUFFER_ENABLE(1'b0), .HEARTBEAT_SPLICE_ENABLE(1'b0),
        .FEEDER_ENABLE(1'b1), .FEEDER_STG_WORDS(256),
        .PERSPECTIVE_CORRECT(1'b1)
    ) dut (
        .clk(clk), .rst_n(rst_n), .core_go(core_go),
        .r(r), .g(g), .b(b), .hsync(hsync), .vsync(vsync), .de(de),
        .core_halt(core_halt), .dma_done_seen(dma_done_seen),
        .frame_seen(frame_seen), .raster_overflow(raster_overflow),
        .frame_toggle(frame_toggle), .dma_done_toggle(dma_done_toggle),
        .joy_a_pressed_i(1'b0), .joy_b_pressed_i(1'b0),
        .feeder_stg_we_i(1'b0), .feeder_stg_waddr_i(12'd0), .feeder_stg_wdata_i(64'd0),
        .feeder_go_i(feeder_go_tb),
        .feeder_ready_o(feeder_ready_tb), .feeder_records_o(), .feeder_waits_o()
    );

    // ---- feeder register-sequence monitor (perspective => ST, not UV) ----
    int n_st, n_uv; logic [7:0] seq_first [0:8]; int seq_n;
    always_ff @(posedge clk) begin
        if (!rst_n) begin n_st<=0; n_uv<=0; seq_n<=0; end
        else if (dut.feeder_gif_reg_wr_en) begin
            if (dut.feeder_gif_reg_num==8'h02) n_st<=n_st+1;
            if (dut.feeder_gif_reg_num==8'h03) n_uv<=n_uv+1;
            if ((dut.feeder_gif_reg_num==8'h01 || dut.feeder_gif_reg_num==8'h02 || dut.feeder_gif_reg_num==8'h05) && seq_n<9) begin
                seq_first[seq_n]<=dut.feeder_gif_reg_num; seq_n<=seq_n+1;
            end
        end
    end

    // ---- diagnostic raster probes (split geometry vs Z/mask reject) ----
    int dbg_inside, dbg_emit, dbg_ztst_pix; time dbg_last_emit;
    always_ff @(posedge clk) begin
        if (!rst_n) begin dbg_inside<=0; dbg_emit<=0; dbg_ztst_pix<=0; dbg_last_emit<=0; end
        else begin
            if (dut.u_gs.s2_valid_q && dut.u_gs.s2_inside_q) dbg_inside<=dbg_inside+1;
            if (dut.u_gs.s2_ztest_pixel)                     dbg_ztst_pix<=dbg_ztst_pix+1;
            if (dut.raster_pixel_emit) begin dbg_emit<=dbg_emit+1; dbg_last_emit<=$time; end
        end
    end

    // ---- frame capture ----
    logic [7:0] cap_r[0:V_ACTIVE-1][0:H_ACTIVE-1], cap_g[0:V_ACTIVE-1][0:H_ACTIVE-1], cap_b[0:V_ACTIVE-1][0:H_ACTIVE-1];
    logic       cap_de[0:V_ACTIVE-1][0:H_ACTIVE-1];
    bit capture_armed;
    initial begin
        for (int y=0;y<V_ACTIVE;y++) for (int x=0;x<H_ACTIVE;x++) begin cap_r[y][x]=0;cap_g[y][x]=0;cap_b[y][x]=0;cap_de[y][x]=0; end
        capture_armed=1'b0;
    end
    logic [31:0] hcnt_d, vcnt_d;
    always_ff @(posedge clk) begin
        if (!rst_n) begin hcnt_d<=0; vcnt_d<=0; end
        else begin hcnt_d<=32'(dut.u_pcrtc.hcnt); vcnt_d<=32'(dut.u_pcrtc.vcnt); end
    end
    always_ff @(posedge clk)
        if (rst_n && capture_armed && de && (vcnt_d<V_ACTIVE) && (hcnt_d<H_ACTIVE)) begin
            cap_r[vcnt_d][hcnt_d]<=r; cap_g[vcnt_d][hcnt_d]<=g; cap_b[vcnt_d][hcnt_d]<=b; cap_de[vcnt_d][hcnt_d]<=1'b1; end

    logic [31:0] cube_tex_ref [0:TEXW*TEXH-1];
    int errors, tex_mismatch, de_pixels, nonzero_pixels, distinct;
    logic [23:0] seen_col [0:31];

    initial begin
        errors=0; tex_mismatch=0; de_pixels=0; nonzero_pixels=0; distinct=0; feeder_go_tb=1'b0;
        $readmemh(`FEEDER_CUBE_FILE,     dut.g_feeder.feeder_stg);
        $readmemh(`FEEDER_CUBE_TEX_FILE, cube_tex_ref);

        rst_n=1'b0; core_go=1'b0;
        repeat(4) @(posedge clk); rst_n=1'b1; repeat(8) @(posedge clk);

        // run the REAL boot: bootlet DMAs the 64x64 texture upload payload into VRAM.
        @(negedge clk); core_go=1'b1; @(negedge clk); core_go=1'b0;
        wait (core_halt==1'b1); repeat(4) @(posedge clk);
        wait (dma_done_seen==1'b1); repeat(10) @(posedge clk);
        if (dut.xfer_busy==1'b1) wait (dut.xfer_busy==1'b0);
        repeat(50) @(posedge clk);

        // ---- TEX: the upload landed at TBP=64 byte-for-byte ----
        for (int i=0; i<TEXW*TEXH; i++)
            if (dut.u_vram.mem[TEX_W0 + i] !== cube_tex_ref[i]) begin
                if (tex_mismatch<8) $error("[feeder_cube] tex word %0d: vram=%08x exp=%08x", i, dut.u_vram.mem[TEX_W0+i], cube_tex_ref[i]);
                tex_mismatch++;
            end
        if (tex_mismatch!=0) begin $error("[feeder_cube] texture upload mismatched %0d/%0d words", tex_mismatch, TEXW*TEXH); errors++; end

        // let the feeder render the 27 perspective triangles, then capture a frame.
        wait (feeder_ready_tb==1'b1);
        repeat(300000) @(posedge clk);
        // probe the FB directly (words 0..4095) right before capture
        begin int fbnz; fbnz=0;
            for (int i=0;i<4096;i++) if (dut.u_vram.mem[i]!==32'd0 && dut.u_vram.mem[i]!==32'hFF000000) fbnz++;
            $display("[feeder_cube] FB-probe @%0t: nonbg FB words=%0d ; last_emit@%0t emits=%0d", $time, fbnz, dbg_last_emit, dbg_emit);
        end
        @(posedge dut.u_pcrtc.end_of_frame); @(posedge clk); capture_armed=1'b1;
        @(posedge dut.u_pcrtc.end_of_frame); @(posedge clk); capture_armed=1'b0;

        // ---- SEQ: perspective ST for all 27 tris, no UV ----
        $display("[feeder_cube] ST=%0d UV=%0d first-seq=%02x %02x %02x", n_st, n_uv, seq_first[0], seq_first[1], seq_first[2]);
        if (n_st != NTRI*3) begin $error("[feeder_cube] ST writes=%0d (want %0d = %0d tris x 3)", n_st, NTRI*3, NTRI); errors++; end
        if (n_uv != 0)       begin $error("[feeder_cube] UV writes=%0d (want 0 in perspective mode)", n_uv); errors++; end
        if (!(seq_first[0]==8'h01 && seq_first[1]==8'h02 && seq_first[2]==8'h05))
            begin $error("[feeder_cube] vertex seq not RGBAQ->ST->XYZ2 (got %02x %02x %02x)", seq_first[0],seq_first[1],seq_first[2]); errors++; end

        // ---- DRAW: non-trivial textured content rendered ----
        for (int y=0;y<V_ACTIVE;y++) for (int x=0;x<H_ACTIVE;x++) if (cap_de[y][x]) begin
            logic [23:0] key; bit found;
            key = {cap_b[y][x],cap_g[y][x],cap_r[y][x]};
            de_pixels++;
            if (key!=24'd0) nonzero_pixels++;
            found = 1'b0;
            for (int k=0;k<distinct;k++) if (seen_col[k]===key) found=1'b1;
            if (!found && distinct<32) begin seen_col[distinct]=key; distinct++; end
        end
        // The authentic cube texture is verified 2-color (black 0xFF000000 / white 0xFFFFFFFF, 50/50). On
        // screen the black texels alias the black background, so "distinct colors" caps at 2; the texture is
        // proven SAMPLED by both values appearing: white texels are visible (nonzero) AND more pixels were
        // emitted than are white (emits > white => black texels also drawn = the B/W pattern, not a flat fill).
        // ---- dump the actual rendered frame as ASCII (ground truth = what the board shows) ----
        $display("[feeder_cube] RENDERED FRAME ('#'=white texel, '.'=black/bg DE, ' '=blank):");
        for (int y=0; y<V_ACTIVE; y++) begin
            string row; row = "";
            for (int x=0; x<H_ACTIVE; x++) begin
                if (!cap_de[y][x])                                   row = {row, " "};
                else if ({cap_b[y][x],cap_g[y][x],cap_r[y][x]}!=24'd0) row = {row, "#"};
                else                                                 row = {row, "."};
            end
            $display("  %s", row);
        end
        $display("[feeder_cube] RASTER PROBE: inside_pixels=%0d ztest_pixels=%0d emits=%0d", dbg_inside, dbg_ztst_pix, dbg_emit);
        $display("[feeder_cube] de_pixels=%0d white(nonzero)=%0d distinct_colors=%0d", de_pixels, nonzero_pixels, distinct);
        if (de_pixels    < 1000)        begin $error("[feeder_cube] too few DE pixels (%0d) — scanout dead?", de_pixels); errors++; end
        if (nonzero_pixels < 80)        begin $error("[feeder_cube] too few white texels (%0d) — cube didn't draw", nonzero_pixels); errors++; end
        if (nonzero_pixels > 2500)      begin $error("[feeder_cube] white area too large (%0d) — flood, not a cube shape", nonzero_pixels); errors++; end
        if (dbg_emit <= nonzero_pixels) begin $error("[feeder_cube] emits(%0d) <= white(%0d) — no black texels: texture pattern not sampled", dbg_emit, nonzero_pixels); errors++; end
        if (raster_overflow)            begin $error("[feeder_cube] raster_overflow set"); errors++; end

        if (errors==0) begin $display("[tb_top_psmct32_feeder_cube_demo] PASS"); $finish; end
        else $fatal(1, "[tb_top_psmct32_feeder_cube_demo] FAIL (%0d errors)", errors);
    end
    initial begin #2000000000; $error("[tb_top_psmct32_feeder_cube_demo] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_feeder_cube_demo
