Parcourir la source

Mouse very smooth now but overpolling

Jari il y a 3 mois
Parent
commit
d6bf914c37

+ 4 - 3
OpenPuck/config.cpp

@@ -65,9 +65,10 @@ void applyActiveType()
 // rumble strength % (200 = double); adjustable from the WebUSB panel
 uint8_t g_rumbleScale = 200;
 
-// poll rate is fixed. Faster than the controller can refresh wastes airtime; slower adds latency. Any rate
-// persisted by an older build is ignored and overwritten with the default on boot (see loadCfg).
-const uint32_t g_pollUs = POLL_US_DEFAULT;
+// poll rate defaults to POLL_US_DEFAULT (333 Hz), just above the controller's ~270 Hz drag peak (see
+// config.h for the RE rationale). Live-adjustable via console "PR<hz>" for on-HW sweeps; session-only, so
+// any rate persisted by an older build is ignored and boot always starts at the default (see loadCfg).
+uint32_t g_pollUs = POLL_US_DEFAULT;
 
 #define CFG_FILE "/cfg.bin"
 // Struct layout/semantics changed (haptic-block bytes repurposed); bump so old flash format is discarded ->

+ 14 - 4
OpenPuck/config.h

@@ -156,16 +156,26 @@ extern uint8_t g_swGyroScale10;
 // persist g_swProRate + g_swGyroScale10 to their flash file
 void swProSaveCfg();
 
-// 250 Hz -- matches SC2 input report rate (1000000/250 = 4000 us)
-#define POLL_US_DEFAULT 4000u
+// RF poll cadence: 333 Hz (3000 us). The controller does NOT stream on its own -- the RE air capture
+// (nrf-sniffer HANDOFF.md) shows the puck POLLS (short frame) and the controller auto-ACKs one input
+// frame ~90 us later, so the controller's observed ~137-270 Hz "packet counter" IS the real puck's poll
+// rate (peaking ~270 Hz during a fast trackpad drag). We poll slightly above that peak so a fast drag is
+// never undersampled, without the wasteful stale-ACK repeats that a much higher rate produces (polling
+// faster than the controller refreshes just re-reads the same ACK payload, deduped by seq -> burned
+// airtime, no new samples). The old 250 Hz default sat BELOW the drag peak -> dropped pad samples ->
+// Steam's pad->mouse velocity mapping stair-stepped ("feels like it misses inputs"). Multi-controller
+// degrades gracefully via the rfConnStep catch-up reset.
+#define POLL_US_DEFAULT 3000u
 // host-side HID stream cadence for translated modes (~250 Hz)
 #define USB_STREAM_MS 4u
 // How long a USB suspend must PERSIST before we power the controllers off. A brief selective-suspend
 // (host idle power-management) resumes in <1s and must not trigger a self-inflicted power-off -> the
 // resulting disconnect/reconnect churn looked like random controller drops. Real host sleep persists.
 #define SUSPEND_OFF_MS 4000u
-// RF poll cadence (us). FIXED -- not configurable (see loadCfg).
-extern const uint32_t g_pollUs;
+// RF poll cadence (us). Defaults to POLL_US_DEFAULT; live-adjustable via the console "PR<hz>" command
+// (session-only -- NOT persisted; loadCfg always forces the default on boot) so the sweet spot can be
+// swept on hardware while watching the delivered report rate.
+extern uint32_t g_pollUs;
 
 // loop-timing diagnostics (defined in OpenPuck.ino) -- surfaced in the WebUSB status blob to find what caps
 // the poll rate: avg loop period, slowest section index, and that section's avg us/iteration.

+ 21 - 3
OpenPuck/haptics.cpp

@@ -39,6 +39,13 @@ uint8_t g_landAll87 = 0;
 // amplifier is configured and haptics play as clean ticks instead of a default-amp buzz. On by default;
 // console "AMP" toggles for A/B. See the land01 whitelist in rfConnFlushRelay.
 uint8_t g_landAmp = 1;
+// Master enable for the puck->controller haptic RELAY (Steam OUTPUT reports 0x80-0x86, incl. the trackpad
+// texture-feedback stream Steam pushes WHILE you drag). Each relayed frame is an extra TX that precedes the
+// E3 poll and steals its reply window, and the controller must stop to process it -- both can depress the
+// input rate exactly during a drag. On by default; console "HR" toggles it so the drag-smoothness cost of
+// haptics can be isolated on hardware (drag with it OFF vs ON). OFF only affects Steam-driven rumble/pad
+// feedback; it does NOT touch settings/config (0x87) or power-off (0x9F) relays.
+bool g_hapticRelay = true;
 // Per-slot reconnect block. 0 = idle; non-zero = drop haptics aimed at this slot until millis() catches up.
 unsigned long g_hapticBlockUntil[NSLOT] = { 0 };
 
@@ -500,12 +507,23 @@ bool rfConnFlushRelay(uint8_t ch, uint8_t s1)
 			}
 			// slot was already consumed under the critical section above.
 			// s1 carries a PID distinct from the GET poll (caller cycles it) so the controller's ESB
-			// dedup never treats the GET as a retransmit of this relay. 80us RX: relay is NO-ACK.
+			// dedup never treats the GET as a retransmit of this relay.
+			//
+			// HARVEST the relay's reply as INPUT. Like any frame we send, the controller auto-ACKs a
+			// relay with its current input in the ACK payload (~90us later, per the RE poll->ACK
+			// capture). The old 80us window closed BEFORE that reply arrived, so every relay's input was
+			// discarded -- and yet HW A/B showed the delivered input rate RISING when haptics relay
+			// (an extra frame/cycle during a trackpad drag), because each frame makes the controller
+			// refresh its ACK payload and the following E3 poll caught the fresher value. Reading the
+			// reply directly turns each relay into a SECOND input sample per cycle: rfConnTx runs the
+			// full F1 decode (seq-dedup guards double-forward), so a drag streaming haptics now collects
+			// ~2x the samples, closing the gap to the real puck. A present reply returns early (~90us);
+			// only a genuine no-reply pays the bounded 400us window, so airtime stays in budget.
 			rfConnTx(ch, s1, p, plen,
-				 80); // one relay per poll cycle
+				 400); // one relay per poll cycle -- reply harvested as input
 		}
 	}
-	return have; // true = a relay frame went out this cycle (steals a reply window from the E3 poll)
+	return have; // true = a relay frame went out this cycle (its reply is harvested as input, above)
 }
 
 // Haptic-subsystem re-init: the captured sequence Steam sends when it (re)takes control (0x81 reset + 0x87

+ 3 - 0
OpenPuck/haptics.h

@@ -62,6 +62,9 @@ extern uint8_t g_landAll87;
 // Land Steam's amp/haptic-config 0x87 (regs 0x18/0x2E/0x34/0x35, not gyro 0x30) so haptics play as clean
 // ticks not a default-amp buzz. On by default; console "AMP" toggles.
 extern uint8_t g_landAmp;
+// Master enable for the puck->controller haptic relay (Steam 0x80-0x86 rumble/pad-feedback). Console "HR"
+// toggles it to isolate the drag-smoothness cost of relaying Steam's trackpad haptics. See haptics.cpp.
+extern bool g_hapticRelay;
 
 // Post-connect haptic block (persisted, panel-controlled): when g_hapticBlockOn, Steam haptics are dropped for
 // g_hapticBlockMs after a (re)connect so the controller's haptic engine settles before the first real haptic.

+ 18 - 5
OpenPuck/puck_hid.cpp

@@ -231,8 +231,9 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
 		// path, so this OUTPUT form was still leaking through and clicking. OpenPuck doesn't need it.
 		bool dropOut81 =
 			(g_drop81 && g_usbMode == MODE_STEAM && rid == 0x81);
-		if (rid >= 0x80 && rid <= 0x86 && !dropOut81 && n >= 1 &&
-		    hapticRelaySlotOk(slot) && !lizardActive() && !muted) {
+		if (g_hapticRelay && rid >= 0x80 && rid <= 0x86 && !dropOut81 &&
+		    n >= 1 && hapticRelaySlotOk(slot) && !lizardActive() &&
+		    !muted) {
 			if (!haptic82Blocked(slot)) {
 				relayEnqueue(rid, b,
 					     (uint8_t)(n > RELAY_MAXP ?
@@ -315,10 +316,15 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
 		bool localAnswer = (cmd == 0x83 || cmd == 0x89 || cmd == 0xAE ||
 				    cmd == 0xA2 || cmd == 0xA3 || cmd == 0xAD ||
 				    cmd == 0xB4 || cmd == 0xED || cmd == 0xA4);
-		// never push haptics while presenting lizard (Steam isn't reading 0x45 -> would buzz-loop)
+		// never push haptics while presenting lizard (Steam isn't reading 0x45 -> would buzz-loop).
+		// HR toggle (g_hapticRelay): when off, suppress the actuator/haptic range (0x80-0x86) -- the
+		// trackpad texture-feedback stream Steam pushes while dragging -- to isolate its cost on drag
+		// smoothness. Config (0x87/0x88) and power-off (0x9F) still relay so nothing else regresses.
+		bool hapticCmd = (cmd >= 0x80 && cmd <= 0x86);
 		bool relayOk = hapticRelaySlotOk(slot) && !drop &&
 			       !localAnswer &&
-			       !(haptic82 && (lizardActive() || muted));
+			       !(haptic82 && (lizardActive() || muted)) &&
+			       !(hapticCmd && !g_hapticRelay);
 		if (relayOk && (!haptic82 || !haptic82Blocked(slot))) {
 			// Relay the DECLARED length (up to the 60B RF frame ceiling), not a truncation: Steam's
 			// multi-register 0x87 settings blocks (LED brightness) and calibration writes exceed the old
@@ -713,7 +719,14 @@ void SteamPuckController::onReport45(int slot, const uint8_t *rep, bool fresh,
 		// forward the puck's raw pad coords untouched (Steam does its own interpolation/smoothing). Forward
 		// only FRESH reports: the real puck dedupes, so stale repeats make Steam's velocity/smoothing
 		// stair-step. g_fwdNewOnly toggles for A/B.
-		if ((fresh || !g_fwdNewOnly) && hid[slot].ready())
+		//
+		// Do NOT gate on hid[slot].ready() here: usbTxHid enqueues into a ring buffer (drop-oldest)
+		// that exists precisely to hold a report while the endpoint is briefly busy -- usbTxDrain
+		// sends it the instant the host next polls. Gating on ready() at enqueue time DROPPED every
+		// fresh report that landed while the endpoint was busy (~1 ms after each send), which at
+		// 300+ captured reports/s silently discarded ~a third of them (measured: RF new/s ~313 but
+		// host delivered ~180). Always enqueue; the ring paces delivery to the host without loss.
+		if (fresh || !g_fwdNewOnly)
 			usbTxHid(
 				&hid[slot], rid, rep + 1,
 				blen); // Steam/SDL Triton: input report 0x45 (old) / 0x42 (new fw)

+ 10 - 9
OpenPuck/rf_link.cpp

@@ -42,12 +42,12 @@ bool g_e7announce =
 bool g_e1keepalive = true;
 
 bool g_connVerbose = false;
-// poll RX-window (us): the poll BUSY-WAITS up to this long for the controller's reply, so it is the dominant
-// per-poll loop cost and directly sets the poll rate -- one slot needs (g_rxWin + overhead) < g_pollUs(4000)
-// to sustain 250 Hz. 1200 is the proven 250 Hz value; it was briefly raised to 2000 (issue-72, delayed-reply
-// tolerance) which dropped the rate to ~220. FIXED + not configurable (like g_pollUs): there is no good
-// reason to raise it in the field, and doing so silently halves the poll rate. Any persisted/old value is
-// ignored.
+// poll RX-window (us): the poll BUSY-WAITS up to this long for the controller's reply. The reply returns
+// EARLY (EVENTS_END) on a successful poll, so this window is only paid IN FULL on a genuine no-reply --
+// one slot needs (g_rxWin + overhead) < g_pollUs(3000) so a no-reply poll still fits inside the 333 Hz
+// cycle. 1200 is the proven value; it was briefly raised to 2000 (issue-72, delayed-reply tolerance) which
+// dropped the rate. FIXED + not configurable (like g_pollUs): there is no good reason to raise it in the
+// field, and doing so silently starves the poll cycle. Any persisted/old value is ignored.
 const uint32_t g_rxWin = 1200;
 unsigned long g_connCooldown = 0;
 
@@ -137,7 +137,8 @@ static uint8_t g_lastSeq[NSLOT] = { 0 };
 static uint32_t g_stNew[NSLOT] = {};
 static uint32_t g_stCrc[NSLOT] = {}, g_stNoRx[NSLOT] = {};
 static uint32_t g_chF1[3] = { 0, 0, 0 };
-// Cycle gate: fires once per g_pollUs; each fire polls every warm slot so all run at ~250 Hz.
+// Cycle gate: fires once per g_pollUs; each fire polls every warm slot so all run at ~333 Hz (oversampling
+// the controller's ~270 Hz report generation so no fresh trackpad sample is dropped -- see config.h).
 static uint32_t g_lastPollUs = 0;
 static uint32_t g_connRx = 0;
 static unsigned long g_lastSessBeacon = 0, g_lastDisc = 0;
@@ -785,7 +786,7 @@ uint8_t rfConnTx(uint8_t ch, uint8_t s1, const uint8_t *payload, uint8_t plen,
 #define SLOT_QUIET_RETRY_MS 50u
 static unsigned long g_slotLastAttemptMs[NSLOT] = {};
 
-// Drive the connected-mode sequence. The cycle gate fires once per g_pollUs (250 Hz); each fire
+// Drive the connected-mode sequence. The cycle gate fires once per g_pollUs (333 Hz); each fire
 // polls EVERY warm slot back-to-back so all bonded controllers run at full rate regardless of count.
 // "Cold" means the slot WAS connected but has been silent for > SLOT_COLD_MS; slots that have never
 // replied stay warm so new controllers can connect at any time after boot. Cold slots retry every
@@ -825,7 +826,7 @@ static void rfConnStep()
 		return;
 	}
 
-	// Cycle gate: fires once per g_pollUs (250 Hz). On each fire we poll EVERY warm slot
+	// Cycle gate: fires once per g_pollUs (333 Hz). On each fire we poll EVERY warm slot
 	// back-to-back so all bonded controllers run at full rate -- the real puck services all
 	// controllers per cycle. Polling one-slot-per-call instead tied the per-slot rate to the
 	// loop frequency AND split it across slots, collapsing 2 controllers to ~90 Hz.

+ 4 - 3
OpenPuck/rf_link.h

@@ -43,8 +43,9 @@ extern bool
 
 // full multi-line debug dump vs compact "I45 <hex>" stream
 extern bool g_connVerbose;
-// poll RX-window (us): caps poll rate (~1e6/rxWin). FIXED, not configurable (like g_pollUs) -- raising it
-// drops the poll rate, so it's pinned at the 250 Hz value rather than exposed as a footgun knob.
+// poll RX-window (us): the full window is paid only on a no-reply poll (a reply returns early), so it must
+// stay < g_pollUs to fit inside the 333 Hz cycle. FIXED, not configurable (like g_pollUs) -- raising it
+// starves the poll cycle, so it's pinned rather than exposed as a footgun knob.
 extern const uint32_t g_rxWin;
 
 // set on 0xF2 disconnect; pauses beacon+poll so a powering-off controller can sleep
@@ -95,7 +96,7 @@ extern uint8_t g_slotCrcps[NSLOT], g_slotNoRxps[NSLOT], g_slotRelayps[NSLOT];
 // firmware caught and recovered from a full-link wedge that previously needed a manual puck replug.
 extern uint16_t g_rfStallRecover;
 
-// MEASURED avg us between GET-poll fires (compare to the intended g_pollUs=4000)
+// MEASURED avg us between GET-poll fires (compare to the intended g_pollUs=3000)
 extern uint16_t g_pollPeriodUs;
 
 // Smoothed controller->puck signal strength, sampled by the radio (RSSISAMPLE) on each CRC-good controller

+ 28 - 0
OpenPuck/serial_console.cpp

@@ -40,6 +40,34 @@ void serialConsolePoll()
 				Serial.printf(
 					"# land amp/haptic 0x87 config %s\n",
 					g_landAmp ? "ON" : "off");
+			} else if (!strcmp(line, "SS")) {
+				// on-demand smoothness snapshot: print the last-second cached rates in ONE line, NOW,
+				// instead of waiting for the guarded 1 Hz "# stat" (which the I45/FC flood starves during
+				// a drag). new/s = the FRESH-delivered rate (what Steam receives); noRx = RF packet loss.
+				Serial.printf(
+					"# SS pollrate=%luHz polls=%u/s F1=%u/s new=%u/s crc=%u noRx=%u slot%d\n",
+					(unsigned long)(1000000UL / g_pollUs),
+					g_pollsps, g_f1ps, g_newps, g_crcps,
+					g_norxps, g_curSlot);
+			} else if (!strncmp(line, "PR", 2) &&
+				   (line[2] >= '0' && line[2] <= '9')) {
+				// live poll-rate sweep: "PR<hz>" sets the RF poll cadence (session-only, not persisted).
+				// Clamped 100..1000 Hz. Watch the delivered report rate / feel to find the sweet spot.
+				long hz = atol(line + 2);
+				if (hz < 100)
+					hz = 100;
+				else if (hz > 1000)
+					hz = 1000;
+				g_pollUs = (uint32_t)(1000000L / hz);
+				Serial.printf("# poll rate -> %ld Hz (%lu us)\n", hz,
+					      (unsigned long)g_pollUs);
+			} else if (!strcmp(line, "HR")) {
+				// A/B: disable the puck->controller haptic relay (Steam 0x80-0x86 rumble/pad feedback)
+				// to isolate whether relaying Steam's trackpad texture haptics degrades drag smoothness.
+				g_hapticRelay = !g_hapticRelay;
+				Serial.printf(
+					"# haptic relay (Steam 0x80-0x86) %s\n",
+					g_hapticRelay ? "ON" : "off");
 			} else if (!strcmp(line, "S81")) {
 				// A/B: drop Steam's relayed 0x81 CLEAR_DIGITAL_MAPPINGS (the connect amp-clicker)
 				g_drop81 = !g_drop81;