/*
 * msk_hevent_replica.c - deterministic replica of if_msk.c's
 * mskc_handle_events()/mskc_intr() status-LE drain protocol.
 *
 * mskc_handle_events() (if_msk.c:3137-3238) walks the DMA'd status
 * ring from sc->msk_stat_cons to the device-supplied STAT_PUT_IDX:
 *
 *   for (cons = sc->msk_stat_cons; cons != idx;) {
 *           control = le32toh(sd->msk_control);
 *           if ((control & HW_OWNER) == 0)
 *                   break;
 *           control &= ~HW_OWNER;
 *           sd->msk_control = htole32(control);      (:3165-3166)
 *           ...
 *           port = (control >> 16) & 0x01;
 *           sc_if = sc->msk_if[port];
 *           if (sc_if == NULL) {
 *                   device_printf("invalid port opcode");
 *                   continue;                         (:3174)  <- BUG
 *           }
 *           switch (...) { ... }
 *           MSK_INC(cons, MSK_STAT_RING_CNT);        (:3224) only advance
 *           if (rxprog > sc->msk_process_limit)
 *                   break;
 *   }
 *   sc->msk_stat_cons = cons;                         (:3229)
 *   return (sc->msk_stat_cons != CSR_READ_2(STAT_PUT_IDX));  (:3237)
 *
 * and mskc_intr() drains it with an unbounded outer loop:
 *
 *   while (mskc_handle_events(sc) != 0)
 *           ;                                        (:3291)
 *
 * The 'continue' at :3174 skips the loop's ONLY consumer-index advance
 * (MSK_INC at :3224).  For a status LE whose port bit names a port
 * with no interface attached (single-port adapter: msk_if[1] == NULL,
 * if_msk.c:1383 only fills the ports that attached), the first pass
 * clears HW_OWNER in the LE and 'continue's with cons unchanged; the
 * second pass reads that same LE, sees HW_OWNER clear and breaks.
 * msk_stat_cons never advances while STAT_PUT_IDX is past it, so
 * mskc_handle_events() returns non-zero forever and the mskc_intr()
 * outer while spins permanently - WITH sc->msk_serializer held
 * (bus_setup_intr muxes the IRQ through it, if_msk.c:1729-1731) and
 * BEFORE the IRQ is re-enabled (B0_Y2_SP_ICR write at :3297): hard
 * system hang, both ports wedged, interrupt never completes.
 *
 * This module reproduces the exact loop shape over a fake 2048-entry
 * status ring (MSK_STAT_RING_CNT = (1+3)*(256+256) = 2048,
 * if_mskreg.h:2313), seeded with one port-1 (unattached-port) LE
 * followed by ordinary entries, and runs the drain in a kernel thread:
 *
 *   mode=0 (shipped shape) : 'continue' skips the advance  -> the
 *                            drain NEVER returns (outer-loop spin count
 *                            grows without bound, stat_cons stuck).
 *   mode=1 (fix.diff shape): MSK_INC(cons) in the NULL-port branch
 *                            -> drain returns 0 immediately, consumer
 *                            index advances past the bad LE, the
 *                            "interrupt" completes cleanly.
 *
 * Instrumentation (NOT part of the replicated code, documented here):
 *   - the outer while checks kern.mskrep2.kill so the module can be
 *     unloaded after the positive test (the replicated inner function
 *     is verbatim; without the kill check the thread would spin
 *     forever exactly like mskc_intr);
 *   - the "invalid port opcode" kprintf fires once (console-flood
 *     guard; the driver would printf on every spin);
 *   - progress kprintf every 1<<26 outer iterations.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/serialize.h>
#include <sys/sysctl.h>
#include <sys/systm.h>
#include <sys/thread.h>

#define	MSK_RX_RING_CNT		256			/* if_mskreg.h:2310 */
#define	MSK_TX_RING_CNT		256			/* if_mskreg.h:2309 */
#define	MSK_STAT_RING_CNT	((1 + 3) * (MSK_TX_RING_CNT + MSK_RX_RING_CNT))
#define	MSK_INC(x, y)		(x) = (x + 1) % y	/* if_mskreg.h:2429 */

#define	HW_OWNER		0x80000000u		/* if_mskreg.h */
#define	STLE_LEN_MASK		0x0000ffffu		/* if_mskreg.h:2275 */
#define	OP_RXSTAT		0xb4			/* pattern, role only */
#define	STLE_OP_MASK		0xff000000u

/* fake coherent status LEs: {control, status} exactly like msk_stat_desc */
struct fake_sd {
	uint32_t	msk_control;
	uint32_t	msk_status;
};
static struct fake_sd stat_ring[MSK_STAT_RING_CNT];

static struct lwkt_serialize ser;	/* == sc->msk_serializer */
static volatile int stat_cons;		/* == sc->msk_stat_cons */
static volatile int put_idx;		/* == CSR_READ_2(STAT_PUT_IDX) */
static volatile int mode = 1;		/* 0 = shipped shape, 1 = fixed */
static volatile int kill;
static volatile long outer_n;
static volatile int spin_marker;
static struct thread *walk_td;
/* sc->msk_if[port]: port 0 attached, port 1 never attached
 * (single-port adapter: if_msk.c:1699-1719 adds only PORT_A). */
static void *msk_if[2] = { (void *)1, NULL };

/* rxeof stub for ordinary port-0 entries */
static int rx_frames;

/* verbatim replica of mskc_handle_events() (if_msk.c:3137-3238) */
static int
handle_events(void)
{
	int rxput[2];
	uint32_t control, status;
	int cons, idx, len, port, rxprog;

	idx = put_idx;					/* :3146 */
	if (idx == stat_cons)
		return (0);				/* :3147-3148 */

	rxput[0] = rxput[1] = 0;
	rxprog = 0;
	for (cons = stat_cons; cons != idx;) {		/* :3153 */
		control = stat_ring[cons].msk_control;
		if ((control & HW_OWNER) == 0)
			break;				/* :3156-3157 */
		control &= ~HW_OWNER;
		stat_ring[cons].msk_control = control;	/* :3165-3166 */
		status = stat_ring[cons].msk_status;
		len = control & STLE_LEN_MASK;
		port = (control >> 16) & 0x01;
		if (msk_if[port] == NULL) {
			if (!spin_marker)
				kprintf("mskrep2: invalid port opcode "
				    "0x%08x (port %d has no ifnet)\n",
				    control & STLE_OP_MASK, port);
			spin_marker = 1;
			if (mode == 1)
				MSK_INC(cons, MSK_STAT_RING_CNT);
			continue;			/* :3174 (bug) */
		}
		/* OP_RXSTAT-ish: count and move on */
		rx_frames++;
		rxput[port]++;
		MSK_INC(cons, MSK_STAT_RING_CNT);	/* :3224 */
		if (rxprog++ > 128)			/* process_limit */
			break;
	}

	stat_cons = cons;				/* :3229 */
	return (stat_cons != put_idx);			/* :3237 */
}

/* verbatim replica of mskc_intr()'s drain + IRQ re-enable (:3291-3297) */
static void
intr_drain(void *arg)
{
	long last_print = 0;

	lwkt_serialize_enter(&ser);			/* ISR muxed thru ser */

	/* while (mskc_handle_events(sc) != 0);  -- kill==0 only */
	while (kill == 0 && handle_events() != 0) {
		if ((outer_n - last_print) >= (1L << 26)) {
			last_print = outer_n;
			kprintf("mskrep2[shipped]: SPINNING outer=%ld "
			    "stat_cons=%d idx=%d (no progress)\n",
			    outer_n, stat_cons, put_idx);
		}
		outer_n++;
	}

	if (kill != 0 && stat_cons != put_idx) {
		kprintf("mskrep2[shipped]: KILLED after %ld outer spins "
		    "with stat_cons STILL STUCK at %d (idx=%d) - the drain "
		    "would NEVER have returned\n", outer_n, stat_cons,
		    put_idx);
	} else {
		kprintf("mskrep2[fixed]: drain returned 0 after %ld outer "
		    "iterations, stat_cons=%d idx=%d rx_frames=%d - "
		    "interrupt re-enabled (B0_Y2_SP_ICR), ISR RETURNS\n",
		    outer_n, stat_cons, put_idx, rx_frames);
	}

	lwkt_serialize_exit(&ser);
	lwkt_exit();
}

static int
run_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return error;
	if (v != 1)
		return 0;

	/* seed: LE0 = port-1 (unattached) RXSTAT; LE1..4 = port-0 RXSTAT */
	bzero(stat_ring, sizeof(stat_ring));
	stat_ring[0].msk_control = HW_OWNER | ((1 << 16) & 0x00010000);
	stat_ring[0].msk_status = 0;
	for (v = 1; v < 5; v++)
		stat_ring[v].msk_control = HW_OWNER;
	stat_cons = 0;
	put_idx = 5;
	outer_n = 0;
	spin_marker = 0;
	rx_frames = 0;
	kill = 0;

	kprintf("mskrep2: mode=%d (%s) - STAT_PUT_IDX=%d, LE0 port=1 "
	    "(no ifnet), HW_OWNER set\n", mode,
	    mode == 0 ? "shipped if_msk.c shape" : "fix.diff shape",
	    put_idx);

	lwkt_create(intr_drain, NULL, &walk_td, NULL, 0, -1,
	    "mskrep2drain");
	return 0;
}

static int
kill_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return error;
	if (v == 1)
		kill = 1;
	return 0;
}

static int
mode_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return error;
	if (v == 0 || v == 1)
		mode = v;
	return 0;
}

SYSCTL_NODE(_kern, OID_AUTO, mskrep2, CTLFLAG_RW, 0, "msk hevent replica");
SYSCTL_PROC(_kern_mskrep2, OID_AUTO, run, CTLTYPE_INT | CTLFLAG_RW, 0, 0,
    run_sysctl, "I", "run the mskc_intr drain replica (seeded ring)");
SYSCTL_PROC(_kern_mskrep2, OID_AUTO, kill, CTLTYPE_INT | CTLFLAG_RW, 0, 0,
    kill_sysctl, "I", "instrumentation: abort the outer spin");
SYSCTL_PROC(_kern_mskrep2, OID_AUTO, mode, CTLTYPE_INT | CTLFLAG_RW, 0, 0,
    mode_sysctl, "I", "0 = shipped if_msk.c shape (spin), 1 = fix.diff shape (clean)");

static int
msk_hevent_load(struct module *m, int what, void *arg)
{
	switch (what) {
	case MOD_LOAD:
		lwkt_serialize_init(&ser);
		kprintf("msk_hevent_replica: loaded (ring=%d)\n",
		    MSK_STAT_RING_CNT);
		return 0;
	case MOD_UNLOAD:
		return 0;
	}
	return 0;
}

static moduledata_t msk_hevent_mod = {
	"msk_hevent_replica",
	msk_hevent_load,
	NULL
};
DECLARE_MODULE(msk_hevent_replica, msk_hevent_mod, SI_SUB_DRIVERS,
    SI_ORDER_ANY);
