// retroDE_ps2 — gs_lpddr_zc_emit  (Ch357 — Z-then-color emit integration: request FIFO + persistent-Z RMW + color writer)
//
// Codex Ch357 integration constraints:
//   (1) Cross gs_clk->axi_clk with ONE async request FIFO carrying the COMPLETE fragment packet {XY, clamped Z, color,
//       masks, scene marker}. No raw level-bus CDC.
//   (2) Z decision FIRST; enqueue color ONLY on pass. A rejected fragment produces NO color write.
//   (3) Ordered end-of-scene marker through the SAME FIFO: on it, flush the dirty Z line, wait Z BRESP + all accepted color
//       BRESPs, then assert combined frame_drained.
//   (4) Z cache persists across scheduler epochs (texture rebind must not clear it) — handled by gs_lpddr_z_rmw (scene_flush
//       keeps the line valid+clean).
//   (5) Preclear/invalidate Z once at frame start (clear_start).
//   (6) LPDDR map (gs_lpddr_map_pkg): color 0x000000, Z 0x140000, tex 0x200000 — disjoint.
//
// Structure: the request async FIFO is the ONLY CDC. Everything downstream runs in axi_clk: the Z RMW (its own Z AXI
// read+write master), and the reused gs_lpddr_axi_master COLOR writer (its gs_clk tied to axi_clk, so its internal CDC FIFO
// is same-clock). A tiny in-order COLOR-ALIGN FIFO carries {coladdr,color} beside the Z pipeline so the pass decision lands
// with the right pixel. Two AXI masters are exposed (z_* and c_*); the system arbitrates them onto the shared LPDDR port.

module gs_lpddr_zc_emit #(
    parameter [31:0] COLBASE  = 32'h0000_0000,
    parameter [31:0] ZBASE    = 32'h0014_0000,
    parameter int    FB_PXW   = 256,
    parameter int    FB_H     = 210,
    parameter [15:0] Z_CLEAR  = 16'h0000,
    parameter int    REQ_DEPTH= 32,
    parameter int    COL_DEPTH= 64
) (
    // ---- gs_clk fragment/marker producer ----
    input  logic         gs_clk,
    input  logic         gs_rst_n,
    input  logic         enable,
    input  logic         g_valid,
    output logic         g_ready,
    input  logic [11:0]  g_x,
    input  logic [11:0]  g_y,
    input  logic [15:0]  g_zq,        // clamped Z (clamp16 done upstream)
    input  logic         g_zmsk,
    input  logic         g_ztest,     // 1 = depth-test this fragment; 0 = always pass (non-Z draw)
    input  logic [1:0]   g_ztst,      // GS TEST.ZTST: NEVER/ALWAYS/GEQUAL/GREATER
    input  logic [31:0]  g_color,
    input  logic [16:0]  g_alpha,     // {ABE,A,B,C,D,FIX}, snapped at raster emit
    input  logic [3:0]   g_be,        // FRAME.FBMSK-compressed byte enables
    input  logic         g_scene,     // 1 = end-of-scene marker (x/y/z/color ignored)

    // ---- axi_clk ----
    input  logic         axi_clk,
    input  logic         axi_rst_n,
    input  logic         clear_start, // preclear Z once at frame start
    output logic         clear_done,
    output logic         frame_drained, // combined: color drained AND Z drained for the scene

    // ---- Z AXI master (read+write) ----
    output logic [31:0]  z_araddr, output logic [7:0] z_arlen, output logic [2:0] z_arsize, output logic [1:0] z_arburst,
    output logic         z_arvalid, input logic z_arready,
    input  logic [255:0] z_rdata, input logic [1:0] z_rresp, input logic z_rlast, input logic z_rvalid, output logic z_rready,
    output logic [31:0]  z_awaddr, output logic [7:0] z_awlen, output logic [2:0] z_awsize, output logic [1:0] z_awburst,
    output logic         z_awvalid, input logic z_awready,
    output logic [255:0] z_wdata, output logic [31:0] z_wstrb, output logic z_wlast, output logic z_wvalid, input logic z_wready,
    input  logic         z_bvalid, output logic z_bready, input logic [1:0] z_bresp,

    // ---- Color AXI master (write only) ----
    output logic [31:0]  c_awaddr, output logic [7:0] c_awlen, output logic [2:0] c_awsize, output logic [1:0] c_awburst,
    output logic         c_awvalid, input logic c_awready,
    output logic [255:0] c_wdata, output logic [31:0] c_wstrb, output logic c_wlast, output logic c_wvalid, input logic c_wready,
    input  logic         c_bvalid, output logic c_bready, input logic [1:0] c_bresp,

    // ---- status ----
    output logic [31:0]  z_beats_read, z_beats_written, c_beats_written, col_ovf, bresp_err,
    // Destination-color read AXI master. Inert for full-byte opaque writes;
    // ABE and partial FRAME.FBMSK writes both use it for destination RMW.
    output logic [31:0]  d_araddr, output logic [7:0] d_arlen, output logic [2:0] d_arsize, output logic [1:0] d_arburst,
    output logic         d_arvalid, input logic d_arready,
    input logic [255:0]  d_rdata, input logic [1:0] d_rresp, input logic d_rlast, input logic d_rvalid, output logic d_rready,
    output logic         idle
);
    // ---------------- request async FIFO (the ONLY CDC) ----------------
    // X/Y have already been clipped to this framebuffer before reaching the
    // external ROP.  Do not spend twelve FIFO bits on each coordinate: the
    // production 640x480 path needs only 10+9.  Besides saving storage, this
    // keeps the deep request FIFO below the RAM-banking threshold that put a
    // bank mux on its 310 MHz registered-read path after ZTST was added.
    localparam int XW = (FB_PXW <= 1) ? 1 : $clog2(FB_PXW);
    localparam int YW = (FB_H   <= 1) ? 1 : $clog2(FB_H);
    localparam int PW = 74 + XW + YW; // {scene,be,alpha,color,ztest,ztst,zmsk,zq,y[YW],x[XW]}
    localparam int Y_LSB     = XW;
    localparam int ZQ_LSB    = XW + YW;
    localparam int ZMSK_BIT  = ZQ_LSB + 16;
    localparam int ZTST_LSB  = ZMSK_BIT + 1;
    localparam int ZTEST_BIT = ZTST_LSB + 2;
    localparam int COLOR_LSB = ZTEST_BIT + 1;
    localparam int ALPHA_LSB = COLOR_LSB + 32;
    localparam int BE_LSB    = ALPHA_LSB + 17;
    logic [PW-1:0] req_wdata, req_rdata; logic req_wfull, req_rempty, req_rd;
    assign req_wdata = {g_scene, g_be, g_alpha, g_color, g_ztest, g_ztst, g_zmsk,
                        g_zq, g_y[YW-1:0], g_x[XW-1:0]};
    assign g_ready   = enable && !req_wfull;
    // Ch439e — the measured f52 occupancy peak is 6,115, so 8K cannot be cut
    // without dropping fragments from the intentionally unthrottled producer.
    // Split that fixed capacity by both depth and width instead: each physical
    // address copy drives one quarter of the original deep/wide RAM tree.
    gs_async_fifo #(.WIDTH(PW), .DEPTH(REQ_DEPTH), .REGISTERED_READ(1'b1),
                    .QUADRANT_READ(1'b1)) u_req (
        .wclk(gs_clk), .wrst_n(gs_rst_n), .wr(g_valid && g_ready), .wdata(req_wdata), .wfull(req_wfull),
        .rclk(axi_clk), .rrst_n(axi_rst_n), .rd(req_rd), .rdata(req_rdata), .rempty(req_rempty)
    );
    // REGISTERED request-HEAD stage.  Ch406 makes the deep request FIFO read
    // synchronous: an accepted req_rd is followed by req_read_pending, which
    // captures the complete registered RAM output here one cycle later.
    // Ch421 keeps stage occupancy independent of packet contents: decoding the
    // RAM's scene bit directly into separate fragment/marker valid registers
    // was the complete post-Ch420 setup family.  A single validity register is
    // driven only by req_read_pending; scene/type decode happens after the full
    // packet register boundary.
    logic          req_stage_valid;
    logic          req_read_pending;
    logic [PW-1:0] req_stage_packet;
    // Ch423 — packet DATA is intentionally unreset and lives outside the
    // async-reset control process.  Keeping the lone unreset data register in
    // that process made Quartus fold axi_rst_n (the 4k-fanout EMIF-cal signal)
    // into its clock enable.  Only req_stage_valid makes this payload
    // observable, so the local registered pending pulse is the complete and
    // sufficient capture enable.
    always_ff @(posedge axi_clk)
        if (req_read_pending) req_stage_packet <= req_rdata;
    wire [PW-2:0] req_stage_data = req_stage_packet[PW-2:0];
    wire        req_stage_frag_valid   = req_stage_valid && !req_stage_packet[PW-1];
    wire        req_stage_marker_valid = req_stage_valid &&  req_stage_packet[PW-1];
    wire        rq_scene = req_stage_marker_valid;
    wire [16:0] rq_alpha = req_stage_data[ALPHA_LSB +: 17];
    wire [3:0]  rq_be    = req_stage_data[BE_LSB +: 4];
    wire [31:0] rq_color = req_stage_data[COLOR_LSB +: 32];
    wire        rq_ztest = req_stage_data[ZTEST_BIT];
    wire [1:0]  rq_ztst  = req_stage_data[ZTST_LSB +: 2];
    wire        rq_zmsk  = req_stage_data[ZMSK_BIT];
    wire [15:0] rq_zq    = req_stage_data[ZQ_LSB +: 16];
    wire [11:0] rq_y     = {{(12-YW){1'b0}}, req_stage_data[Y_LSB +: YW]};
    wire [11:0] rq_x     = {{(12-XW){1'b0}}, req_stage_data[0 +: XW]};

`ifndef SYNTHESIS
    initial begin
        if (XW > 12 || YW > 12)
            $fatal(1, "gs_lpddr_zc_emit framebuffer dimensions exceed 12-bit fragment coordinates");
    end
`endif

    // ---------------- Z RMW (axi_clk) ----------------
    logic        z_fvalid, z_fready, z_pvalid, z_pready, z_ppass, z_sflush, z_drained;
    logic [11:0] z_px, z_py; logic [15:0] z_pzq;
    gs_lpddr_z_rmw #(.ZBASE(ZBASE), .FB_PXW(FB_PXW), .FB_H(FB_H), .Z_CLEAR(Z_CLEAR)) u_z (
        .clk(axi_clk), .rst_n(axi_rst_n), .enable(enable), .clear_start(clear_start), .clear_done(clear_done),
        .scene_flush(z_sflush), .z_drained(z_drained),
        .f_valid(z_fvalid), .f_ready(z_fready), .f_x(rq_x), .f_y(rq_y), .f_z({16'd0, rq_zq}),
        .f_zmsk(rq_zmsk || !rq_ztest), .f_ztst(rq_ztest ? rq_ztst : 2'd1),
        .p_valid(z_pvalid), .p_ready(z_pready), .p_pass(z_ppass), .p_x(z_px), .p_y(z_py), .p_zq(z_pzq),
        .araddr(z_araddr), .arlen(z_arlen), .arsize(z_arsize), .arburst(z_arburst), .arvalid(z_arvalid), .arready(z_arready),
        .rdata(z_rdata), .rresp(z_rresp), .rlast(z_rlast), .rvalid(z_rvalid), .rready(z_rready),
        .awaddr(z_awaddr), .awlen(z_awlen), .awsize(z_awsize), .awburst(z_awburst), .awvalid(z_awvalid), .awready(z_awready),
        .wdata(z_wdata), .wstrb(z_wstrb), .wlast(z_wlast), .wvalid(z_wvalid), .wready(z_wready),
        .bvalid(z_bvalid), .bready(z_bready), .bresp(z_bresp),
        .beats_read(z_beats_read), .beats_written(z_beats_written), .bresp_err(bresp_err), .idle(z_idle)
    );
    // A fragment carries ztest: when ztest=0 it must ALWAYS pass. Feed the RMW a zmsk so it never writes Z for a
    // non-Z fragment, and force its zq to max so GEQUAL always passes. (All scheduler draws are ztest=1.)
    // (Handled at feed below via the always-pass override on the pass decision.)

    // ---------------- color-align FIFO (in-order {coladdr,color,ztest}) ----------------
    localparam int CAW = 86;   // {ztest, be[3:0], alpha[16:0], coladdr[31:0], color[31:0]}
    localparam int CAPTR_W = $clog2(COL_DEPTH);
    logic [CAW-1:0] ca_mem [0:COL_DEPTH-1];
    logic [CAPTR_W:0] ca_head, ca_tail;
    logic             ca_wr_q;
    logic [CAPTR_W-1:0] ca_waddr_q;
    logic [CAW-1:0]   ca_wdata_q;
    wire ca_empty = (ca_head==ca_tail);
    // Power-of-two ring full test: same low address with opposite wrap bit.
    // This is equivalent to (tail-head)==COL_DEPTH while occupancy is bounded,
    // but removes the pointer subtract/carry chain from feed_ok -> ca_mem write
    // enable (the post-scanout fit's final -0.007 ns, two-endpoint family).
    wire ca_full  = (ca_tail[CAPTR_W] != ca_head[CAPTR_W])
                  && (ca_tail[CAPTR_W-1:0] == ca_head[CAPTR_W-1:0]);
    wire [31:0] frag_coladdr = COLBASE + (((rq_y*FB_PXW) + rq_x) << 2);
    // Register the color-align RAM write port.  Ch423 captures address/data
    // every cycle instead of enabling these wide DATA registers with feed_ok.
    // Their contents are observable only when the separately-reset ca_wr_q is
    // high, so this removes ca_tail->ca_full->feed_ok from 82 data-register
    // enables without changing the committed address/data/write-strobe tuple.
    // Nonblocking ordering makes the RAM consume the prior cycle's captured
    // tuple exactly when the prior feed_ok raised ca_wr_q.
    always_ff @(posedge axi_clk) begin
        ca_waddr_q <= ca_tail[CAPTR_W-1:0];
        ca_wdata_q <= {rq_ztest, rq_be, rq_alpha, frag_coladdr, rq_color};
    end
    always_ff @(posedge axi_clk)
        if (ca_wr_q) ca_mem[ca_waddr_q] <= ca_wdata_q;

    // ---------------- color writer (reused; gs_clk tied to axi_clk => internal FIFO is same-clock) ----------------
    logic        col_px_emit; logic [31:0] col_px_addr; logic [31:0] col_px_pix32; logic col_flush, col_drained;
    logic        col_px_ready;   // Ch357 (Codex) — color-writer producer-backpressure ready (elastic stage can accept)
    // Ch357 (Codex) — ONE-ENTRY color output register. Registering the payload here breaks the ca_mem->barrel-insert
    // cone (the -0.98ns 310MHz path) AND holds the emit valid until u_c accepts it, so no pixel is dropped.
    logic        col_out_valid; logic [31:0] col_out_addr; logic [31:0] col_out_color; logic [16:0] col_out_alpha; logic [3:0] col_out_be;
    logic        blend_in_ready, blend_out_valid, blend_idle, col_writer_idle;
    logic        blend_write_pending, blend_flush_issued, blend_drain_seen_low;
    wire         blend_input_gate = !blend_write_pending;
    logic [31:0] blend_out_addr, blend_out_color;
    gs_lpddr_color_blend u_blend (
        .clk(axi_clk), .rst_n(axi_rst_n),
        .in_valid(col_out_valid && blend_input_gate), .in_ready(blend_in_ready), .in_addr(col_out_addr), .in_color(col_out_color), .in_alpha(col_out_alpha), .in_be(col_out_be),
        .out_valid(blend_out_valid), .out_ready(col_px_ready), .out_addr(blend_out_addr), .out_color(blend_out_color), .idle(blend_idle),
        .araddr(d_araddr), .arlen(d_arlen), .arsize(d_arsize), .arburst(d_arburst), .arvalid(d_arvalid), .arready(d_arready),
        .rdata(d_rdata), .rresp(d_rresp), .rlast(d_rlast), .rvalid(d_rvalid), .rready(d_rready)
    );
    logic        col_commit; logic [1:0] commit_sr;
    // one-shot ctrl_commit toggle after reset to latch fb_base=COLBASE
    always_ff @(posedge axi_clk or negedge axi_rst_n)
        if (!axi_rst_n) begin commit_sr<=2'b00; col_commit<=1'b0; end
        else begin commit_sr<={commit_sr[0],1'b1}; if (commit_sr==2'b01) col_commit<=~col_commit; end
    gs_lpddr_axi_master #(.FIFO_DEPTH(COL_DEPTH), .PIX_BYTES(4), .ELASTIC_BACKPRESSURE(1'b1)) u_c (
        .gs_clk(axi_clk), .gs_rst_n(axi_rst_n), .enable(enable),
        .arm(enable), .canary(1'b0), .fb_base(COLBASE), .ctrl_commit(col_commit),
        .px_emit(col_px_emit), .px_addr(col_px_addr), .px_pix32(col_px_pix32), .px_ready(col_px_ready), .flush(col_flush),
        .axi_clk(axi_clk), .axi_rst_n(axi_rst_n),
        .awaddr(c_awaddr), .awlen(c_awlen), .awsize(c_awsize), .awburst(c_awburst), .awvalid(c_awvalid), .awready(c_awready),
        .wdata(c_wdata), .wstrb(c_wstrb), .wlast(c_wlast), .wvalid(c_wvalid), .wready(c_wready),
        .bvalid(c_bvalid), .bready(c_bready), .bresp(c_bresp),
        .beats_written(c_beats_written), .bursts_issued(), .bresp_err_count(),
        .fifo_overflow_count(col_ovf), .idle(col_writer_idle), .frame_drained(col_drained)
    );

    // Destination RMW reads must observe the preceding blended/masked write.  Merely
    // waiting for u_c.idle is insufficient: the elastic packer can hold a
    // partial beat while the AXI-side FIFO still reports empty.  Flush an
    // ordered marker after every blended output and wait for col_drained to go
    // low then high.  That high transition is after the data beat's BRESP, so
    // the next destination read sees the committed result.  This deliberately
    // serializes the color stream only while an ABE or partial-byte write is
    // being committed.
    wire blend_rmw_in_accept = col_out_valid && blend_in_ready && blend_input_gate
                             && (col_out_alpha[16] || (col_out_be != 4'hF));
    wire blend_write_accept  = blend_out_valid && col_px_ready && blend_write_pending;
    always_ff @(posedge axi_clk or negedge axi_rst_n) begin
        if (!axi_rst_n) begin
            blend_write_pending  <= 1'b0;
            blend_flush_issued <= 1'b0;
            blend_drain_seen_low <= 1'b0;
        end else begin
            if (blend_rmw_in_accept) begin
                blend_write_pending  <= 1'b1;
                blend_flush_issued <= 1'b0;
                blend_drain_seen_low <= 1'b0;
            end else if (blend_write_accept) begin
                blend_flush_issued <= 1'b1;
            end else if (blend_write_pending && blend_flush_issued) begin
                if (!col_drained)
                    blend_drain_seen_low <= 1'b1;
                else if (blend_drain_seen_low) begin
                    blend_write_pending  <= 1'b0;
                    blend_flush_issued <= 1'b0;
                    blend_drain_seen_low <= 1'b0;
                end
            end
        end
    end

    // ---------------- control FSM (axi_clk): feed Z, emit color on pass, drain on scene marker ----------------
    typedef enum logic [1:0] { C_RUN, C_MARK_WAIT, C_DRAIN } cst_t; cst_t cst;

    // POP only when the head stage and the synchronous-read pipeline are empty;
    // CONSUME the staged fragment via feed_ok.  req_rd remains a registered
    // pulse, isolated from feed_ok and the color-align control cones.
    wire req_read_accept = req_rd && !req_rempty;
    // feed the STAGED fragment into the Z RMW (and push its color) when it is a fragment and both can accept
    wire feed_ok = (cst==C_RUN) && req_stage_frag_valid && z_fready && !ca_full;
    // consume the STAGED end-of-scene marker once all fed fragments have drained (see C_MARK_WAIT)
    wire marker_consume = (cst==C_MARK_WAIT) && req_stage_marker_valid && ca_empty && z_idle && !col_out_valid && blend_idle && !blend_write_pending;
    assign z_fvalid = feed_ok;
    // Ch357 (Codex) — PRODUCER BACKPRESSURE via a ONE-ENTRY output register (col_out_*). Consume a Z result only when the
    // output register can accept the color it would emit: z_pready = !col_out_valid || col_px_ready (empty, OR draining
    // this cycle -> simultaneous drain/refill). ca_head pops on z_pvalid && z_pready (pass OR reject; a reject consumes its
    // CA entry WITHOUT loading the output register). col_px_* are driven from the REGISTER (not combinationally from
    // ca_mem), which breaks the ca_mem->u_c barrel-insert cone (the -0.98ns 310MHz path) AND holds the payload valid until
    // u_c accepts it (px_ready), so no pixel is dropped. No comb loop: col_px_ready depends only on u_c's registered stage.
    assign z_pready = !col_out_valid || (blend_in_ready && blend_input_gate);
    wire [CAW-1:0] ca_headword  = ca_mem[ca_head[CAPTR_W-1:0]];
    wire           ca_head_zt   = ca_headword[85];
    wire [3:0]     ca_head_be   = ca_headword[84:81];
    wire [16:0]    ca_head_alpha= ca_headword[80:64];
    wire [31:0]    ca_head_addr = ca_headword[63:32];
    wire [31:0]    ca_head_col  = ca_headword[31:0];
    wire           z_consume    = z_pvalid && z_pready;                    // pop the color-align head this cycle
    wire           z_emit_pass  = z_consume && (z_ppass || !ca_head_zt);   // load the output register (Z pass or non-Z frag)
    assign col_px_emit  = blend_out_valid;
    assign col_px_addr  = blend_out_addr;
    assign col_px_pix32 = blend_out_color;
    always_ff @(posedge axi_clk or negedge axi_rst_n) begin
        if (!axi_rst_n) begin
            cst<=C_RUN; ca_head<=0; ca_tail<=0;
            ca_wr_q<=1'b0;
            col_flush<=0; z_sflush<=0; frame_drained<=0;
            col_out_valid<=0; col_out_addr<=0; col_out_color<=0; col_out_alpha<=0; col_out_be<=4'hF;
            req_stage_valid<=1'b0;
            req_rd<=1'b0; req_read_pending<=1'b0;
        end else begin
            col_flush<=blend_write_accept;
            ca_wr_q<=1'b0;

            // u_req has a registered read port.  Allow one accepted pop, wait
            // one cycle for its RAM output, then fill the request-head stage.
            // The pending gate prevents a second pop while that word is in
            // flight but req_stage_valid has not asserted yet.
            req_rd <= !req_stage_valid && !req_read_pending && !req_rempty && !req_rd;
            req_read_pending <= req_read_accept;

            // Capture one cycle after the synchronous FIFO pop.  Capture needs
            // an empty stage; consume needs a full one, so they remain mutually
            // exclusive.
            if (req_read_pending) begin
                req_stage_valid  <= 1'b1;
            end
            if (feed_ok || marker_consume) req_stage_valid <= 1'b0;

            // push color-align entry as we feed a fragment
            if (feed_ok) begin
                ca_wr_q    <= 1'b1;
                ca_tail <= ca_tail + 1'b1;
            end

            // ONE-ENTRY color output register. DRAIN when u_c accepts (px_ready); may be re-loaded the same cycle (refill
            // below) for simultaneous drain/refill. A consumed Z result (z_consume = z_pvalid && z_pready) pops the
            // color-align head (pass OR reject); a PASS additionally loads the register (a reject writes no color).
            if (col_out_valid && blend_in_ready && blend_input_gate) col_out_valid <= 1'b0;
            if (z_consume) begin
                ca_head <= ca_head + 1'b1;
                if (z_emit_pass) begin
                    col_out_valid <= 1'b1;
                    col_out_addr  <= ca_head_addr;
                    col_out_color <= ca_head_col;
                    col_out_alpha <= ca_head_alpha;
                    col_out_be    <= ca_head_be;
                end
            end

            case (cst)
                C_RUN: if (req_stage_marker_valid) cst<=C_MARK_WAIT;   // staged marker: stop feeding, wait in-flight to drain
                C_MARK_WAIT: begin
                    // all fed fragments have resulted (CA empty), Z idle, AND the output register drained to u_c
                    if (ca_empty && z_idle && !col_out_valid && blend_idle && !blend_write_pending) begin
                        col_flush<=1'b1;           // push color partial beat + EOF marker
                        z_sflush <=1'b1;           // flush the dirty Z line (cache persists)
                        // req_rd pops the marker this cycle (see assign)
                        cst<=C_DRAIN;
                    end
                end
                C_DRAIN: begin
                    z_sflush<=1'b1;                // hold until z_drained
                    if (col_drained && z_drained) begin
                        frame_drained<=1'b1; z_sflush<=1'b0; cst<=C_RUN;
                    end
                end
                default: cst<=C_RUN;
            endcase
            // frame_drained is a per-scene ack; drop it once new fragments flow again (feed_ok => a fresh fragment consumed)
            if (feed_ok) frame_drained<=1'b0;
        end
    end

    assign idle = (cst==C_RUN) && req_rempty && !req_stage_valid && ca_empty && z_idle && !col_out_valid && blend_idle && !blend_write_pending;
endmodule : gs_lpddr_zc_emit
