Skip to content

raphnet: query Windows HidP_GetCaps for feature-report buffer size#132

Merged
JRickey merged 2 commits intomainfrom
agent/raphnet-win-hidcaps
May 4, 2026
Merged

raphnet: query Windows HidP_GetCaps for feature-report buffer size#132
JRickey merged 2 commits intomainfrom
agent/raphnet-win-hidcaps

Conversation

@JRickey
Copy link
Copy Markdown
Owner

@JRickey JRickey commented May 4, 2026

Summary

Fixes intermittent native Raphnet failure for users with gc_n64_usb-v3.6.1 firmware (vid=0x289b pid=0x0061, "N64 to USB v3.6") on Windows, plus adds diagnostic logging so the next failure (if any) is fully captured in release-build BattleShip.log.

The original v0.7.3-beta tester's logs show three intermittent failure modes across ~12 launches:

  • Mode A (8 launches): hid_get_feature_report returns -1 / ERROR_INVALID_PARAMETER (0x57) on the first auto-negotiation probe; both 63 and 32 byte attempts fail identically. Open() returns false → SDL fallback claims the controller.
  • Mode B (4 launches): SUSPEND_POLLING(0) probe succeeds (auto-negotiation passes), then GET_VERSION read times out with n=0, err=Success after 1 s.
  • Mode C (1 launch): full Open() succeeds, every RAW_SI poll then times out and the auto-fallback drops to SDL after 60 frames.

The auto-fallback (PR #111) consistently delivered them a working SDL-mapped controller within 1 s in all three modes, but the "Native Raphnet enabled" banner never stuck.

Root cause (Mode A)

Asymmetric size handling inside hidapi 0.14.0 on Windows:

  • hid_send_feature_report auto-pads the caller's buffer up to caps.FeatureReportByteLength (windows/hid.c:1189-1204, explicit comment: "Windows expects at least caps.FeatureReportByteLength bytes passed to HidD_SetFeature(), even if the report is shorter. Any less sent and the function fails with error ERROR_INVALID_PARAMETER set.").
  • hid_get_feature_report does not auto-pad — it passes the buffer length straight to DeviceIoControl(IOCTL_HID_GET_FEATURE).

So any caller-supplied size that doesn't exactly match caps.FeatureReportByteLength silently sends fine but reads fail with INVALID_PARAMETER. Our hardcoded 64-byte buffer matches the firmware's REPORT_COUNT 63 + 1 report-id byte in the common case, but driver/OS-version interactions can produce a different OS-reported caps value, or transiently fail until the HID handle settles. The auto-negotiation can't recover because the 32-byte fallback is also wrong size, so it fails identically.

Modes B and C are likely the same root cause expressed at different points in the command stream — short replies (SUSPEND_POLLING echo, version string) sometimes squeak through a wrong-size buffer while the longer 7-byte RAW_SI reply does not.

Fix

libultraship/src/ship/controller/raphnet/RaphnetTransport.cpp:

  1. Windows path: query the HID stack directly via HidD_GetPreparsedData + HidP_GetCaps and use caps.FeatureReportByteLength as the authoritative buffer size. Skip the auto-negotiation probe entirely — the OS knows the exact size, so don't guess. (gcn64tools' rntlib/raphnetadapter.c does effectively the same thing internally.)
  2. Linux/macOS: unchanged. The hidraw and IOKit backends are size-tolerant and direct HID API isn't easily exposed; the existing SUSPEND_POLLING(0) opcode-echo probe stays.
  3. 50 ms post-hid_open_path settle delay (Windows only) — Mode B's symptom (read times out after the first command works) and intermittent Mode A failures across launches are consistent with the HID handle not being fully ready for IOCTLs immediately after hid_open_path returns. Invisible to the user, covers the stack initialization window.

Diagnostic logging (second commit)

Release builds default the spdlog level to WARN (Context.h:73), so existing INFO-level instrumentation in the Open path was silently dropped from BattleShip.log. When a tester reports Native Raphnet not working we currently see only the failure ERRORs — not the values that would explain why.

Promote the load-bearing Open-time logs to WARN behind a [raphnet-diag] prefix so the next tester's log captures the whole flow:

  • HidP_GetCaps dumps every relevant field (UsagePage, Usage, InputReportByteLength, OutputReportByteLength, FeatureReportByteLength, NumberLink/Button/Value caps). On any failure path log the GetLastError or NTSTATUS so we can distinguish "device not exposed yet" from "preparsed data corrupt" from "caps unsupported".
  • Each Open-phase Exchange logs n, rep[0], and the raw reply bytes — covers the defensive SUSPEND_POLLING(0), GET_VERSION, SUSPEND_POLLING(1) steps individually.
  • Auto-negotiation (non-Windows fallback) logs every probe size, n, rep[0], reply bytes regardless of outcome.
  • Open() entry logs vid/pid/serial/path at WARN; a separate WARN marks Open() complete with the chosen mReportSize so we know whether failure was inside Open or later during Poll.
  • Hid send/get errors include buffer_size and (for get) retry_count so Mode A's INVALID_PARAMETER can be correlated against the caps-reported FeatureReportByteLength immediately above in the log.
  • FIRST successful poll on a channel is now WARN — canonical "native Raphnet alive" signal. FIRST poll FAILURE already at ERROR; now also includes mReportSize for cross-reference.
  • Per-frame polls stay at TRACE / no-log; only the FIRST per channel is bumped. Open runs once per session so noise is bounded.

Mode C (RAW_SI silently drops after Open succeeds) isn't addressed by code change — but with the new diagnostics, if it persists post-fix the next log will show the full caps fields, every Exchange reply, and the exact mReportSize used, which should be enough to pinpoint whether it's a residual size mismatch, a firmware timing constraint on the SI bus, or something else. The existing auto-fallback continues to handle Mode C correctly: returns the user to a working SDL-mapped controller within 1 s.

Companion libultraship PR

JRickey/libultraship#raphnet-win-hidcaps (commits c53d401f and 9b7f3f99)

Sources

  • raphnet/gc_n64_usb-v3 dataHidReport.c:20-30 — REPORT_COUNT 63, no Report ID; was 40 pre-v3.4
  • raphnet/gcn64tools rntlib/raphnetadapter.c:81 — PID 0x0061 caps row, interface 1, bio_support enabled
  • raphnet/gcn64tools rntlib/raphnetadapter.c:416,525 — default report size 63, send/recv buffer = report_size+1 = 64
  • libusb/hidapi 0.14.0 windows/hid.c:1189-1204 — asymmetric padding (root cause)

Test plan

  • Reviewed: protocol constants, opcodes, and report sizes match raphnet's published firmware and gcn64tools reference exactly
  • Tester with v3.6.1 hardware confirms [raphnet-diag] HidP_GetCaps OK: ... FeatureReportByteLength=N appears in BattleShip.log on next launch
  • Tester confirms native Raphnet banner stays in InputEditor (no auto-fallback to SDL within 1 s) OR captures a full [raphnet-diag] trail showing exactly where the new flow still fails
  • No regression on already-working hardware (mocked transport works in Debug, no real-device verification on macOS/Linux until a v3.6.x adapter is available)
  • CI / Linux / macOS Debug build clean

JRickey and others added 2 commits May 3, 2026 22:02
…on Windows

Pulls in JRickey/libultraship#raphnet-win-hidcaps (commit c53d401f).
Fixes intermittent native Raphnet failure on Windows for raphnet
v3.6.1 firmware (vid=0x289b pid=0x0061, "N64 to USB v3.6"):

  - hid_get_feature_report no longer fails with ERROR_INVALID_PARAMETER
    when the device's caps-reported FeatureReportByteLength differs from
    our hardcoded 64. We query the HID stack directly via
    HidD_GetPreparsedData + HidP_GetCaps and use the OS-reported size.
  - 50 ms post-hid_open_path settle delay (Windows only) covers the HID
    handle initialization window where the first DeviceIoControl could
    return INVALID_PARAMETER on a freshly-opened handle.

Linux/macOS: unchanged. Auto-fallback to SDL when polling fails 60
times in a row continues to work as a safety net for any device that
still can't be polled natively.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls in JRickey/libultraship#raphnet-win-hidcaps commit 9b7f3f99
so testers' release-build BattleShip.log captures every step of the
Raphnet Open() sequence (HidP_GetCaps fields, each command's reply
bytes, firmware version, FIRST successful poll). Default release log
level is WARN+ so the existing INFO-level narration was being dropped.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JRickey JRickey merged commit 3779ac7 into main May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant