// retroDE_ps2 — tb_ee_core_pcpyud
//
// Ch283 — Focused TB for R5900 EE MMI3/PCPYUD (Parallel Copy Upper
// Doubleword). Primary opcode 0x1C, funct 0x29 (MMI3), sa 0x0E
// (PCPYUD). Architectural:
//   $rd[127:64] = $rs[127:64];  $rd[63:0] = $rt[127:64]
// First MMI op that *reads* from the upper 64 bits of source
// registers; the introduction of the gpr128 shadow in Ch283 exists
// specifically so this op (and future upper-half consumers) can do
// the right thing instead of always producing zero.
//
// qbert.elf hits `pcpyud $a0, $v0, $t1` (instr 0x704923A9) at PC
// 0x00112CA0 — encoding-asserted in Case 1.
//
// Cases:
//   1. Exact qbert encoding asserted == 0x704923A9. Both $v0 and $t1
//      have their upper halves at 0 (set via LUI+ORI scalar writes),
//      so PCPYUD's low-32 result is 0. This is the *normal* qbert
//      path: PCPYUD reads upper halves that, with scalar-zero
//      construction, will be zero. The runner should classify this
//      result and continue.
//   2. PCPYLD-then-PCPYUD round-trip — proves the gpr128 shadow is
//      actually carrying upper bits. PCPYLD($t2, $t0, $t1) makes
//      gpr128[$t2][127:64] = $t0[63:0] (= 0xAABBCCDD with upper zero).
//      PCPYUD($t3, $t2, $t2) extracts $t2's upper-D into both halves
//      of $t3, so $t3's low 32 (the regfile mirror) == 0xAABBCCDD.
//   3. PCPYUD with $0 as rt — exercises rs upper-half copy while
//      forcing rt-side to zero. Also peeks at gpr128 directly to
//      assert the upper 32 bits of the destination's lower-D landed.
//
// Strict mode ON.

`timescale 1ns/1ps

module tb_ee_core_pcpyud;

    localparam int BIOS_BYTES    = 4 * 1024 * 1024;
    localparam int EE_RAM_BYTES  = 4 * 1024;
    localparam int EE_RAM_ADDR_W = $clog2(EE_RAM_BYTES);

    localparam logic [31:0] RESET_VECTOR   = 32'hBFC0_0000;
    localparam logic [31:0] BOOTSTRAP_VA   = 32'hBFC0_0100;
    localparam int          RESET_SLOT     = 0;
    localparam int          BOOTSTRAP_SLOT = 32'h100 >> 2;

    // Case 1: qbert exact encoding. $v0 and $t1 both set to nonzero
    // scalar values; PCPYUD pulls from their upper halves which are 0.
    localparam int B_LUI_V0      = BOOTSTRAP_SLOT + 0;
    localparam int B_ORI_V0      = BOOTSTRAP_SLOT + 1;
    localparam int B_LUI_T1A     = BOOTSTRAP_SLOT + 2;
    localparam int B_ORI_T1A     = BOOTSTRAP_SLOT + 3;
    localparam int B_PCPYUD_A    = BOOTSTRAP_SLOT + 4;
    localparam int B_BNE_A       = BOOTSTRAP_SLOT + 5;
    localparam int B_DS_A        = BOOTSTRAP_SLOT + 6;

    // Case 2: PCPYLD-then-PCPYUD round-trip.
    localparam int B_LUI_T0B     = BOOTSTRAP_SLOT + 7;
    localparam int B_ORI_T0B     = BOOTSTRAP_SLOT + 8;
    localparam int B_LUI_T1B     = BOOTSTRAP_SLOT + 9;
    localparam int B_ORI_T1B     = BOOTSTRAP_SLOT + 10;
    localparam int B_PCPYLD_B    = BOOTSTRAP_SLOT + 11;
    localparam int B_PCPYUD_B    = BOOTSTRAP_SLOT + 12;
    localparam int B_LUI_EXPB    = BOOTSTRAP_SLOT + 13;
    localparam int B_ORI_EXPB    = BOOTSTRAP_SLOT + 14;
    localparam int B_BNE_B       = BOOTSTRAP_SLOT + 15;
    localparam int B_DS_B        = BOOTSTRAP_SLOT + 16;

    // Case 3: PCPYUD with rt=$0 — exercises the rs-upper-D path
    // alone. After Case 2, $t2 still holds the PCPYLD result.
    localparam int B_PCPYUD_C    = BOOTSTRAP_SLOT + 17;
    localparam int B_BNE_C       = BOOTSTRAP_SLOT + 18;
    localparam int B_DS_C        = BOOTSTRAP_SLOT + 19;

    localparam int B_PASS        = BOOTSTRAP_SLOT + 20;
    localparam int B_FAIL        = BOOTSTRAP_SLOT + 21;

    localparam logic [5:0] OP_J      = 6'h02;
    localparam logic [5:0] OP_BNE    = 6'h05;
    localparam logic [5:0] OP_ORI    = 6'h0D;
    localparam logic [5:0] OP_LUI    = 6'h0F;
    localparam logic [5:0] OP_MMI    = 6'h1C;
    localparam logic [5:0] FUNC_MMI2 = 6'h09;
    localparam logic [5:0] FUNC_MMI3 = 6'h29;
    localparam logic [4:0] SA_PCPYLD = 5'h0E;
    localparam logic [4:0] SA_PCPYUD = 5'h0E;

    localparam int RV0  = 2;
    localparam int RT1  = 9;
    localparam int RA0  = 4;
    localparam int RT0  = 8;
    localparam int RT2  = 10;
    localparam int RT3  = 11;
    localparam int RT5  = 13;
    localparam int RTMP = 24;

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

    logic        core_go, core_halt, core_trap;
    logic [31:0] core_pc, core_trap_pc, core_trap_instr;
    logic        ee_rd_en, ee_rd_valid, ee_wr_en;
    logic [31:0] ee_rd_addr, ee_rd_data, ee_wr_addr, ee_wr_data;
    logic [3:0]  ee_wr_be;

    logic                core_ev_valid;
    trace_pkg::subsys_e  core_ev_subsys;
    trace_pkg::event_e   core_ev_event;
    logic [63:0]         core_ev_arg0, core_ev_arg1, core_ev_arg2, core_ev_arg3;
    logic [31:0]         core_ev_flags;

    ee_core_stub #(.STRICT_UNSUPPORTED(1'b1)) u_core (
        .clk(clk), .rst_n(rst_n), .go_i(core_go),
        .map_rd_en(ee_rd_en), .map_rd_addr(ee_rd_addr),
        .map_rd_data(ee_rd_data), .map_rd_valid(ee_rd_valid),
        .map_wr_en(ee_wr_en), .map_wr_addr(ee_wr_addr),
        .map_wr_data(ee_wr_data), .map_wr_be(ee_wr_be),
        .cpu_irq(1'b0),
        .halt_o(core_halt), .pc_o(core_pc),
        .trap_o(core_trap), .trap_pc_o(core_trap_pc),
        .trap_instr_o(core_trap_instr),
        .ev_valid(core_ev_valid), .ev_subsys(core_ev_subsys),
        .ev_event(core_ev_event),
        .ev_arg0(core_ev_arg0), .ev_arg1(core_ev_arg1),
        .ev_arg2(core_ev_arg2), .ev_arg3(core_ev_arg3),
        .ev_flags(core_ev_flags)
    );

    logic         bios_rd_en, bios_rd_valid;
    logic [21:0]  bios_rd_addr;
    logic [31:0]  bios_rd_data;
    logic         ee_ram_rd_en, ee_ram_rd_valid, ee_ram_wr_en;
    logic [24:0]  ee_ram_rd_addr, ee_ram_wr_addr;
    logic [127:0] ee_ram_rd_data, ee_ram_wr_data;
    logic [15:0]  ee_ram_wr_be;
    logic [7:0]   ee_ram_master_id;

    ee_memory_map_stub u_ee_map (
        .clk(clk), .rst_n(rst_n),
        .ee_rd_en(ee_rd_en), .ee_rd_addr(ee_rd_addr),
        .ee_rd_data(ee_rd_data), .ee_rd_valid(ee_rd_valid),
        .ee_wr_en(ee_wr_en), .ee_wr_addr(ee_wr_addr),
        .ee_wr_data(ee_wr_data), .ee_wr_be(ee_wr_be),
        .dmac_rd_en(1'b0), .dmac_rd_addr(32'd0),
        .dmac_rd_data(), .dmac_rd_valid(),
        .bridge_wr_en(1'b0), .bridge_wr_addr(32'd0),
        .bridge_wr_data(128'd0), .bridge_wr_be(16'd0),
        .bridge_master_id(8'd0),
        .bios_rd_en(bios_rd_en), .bios_rd_addr(bios_rd_addr),
        .bios_rd_data(bios_rd_data), .bios_rd_valid(bios_rd_valid),
        .ram_rd_en(ee_ram_rd_en), .ram_rd_addr(ee_ram_rd_addr),
        .ram_rd_data(ee_ram_rd_data), .ram_rd_valid(ee_ram_rd_valid),
        .ram_wr_en(ee_ram_wr_en), .ram_wr_addr(ee_ram_wr_addr),
        .ram_wr_data(ee_ram_wr_data), .ram_wr_be(ee_ram_wr_be),
        .ram_master_id(ee_ram_master_id),
        .ee_dmac_ch2_wr_en(), .ee_dmac_ch2_wr_addr(), .ee_dmac_ch2_wr_data(),
        .ee_dmac_ch2_rd_en(), .ee_dmac_ch2_rd_addr(),
        .ee_dmac_ch2_rd_data(32'd0), .ee_dmac_ch2_rd_valid(1'b0),
        .ee_intc_wr_en(), .ee_intc_wr_addr(), .ee_intc_wr_data(),
        .ee_intc_rd_en(), .ee_intc_rd_addr(),
        .ee_intc_rd_data(32'd0), .ee_intc_rd_valid(1'b0),
        .ee_misc_mmio_wr_en(), .ee_misc_mmio_wr_addr(), .ee_misc_mmio_wr_data(), .ee_misc_mmio_wr_be(),
        .ee_misc_mmio_rd_en(), .ee_misc_mmio_rd_addr(),
        .ee_misc_mmio_rd_data(32'd0), .ee_misc_mmio_rd_valid(1'b0),
        .ee_biu_wr_en(), .ee_biu_wr_addr(), .ee_biu_wr_data(), .ee_biu_wr_be(),
        .ee_biu_rd_en(), .ee_biu_rd_addr(),
        .ee_biu_rd_data(32'd0), .ee_biu_rd_valid(1'b0),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    ee_ram_stub #(.SIZE_BYTES(EE_RAM_BYTES)) u_ee_ram (
        .clk(clk), .rst_n(rst_n),
        .rd_en(ee_ram_rd_en), .rd_addr(ee_ram_rd_addr[EE_RAM_ADDR_W-1:0]),
        .rd_data(ee_ram_rd_data), .rd_valid(ee_ram_rd_valid),
        .wr_en(ee_ram_wr_en), .wr_addr(ee_ram_wr_addr[EE_RAM_ADDR_W-1:0]),
        .wr_data(ee_ram_wr_data), .wr_be(ee_ram_wr_be),
        .master_id(ee_ram_master_id),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    bios_rom_stub #(.SIZE_BYTES(BIOS_BYTES)) u_bios (
        .clk(clk), .rst_n(rst_n),
        .rd_en(bios_rd_en), .rd_addr(bios_rd_addr),
        .rd_data(bios_rd_data), .rd_valid(bios_rd_valid),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    trace_sink_stub #(.FILENAME("ee_core_pcpyud_core.trace"), .SINK_LABEL("ee_core"))
        u_trace_core (.clk(clk), .rst_n(rst_n),
            .ev_valid(core_ev_valid), .ev_subsys(core_ev_subsys),
            .ev_event(core_ev_event), .ev_arg0(core_ev_arg0),
            .ev_arg1(core_ev_arg1), .ev_arg2(core_ev_arg2),
            .ev_arg3(core_ev_arg3), .ev_flags(core_ev_flags));

    int core_retired_events, core_halt_events, core_trap_events, errors;
    initial begin
        core_retired_events = 0; core_halt_events = 0;
        core_trap_events = 0; errors = 0;
    end
    always_ff @(posedge clk) begin
        if (rst_n && core_ev_valid) begin
            core_retired_events <= core_retired_events + 1;
            if ((core_ev_flags & 32'h08) != 32'd0) core_halt_events <= core_halt_events + 1;
            if ((core_ev_flags & 32'h80) != 32'd0) core_trap_events <= core_trap_events + 1;
        end
    end

    task automatic bios_word(input int slot, input logic [31:0] word);
        u_bios.mem[slot] = word;
    endtask
    function automatic logic [31:0] enc_i(input logic [5:0] op,
                                          input int rs, input int rt,
                                          input logic [15:0] imm);
        return {op, rs[4:0], rt[4:0], imm};
    endfunction
    function automatic logic [31:0] enc_j(input logic [31:0] target_addr);
        return {OP_J, target_addr[27:2]};
    endfunction
    function automatic logic [31:0] enc_rtype(input logic [5:0] op,
                                              input int rs, input int rt,
                                              input int rd, input int sa,
                                              input logic [5:0] funct);
        return {op, rs[4:0], rt[4:0], rd[4:0], sa[4:0], funct};
    endfunction
    function automatic logic [15:0] branch_imm(input int branch_slot,
                                               input int target_slot);
        int diff;
        diff = target_slot - branch_slot - 1;
        return diff[15:0];
    endfunction

    initial begin
        logic [31:0] qbert_pcpyud_raw;

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

        bios_word(RESET_SLOT,     enc_j(BOOTSTRAP_VA));
        bios_word(RESET_SLOT + 1, 32'd0);

        // ===== Case 1: qbert encoding. $v0=0xCAFE_BABE, $t1=0xDEAD_BEEF
        //               (both scalar, upper halves zero). PCPYUD picks
        //               upper-D of each (= 0), so $a0_low = 0.
        bios_word(B_LUI_V0,   enc_i(OP_LUI, 0, RV0, 16'hCAFE));
        bios_word(B_ORI_V0,   enc_i(OP_ORI, RV0, RV0, 16'hBABE));
        bios_word(B_LUI_T1A,  enc_i(OP_LUI, 0, RT1, 16'hDEAD));
        bios_word(B_ORI_T1A,  enc_i(OP_ORI, RT1, RT1, 16'hBEEF));
        qbert_pcpyud_raw = enc_rtype(OP_MMI, RV0, RT1, RA0, SA_PCPYUD, FUNC_MMI3);
        if (qbert_pcpyud_raw !== 32'h7049_23A9) begin
            $error("[tb_ee_core_pcpyud] enc_rtype mismatch: produced 0x%08h, expected 0x704923A9",
                   qbert_pcpyud_raw);
            errors = errors + 1;
        end
        bios_word(B_PCPYUD_A, qbert_pcpyud_raw);
        // expect $a0 == 0
        bios_word(B_BNE_A,    enc_i(OP_BNE, RA0, 0, branch_imm(B_BNE_A, B_FAIL)));
        bios_word(B_DS_A,     32'd0);

        // ===== Case 2: PCPYLD-then-PCPYUD round-trip.
        //   $t0 = 0xAABB_CCDD; $t1 = 0x1122_3344
        //   pcpyld $t2, $t0, $t1
        //     gpr128[$t2][127:64] = {32'd0, 0xAABBCCDD}
        //     gpr128[$t2][63:0]   = {32'd0, 0x11223344}
        //   pcpyud $t3, $t2, $t2
        //     gpr128[$t3][127:64] = {32'd0, 0xAABBCCDD}
        //     gpr128[$t3][63:0]   = {32'd0, 0xAABBCCDD}
        //   regfile[$t3] = 0xAABB_CCDD
        bios_word(B_LUI_T0B,  enc_i(OP_LUI, 0, RT0, 16'hAABB));
        bios_word(B_ORI_T0B,  enc_i(OP_ORI, RT0, RT0, 16'hCCDD));
        bios_word(B_LUI_T1B,  enc_i(OP_LUI, 0, RT1, 16'h1122));
        bios_word(B_ORI_T1B,  enc_i(OP_ORI, RT1, RT1, 16'h3344));
        bios_word(B_PCPYLD_B, enc_rtype(OP_MMI, RT0, RT1, RT2, SA_PCPYLD, FUNC_MMI2));
        bios_word(B_PCPYUD_B, enc_rtype(OP_MMI, RT2, RT2, RT3, SA_PCPYUD, FUNC_MMI3));
        bios_word(B_LUI_EXPB, enc_i(OP_LUI, 0, RTMP, 16'hAABB));
        bios_word(B_ORI_EXPB, enc_i(OP_ORI, RTMP, RTMP, 16'hCCDD));
        bios_word(B_BNE_B,    enc_i(OP_BNE, RT3, RTMP, branch_imm(B_BNE_B, B_FAIL)));
        bios_word(B_DS_B,     32'd0);

        // ===== Case 3: pcpyud $t5, $t2, $0
        //   gpr128[$t5][127:64] = gpr128[$t2][127:64] = {32'd0, 0xAABBCCDD}
        //   gpr128[$t5][63:0]   = gpr128[$0][127:64]  = 0
        //   regfile[$t5] = 0 (low 32 of 0)
        bios_word(B_PCPYUD_C, enc_rtype(OP_MMI, RT2, 0, RT5, SA_PCPYUD, FUNC_MMI3));
        bios_word(B_BNE_C,    enc_i(OP_BNE, RT5, 0, branch_imm(B_BNE_C, B_FAIL)));
        bios_word(B_DS_C,     32'd0);

        bios_word(B_PASS, 32'h0000_000C);
        bios_word(B_FAIL, 32'h0000_000C);

        @(negedge clk); core_go = 1'b1;
        @(negedge clk); core_go = 1'b0;

        while (!core_halt) @(posedge clk);
        repeat (4) @(posedge clk);

        if (core_trap_events != 0) begin
            $error("[tb_ee_core_pcpyud] unexpected trap: %0d pc=0x%08h instr=0x%08h",
                   core_trap_events, core_trap_pc, core_trap_instr);
            errors = errors + 1;
        end
        if (core_halt_events != 1) begin
            $error("[tb_ee_core_pcpyud] expected 1 halt, got %0d", core_halt_events);
            errors = errors + 1;
        end
        if (core_pc !== BOOTSTRAP_VA + ((B_PASS - BOOTSTRAP_SLOT) << 2)) begin
            $error("[tb_ee_core_pcpyud] final pc mismatch: got 0x%08h expected PASS 0x%08h",
                   core_pc, BOOTSTRAP_VA + ((B_PASS - BOOTSTRAP_SLOT) << 2));
            errors = errors + 1;
        end

        // Case 1: $a0 low 32 = 0
        if (u_core.regfile[RA0] !== 32'd0) begin
            $error("[tb_ee_core_pcpyud] CASE 1 (qbert, scalar-zero-upper): $a0 = 0x%08h (expected 0)",
                   u_core.regfile[RA0]);
            errors = errors + 1;
        end
        // Case 2: $t3 low 32 = 0xAABBCCDD
        if (u_core.regfile[RT3] !== 32'hAABB_CCDD) begin
            $error("[tb_ee_core_pcpyud] CASE 2 (round-trip): $t3 = 0x%08h (expected 0xAABBCCDD)",
                   u_core.regfile[RT3]);
            errors = errors + 1;
        end
        // Case 2: peek into gpr128 to confirm upper-D landed
        if (u_core.gpr128[RT3][127:64] !== 64'h0000_0000_AABB_CCDD) begin
            $error("[tb_ee_core_pcpyud] CASE 2 (round-trip): gpr128[$t3][127:64] = 0x%016h (expected 0x00000000AABBCCDD)",
                   u_core.gpr128[RT3][127:64]);
            errors = errors + 1;
        end
        // Case 3: $t5 low 32 = 0, and gpr128 upper-D matches $t2's upper-D
        if (u_core.regfile[RT5] !== 32'd0) begin
            $error("[tb_ee_core_pcpyud] CASE 3 (rt=$0): $t5 low = 0x%08h (expected 0)",
                   u_core.regfile[RT5]);
            errors = errors + 1;
        end
        if (u_core.gpr128[RT5][127:64] !== 64'h0000_0000_AABB_CCDD) begin
            $error("[tb_ee_core_pcpyud] CASE 3 (rt=$0): gpr128[$t5][127:64] = 0x%016h (expected 0x00000000AABBCCDD)",
                   u_core.gpr128[RT5][127:64]);
            errors = errors + 1;
        end

        $display("[tb_ee_core_pcpyud] retired=%0d halt=%0d trap=%0d pc=0x%08h errors=%0d",
                 core_retired_events, core_halt_events,
                 core_trap_events, core_pc, errors);
        $display("[tb_ee_core_pcpyud]   $a0=0x%08h $t3=0x%08h $t5=0x%08h",
                 u_core.regfile[RA0], u_core.regfile[RT3], u_core.regfile[RT5]);
        $display("[tb_ee_core_pcpyud]   gpr128[$t3]=0x%032h",
                 u_core.gpr128[RT3]);

        if (errors == 0)
            $display("[tb_ee_core_pcpyud] PASS");
        else
            $display("[tb_ee_core_pcpyud] FAIL");
        $finish;
    end

    initial begin
        #2000000;
        $error("[tb_ee_core_pcpyud] timeout");
        $finish;
    end

endmodule : tb_ee_core_pcpyud
