// ============================================================================
// tb_gs_lpddr_scanout_lb_hstretch — Ch418 captured SH3 display mapping
//
// Proves the divider-free 5-output/4-source presentation map and Ch437 linear
// reconstruction used to reduce
// the dump's 512-source-pixel, MAGH=4 display onto a 640-pixel active line.
// A compact 64-source -> 80-output instance locks the exact x0/xf coordinates
// across all eight beat slots, including the source-right clamp.
// Thirty output lines also lock the captured interlace reduction, source_y =
// 2 + floor(output_y*14/15), including its repeated first line.
// ============================================================================
`timescale 1ns/1ps

module tb_gs_lpddr_scanout_lb_hstretch;
    localparam int SRC_W=64, OUT_W=80, OUT_H=30;
    // Physical stride is 80 pixels, matching the output raster; the captured
    // display source occupies the first 64 pixels of each row.
    localparam int STRIDE=320, ROW_BEATS=10, N_ROWS=32, VSTART=2;

    logic axi_clk=0, video_clk=0, rst_n=0, enable=0;
    always #2 axi_clk=~axi_clk;
    always #10 video_clk=~video_clk;

    logic frame_start=0, in_window=0;
    logic [11:0] pixel_x=0, pixel_y=0;
    wire [7:0] r,g,b;
    wire line_valid, underflow;
    wire [31:0] rd_errs;
    wire [29:0] araddr;
    wire [1:0] arburst;
    wire [6:0] arid;
    wire [7:0] arlen;
    wire [2:0] arsize;
    wire arvalid, rready;
    logic arready=0, rvalid=0, rlast=0;
    logic [255:0] rdata=0;
    logic [1:0] rresp=0;

    logic [255:0] mem [0:N_ROWS*ROW_BEATS-1];
    initial begin
        for (int beat=0; beat<N_ROWS*ROW_BEATS; beat++) mem[beat]='0;
        for (int y=0; y<N_ROWS; y++) begin
            for (int x=0; x<SRC_W; x++)
                mem[y*ROW_BEATS + (x>>3)][(x&7)*32 +: 32] =
                    {8'hff, 8'(8'h80+x+y), 8'(y), 8'(x)};
        end
    end

    gs_lpddr_scanout_lb #(
        .FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
        .N_ROWS(N_ROWS), .PSMCT32(1'b1), .H_STRETCH_5_TO_4(1'b1),
        .V_SOURCE_START(VSTART), .V_STRETCH_15_TO_14(1'b1),
        .V_LINEAR_FILTER(1'b1), .H_LINEAR_FILTER(1'b1),
        .H_SOURCE_PIXELS(SRC_W)
    ) dut (
        .axi_clk(axi_clk), .axi_rst_n(rst_n), .enable(enable),
        .video_clk(video_clk), .frame_start(frame_start),
        .pixel_x(pixel_x), .pixel_y(pixel_y), .in_window(in_window),
        .r(r), .g(g), .b(b), .line_valid(line_valid), .underflow(underflow),
        .rd_errs(rd_errs), .araddr(araddr), .arburst(arburst), .arid(arid),
        .arlen(arlen), .arsize(arsize), .arvalid(arvalid), .arready(arready),
        .rdata(rdata), .rresp(rresp), .rlast(rlast), .rvalid(rvalid), .rready(rready)
    );

    typedef enum logic [1:0] {S_AR, S_R} state_t;
    state_t state=S_AR;
    localparam int MEM_BEAT_BITS=$clog2(N_ROWS*ROW_BEATS);
    logic [MEM_BEAT_BITS-1:0] beat_q;
    always_ff @(posedge axi_clk) begin
        arready <= 1'b0;
        if (!rst_n) begin
            state<=S_AR; rvalid<=1'b0; rlast<=1'b0;
        end else case (state)
            S_AR: if (arvalid) begin
                beat_q<=araddr[MEM_BEAT_BITS+4:5]; arready<=1'b1; state<=S_R;
            end
            S_R: begin
                rdata<=mem[beat_q]; rresp<=2'b00; rlast<=1'b1; rvalid<=1'b1;
                if (rready && rvalid) begin rvalid<=1'b0; rlast<=1'b0; state<=S_AR; end
            end
        endcase
    end

    function automatic int source_x(input int out_x);
        source_x = (out_x * 4) / 5; // testbench oracle only; not synthesized
    endfunction
    function automatic int source_x_frac(input int out_x);
        source_x_frac = (out_x * 4) % 5;
    endfunction
    function automatic int source_y(input int out_y);
        source_y = VSTART + (out_y * 14) / 15;
    endfunction
    function automatic int source_y_frac(input int out_y);
        source_y_frac = (out_y * 14) % 15;
    endfunction
    function automatic int blend15(input int cur, input int nxt, input int frac);
        blend15 = ((15-frac)*cur + frac*nxt + 7) / 15;
    endfunction
    function automatic int blend5(input int left, input int right, input int frac);
        blend5 = ((5-frac)*left + frac*right + 2) / 5;
    endfunction

    int errors=0, checked=0;
    logic check_en=0;
    logic [11:0] x_d, y_d;
    logic in_d;
    always_ff @(posedge video_clk) begin
        x_d<=pixel_x; y_d<=pixel_y; in_d<=in_window;
        if (check_en && in_d) begin
            int sx,sx1,xf,sy,sy1,yf,er,eg,eb;
            sx=source_x(x_d);
            sx1=(sx+1<SRC_W) ? sx+1 : sx;
            xf=source_x_frac(x_d);
            sy=source_y(y_d); yf=source_y_frac(y_d);
            sy1=(sy+1<N_ROWS) ? sy+1 : sy;
            er=blend5(sx,sx1,xf);
            eg=blend15(sy,sy1,yf);
            eb=blend5(blend15(8'h80+sx +sy,8'h80+sx +sy1,yf),
                      blend15(8'h80+sx1+sy,8'h80+sx1+sy1,yf),xf);
            checked++;
            if (r!==8'(er) || g!==8'(eg) || b!==8'(eb)) begin
                $display("[hstretch] out=(%0d,%0d) source=(%0d+%0d/5,%0d+%0d/15) got=%02x/%02x/%02x exp=%02x/%02x/%02x",
                         x_d,y_d,sx,xf,sy,yf,r,g,b,8'(er),8'(eg),8'(eb));
                errors++;
            end
        end
    end

    initial begin
        repeat(8) @(posedge axi_clk); rst_n=1; enable=1;
        frame_start=1; repeat(3) @(posedge video_clk); frame_start=0;
        // Load both rows before scanout so this TB isolates coordinate mapping.
        repeat(300) @(posedge axi_clk);
        repeat(3) @(posedge video_clk);
        check_en=1;
        for (int y=0; y<OUT_H; y++) begin
            for (int x=0; x<OUT_W; x++) begin
                // Drive away from the sampling edge.  The original Ch418 TB
                // raced its next x assignment against the DUT at posedge;
                // nearest filtering hid the duplicate x=1 sample, while a
                // fractional filter correctly makes x=0 and x=1 differ.
                @(negedge video_clk);
                pixel_x=12'(x); pixel_y=12'(y); in_window=1;
                @(posedge video_clk);
            end
            // Model a real horizontal blank interval.  Linear filtering uses
            // it to prefetch the next source row into the retired parity
            // buffer before active video resumes.
            @(negedge video_clk); in_window=0;
            repeat(25) @(posedge video_clk);
        end
        check_en=0;
        $display("[hstretch] checked=%0d errors=%0d underflow=%0b rd_errs=%0d", checked,errors,underflow,rd_errs);
        if (checked==OUT_W*OUT_H && errors==0 && !underflow && rd_errs==0)
            $display("[tb_gs_lpddr_scanout_lb_hstretch] PASS");
        else
            $display("[tb_gs_lpddr_scanout_lb_hstretch] FAIL");
        $finish;
    end
    initial begin #2_000_000; $display("[tb_gs_lpddr_scanout_lb_hstretch] TIMEOUT"); $finish; end
endmodule
