WIP: feat(haptics): a DualSense's fine haptics, from the game's own bands #405
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!405
Loading…
Reference in a new issue
No description provided.
Delete branch "Stellarznith/Longshot:feat/dualsense-haptics"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type: feat · Risk: low · Tested on: Windows 11, RTX 4060 Ti, DualSense on USB · Needs: Linux, a Switch Pro Controller, a DualSense on Bluetooth
Why
A DualSense on a cable plays the game's vibration through its two motors only. The game writes a value of four floats to
nn::hidfor every hand: a low band and a high band, each with its own amplitude and frequency.host/nn_hid.c'srumble_mapflattens those four into two motor strengths, and only a Switch Pro Controller or a Joy-Con gets them whole, as HD Rumble. So a sword hit, a footstep and a rolling boulder reach a DualSense as the same buzz, and the layering the game wrote is lost.See #752, the request for exactly this.
What changed
A DualSense on a cable now plays the game's own two bands instead of one buzz.
host/haptic.chands the four floats thatnn::hidreceives to RenHDrum, which turns them into the pad's audio haptics on its four-channel 48 kHz endpoint, found by name and by channel count and never the desktop's speakers; a Switch Pro Controller keeps its HD Rumble, a DualSense on Bluetooth keeps the standard rumble, and every failure (no DLL, wrong ABI, no endpoint) falls back to the standard rumble with the reason in the menu. The Controls page gains four rows: the mode, the fine haptics' own strength, a read-only status line and a Test haptics action.host/haptic.c,host/haptic.h(new): the haptics back end. It takes the same four floatsrumble_mapreceives and hands them, unflattened, to RenHDrum (third_party/renhdrum, MIT OR Apache-2.0, v0.3.0, ABI0x000300), which turns them into the DualSense's own audio haptics. The output goes to the controller's four-channel 48 kHz playback endpoint, found by name ("DualSense" or "Wireless Controller") and by four channels, never to the desktop's speakers.renhdrum.dllis loaded by full path from the game's own folder withLoadLibraryExW(..., LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32), its ABI version checked, and every failure (no DLL, wrong version, no endpoint, init refused) falls back to the standard rumble with the reason in the status row. A 1 ms worker keeps a silence watchdog, ends the test buzz on its own, and re-enumerates the audio devices when they change.host/hos_sdl.c,host/hos_sdl.h: the SDL3 calls that endpoint needs — playback device enumeration with each device's format, a stream opened on a named device, its health, andSDL_GetJoystickConnectionStatefor whether a pad is on a cable.host/pad_sdl.c: the pad's arrival and departure, the mode and both strengths reach the back end each poll; a pad the haptics drive is sent no SDL rumble at the same time (hos_haptic_owns).host/nn_hid.c:vibration_sendpasses the same four floats tohos_haptic_push, beside the existinghos_sdl_rumble_hd.host/hos_main.c:hos_sdl_haptic_shutdown()oncennMainreturns, so the endpoint and the worker are let go.gpu/vk/vk_menu.cpp,gpu/vk/vk_menu.h,gpu/vk/vk_replay.cpp,gpu/vk/vk_api.h: four rows on the Controls page after Rumble strength — Haptic feedback (Auto, Standard rumble, DualSense haptics, Off), Haptic strength (Game's own, or 25-200 % of what the game asks for), the read-only Haptic status line, and Test haptics, a 400 ms buzz that ends by itself. The Rumble strength stays the one master strength and its Off stops every backend; the percent is applied once, as a gain after the DSP, because RenHDrum raises each amplitude to the power 0.5 and normalises two bands that add past 1, so a factor on its input came out as its square root and stopped changing above about 30 % of the game's amplitude.gpu/vk/menu_i18n.h: the new texts in all 13 languages.nx2/build.py,nx2/make_game.py:renhdrum.dllgoes beside the game, as the SDL DLLs do. Optional: without it the game plays the standard rumble.README.md,docs/development.md: the four rows, thehapticandhaptic_strengthini keys.tests/test_haptics_menu.py(new) and the checks that read the changed source.Flags and settings
haptic(ini, the Haptic feedback row)haptic_strength(ini, the Haptic strength row)renhdrum.dllbeside the gamemake_game.pycopies it fromthird_party/renhdrum/when it is thereMeasurements
Verdict: it works and it is not a subtle difference: the same game values that reached a DualSense as one buzz now reach it as the game's own two bands, and the strength control multiplies the coils' output straight (25 / 50 / 100 / 150 / 200 % of the peak, measured), with no clipping and no rebuild of the audio stream. Nothing regressed: every check that fails here fails on
maintoo.Hardware: Windows 11 Pro 26300, RTX 4060 Ti 16 GB, NVIDIA 617.42, i5-14600KF, 32 GB
Scene: a copied save in Kokiri Forest (adult), the pause menu's System page and the gameplay behind it; 1589x720 window, default options, frame generation off
Method: the game's own
logs/longshot_log.txtlines —haptic:for the back end's state and the peaks it played,vk: ... presents, ... fpsfor the frame rate; one run of this PR, with the five Test haptics buzzes in itBaseline: the
Beforecolumn is a run of the same save on the same PC with the same options,hapticabsent from its ini (the key does not exist there); its log is the one the game writes tologs/longshot_log.txt. The haptics were measured before the rebase ontoaa95d35f; the rebase moved option ids and nothing in the audio path (see Reviewer notes).How to reproduce:
python -I -B nx2/make_game.py --romfs <romfs> --exefs <.longshot_exefs> --out <dir> --jobs 12 --no-prepare-shaderswithLONGSHOT_MINGWset, copySDL2.dllandSDL3.dllbeside the exe, setbutton_prompts=4andhaptic=0inlongshot_options.ini, start it with a DualSense on a USB cable, Continue, then the pause menu's System page and Test hapticsaa95d35f083f33brumble_map, and not onehaptic:line in the whole run (0 matches forhapticand forrenhdruminmain @ aa95d35f's log;hid: 4096 vibration values, strongest 0.00 since the last countis all it says about vibration)haptic: audio endpoint "扬声器 (DualSense Wireless Controller)": 4 channels,haptic: 4-channel 48 kHz audio on the DualSense's endpoint, 203,120 values in the longest 5 s window, peaks 0.288 on the page and 0.799 on the test buzzhaptic: test, 400 mslines, each ending on its own6 playback devices, none of them a DualSense with four channels yet,waiting for the DualSense's audio endpoint: plug it in by cable; plugged back: the endpoint reopened, no crash, no stuck vibrationhid: vibration #38304: handle 00000003, low 0.06 @ 178 Hz, high 0.02 @ 306 Hz, log lines 4706-4711, before any Test haptics) played through the shippedrenhdrum.dllpeak at 0.309 / 0.306 / 0.292; the run's peak 0.288 on log line 4803, 1426 lines before the first test buzz (line 6229), is thatvk: 53880 presents, 60.0 fps,vk: 53760 presents, 60.1 fpsFlags and settings
It adds two menu rows on the Controls page (Haptic feedback, Haptic strength), a read-only status row and a Test haptics action, and with them the ini keys
hapticandhaptic_strength;renhdrum.dllis the one new file beside the game.haptic(the Haptic feedback row)haptic_strength(the Haptic strength row)renhdrum.dllbeside the gamethird_party/renhdrum/bynx2/build.pyandnx2/make_game.pywhen the file is presentTesting
Tested: Windows 11 Pro 26300, RTX 4060 Ti, NVIDIA 617.42, i5-14600KF, 32 GB, llvm-mingw 20261104. In the game: a copied save, Kokiri Forest (adult) — Continue from the title screen, the pause menu's System page, Test haptics five times, the gameplay behind it, and a USB unplug and replug in the middle of the run; the log lines above come from that run, and
docs/tested/2026-11-14-dualsense-haptics-yg-run01.mdrecords it. Game-free:python -I -B tests/run_all.pyon the rebased tree (083f33b,LONGSHOT_MINGWset, noCC/CXX) — 504 passed, 19 failed, 72 skipped; the same suite on a worktree ofmain @ aa95d35f— 503 passed, 18 failed, 72 skipped, its failure set exactly this one's minustest_mesh_args_cleanup, which passes on its own here (a file-lock race under the runner's 20 parallel checks, not a check on this diff). The shared failures are the ones this PC cannot satisfy:test_bug_report(a Chinese-locale console read as UTF-8),test_cpp_preflight,test_appimage_player_build,test_crash_dialog,test_cpm_discovery,test_env_on,test_lift_cleanup_error,test_make_game_cache,test_make_game_prepare,test_missing_nup,test_options_budget_once,test_readback_late,test_vk_pgo,test_walk_key,test_release,test_linux_preflightandtest_oot_state_tap(a tool, a dependency or a POSIX facility this host lacks), andtest_tested_configurations(a CRLF/LF difference in a piped read on Windows). The newtests/test_haptics_menu.pyreads the rows, their steps, the read-only status, the option ids, andhost/haptic.c's loading, ABI check, gain placement and callback rules;tests/test_renhdrum_pin.pydrivesscripts/renhdrum.pyover a synthetic PE (a pinned copy passes, one importing VCRUNTIME140.dll is refused and says why, one missing an export names it, other bytes fail, a damaged file does not end the run with a traceback, and no copy at all says what to do).Not tested: a DualSense on Bluetooth (Bluetooth exposes no four-channel sound card, and this build does not implement the pad's HID haptics reports, so the status row says so and the standard rumble plays; the row itself was not exercised), a Switch Pro Controller and a Joy-Con in this build (their HD Rumble path is untouched but unmeasured here), and the Linux build (this host has no WSL or container;
host/haptic.ckeeps every Win32 and RenHDrum symbol inside#ifdef _WIN32, so the file compiles to the fallback there, but that is a reading, not a build).Must be tested:
haptic: ... the standard rumble only, expecting no build error and no crash (by hand: whoever takes T-2ZP5, or the Test Farm with@Beanstalk linux).main, expecting the same feel and the samehid: ... HD rumblelines (by hand: T-2ZP5).Risks
What could break: a wrong endpoint chosen on a machine whose speakers are named "DualSense" (the name and the four channels are both required); an audio device change mid-game leaving the stream open (the worker re-enumerates every 2 s and closes the old stream before the DSP); the haptics thread on exit (the stream is destroyed before the DSP, and
hos_main.ccalls the shutdown oncennMainreturns).Turning it off: Haptic feedback = Off, or Rumble strength = Off, which stops every backend;
haptic=3inlongshot_options.inidoes the same for a build that will not start; removingrenhdrum.dllfalls back to the standard rumble. Reverting the commits is enough to getmain's behaviour back.Compatibility:
longshot_options.inigains two keys (haptic,haptic_strength), written on the next save of the options; a file without them loads as Auto and Game's own. Saves, the caches and the builder's tools are untouched, andMKWVK_API_VERSIONstays as it is (no member ofMkwVkHostorMkwVkRenderermoves).Issues
See #752 — the request for RenHD Rumble in Longshot, which this PR answers.
Checklist
Everyone
type(scope): description) of the status line's type.main.python -I -B tests/run_all.pypasses (any failure that also fails onmainis named).third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.docs/tested/is one run of this PR and one ofmain, on the same PC, save and settings; the numbers are counts and peaks from the log rather than frame times, so a second pair is worth having before this leaves WIP.)docs/tested/for each new measured run (format indocs/tested-configurations.md).mainand passing here.tests/that reads changed source updated in this PR, not weakened.host/haptic.ckeeps every Win32 and RenHDrum symbol inside#ifdef _WIN32, which is a reading of the file, not a build. This is one of the things T-2ZP5 asks for.)Reviewer notes
host/haptic.cis the file to read first: the lock order isg_lifethen one ofg_host,g_eng,g_audio,g_note, never two inner ones at once, and the audio callback takesg_engonly, for one fill. The strength gain sits inendpoint_pull, not inhos_haptic_push, and the comment there says why (RenHDrum'sa^0.5and its normalisation).third_party/renhdrum/PROVENANCE.mdrecords the version, the licence and what is not used (the GPL integration folders).gpu/vk/vk_api.hchanges only by fourHOS_OPT_*ids in the existing enum: no member ofMkwVkHostorMkwVkRenderermoves, soMKWVK_API_VERSIONstays as it is. They are 63, 64, 65 and 66 (mode, status, strength, test): the branch first took 58, and main's render thread took 58 while this was open, so the haptics moved past it rather than renumbering main's row.python -I tools/check_opt_ids.pyover every id in the header, implicit enum sequences included, finds 39 ids and no value used twice.The rebase onto
aa95d35f. The branch was 58 commits behind; eight files conflicted and each was settled by keeping both sides' meaning:gpu/vk/vk_api.h(the ids above),gpu/vk/vk_replay.cpp(main'sHOS_OPT_SKIP_TITLEandHOS_OPT_RENDER_THREADrows, then the four haptics),nx2/build.py(therenhdrum.dllcopy kept; this branch'svk_prebuiltblock dropped, since main removed that mechanism),gpu/vk/vk_menu.cpp(three places: main'sweak_laptop_apply, main's richer prompt chain with itsfieldandcheatbranches — this branch only addsis_haptic_test_rowto that chain's last arm — and main'sfield_rowkey branch beside the Test haptics one),gpu/vk/menu_i18n.h(main's 13 language rows, then the counts recomputed: every table sorted, no repeats, all 17 new texts present in all 13),README.md,RELEASING.mdanddocs/development.md(both sides' paragraphs). The audio path is untouched by the rebase:host/haptic.c,host/nn_hid.c,host/hos_sdl.candhost/pad_sdl.cmerged without a conflict.The game sends nn::hid a vibration value of four floats: a low band and a high band, each with its own amplitude and frequency, per hand. Today those four become two motor strengths (rumble_map, host/nn_hid.c) and a Switch Pro Controller or a Joy-Con gets them whole as HD Rumble. A DualSense got the motors only, so the layering the game writes was lost: a sword hit and a footstep came out as the same buzz. This adds a haptics back end beside that, not instead of it. The same four floats go to RenHDrum (third_party/renhdrum, MIT OR Apache-2.0, v0.3.0, the stable C ABI at 0x000300) which turns them into the DualSense's own audio haptics, played into the controller's four-channel 48 kHz playback endpoint: the USB endpoint the controller exposes, never the desktop's speakers. - host/haptic.c, host/haptic.h: the back end. renhdrum.dll is loaded by full path from the game's own folder (LoadLibraryExW with LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32), its ABI version checked, and every failure falls back to the standard rumble with the reason in the status. The endpoint is found by name ("DualSense" or "Wireless Controller") and by four channels, reopened when the audio devices change, and a 1 ms worker keeps a silence watchdog so nothing keeps buzzing. - host/hos_sdl.c, host/hos_sdl.h: the SDL3 calls for that endpoint (device enumeration with their formats, a stream opened on a named device, its health, and SDL_GetJoystickConnectionState for the cable). - host/pad_sdl.c, host/nn_hid.c, host/hos_main.c: the wiring. The pad's connection, the mode and the strengths reach the back end each poll; a pad the haptics drive gets no SDL rumble at the same time; the game's exit stops it and lets go of the controller. - menu: Haptic feedback (Auto, Standard rumble, DualSense haptics, Off), Haptic strength (Game's own, or a percent of what the game asks for), the read-only Haptic status line with its reason, and Test haptics, a half second buzz that ends on its own. The Rumble strength stays the one master strength and its Off stops every backend. - Strength is applied once, as a gain after the DSP: RenHDrum raises every amplitude to the power 0.5 and normalises two bands that add past 1, so a factor on its input came out as its square root and stopped changing above about 30 %. - nx2/build.py, nx2/make_game.py: renhdrum.dll goes beside the game, as the SDL DLLs do. It is optional: without it the game plays the standard rumble. Assisted-by: DeepSeek Harnessthis PR @ 2116930d: the Controls page's four new rows, right under Rumble strength — Haptic feedback (Auto), Haptic strength (Game's own or 25-200 %), the read-only Haptic status line, and Test haptics.The status line reads "DualSense haptics (USB)" here because the controller is on a cable and the endpoint was found; the row says "DualSense Bluetooth (standard rumble)" on Bluetooth, "Switch Pro HD rumble" on a wired Pro Controller, "Waiting for the DualSense audio endpoint" while a cable is unplugged, and "Haptic backend unavailable" when
renhdrum.dllis missing or the wrong version.Test haptics plays 400 ms at half the game's amplitude and ends by itself; the log shows one
haptic: test, 400 msline per press, five of them in this run.Pushed
dd6cd26: the Bluetooth wording, a pin forenhdrum.dll, and the strength evidence in the table.
Strength is applied once, on the coils' samples. endpoint_pull (host/haptic.c:194) multiplies the DSP's output, channels 2 and 3 of the 4-channel stream, by strength_gain(). What goes into the DSP (
h.dsp_set_half, host/haptic.c:529) is the game's four values untouched, and the DSP itself is made at a fixed 100 % (
h.dsp_create(100, false)). Feeding the game's own 0.06 @ 178 Hz / 0.02 @ 306 Hz through the pinned DLL: 25 % -> 0.077273, 50 % -> 0.154547, 100 % -> 0.309093, 150 % -> 0.463640, 200 % -> 0.618187, so 1.000x / 1.500x / 2.000x of the 100 % peak, straight and unclipped. Scaled before the DSP instead, the same values read 1.225x / 1.414x, because RenHDrum takes the amplitude to a 0.5 power and normalises: that is the 40 %-plays-at-16 % shape. The two measurements are in ools/measure_gain.py and ools/measure_gain_input.py outside the tree; the numbers are in the table.
The game does send non-zero values, and they are what the peak came from. Log lines 4705-4710 of the run carry six of them, hid: vibration #38304: handle 00000003, low 0.06 @ 178 Hz, high 0.02 @ 306 Hz and its pairs. Played through the pinned DLL they peak at 0.309 / 0.306 / 0.292, which is the peak 0.288 line 4802 reports, 1400 lines before the first haptic: test, 400 ms. A test buzz on the same DLL measures 0.800, matching the 0.799 beside the five buzzes, so the two are told apart. Honest limit: in this scene most of the time the game sends silence, so 49 of the 50 five-second counters say strongest 0.00; one says strongest 0.08.
Bluetooth. The earlier wording read as a physical impossibility. It is not: this build plays haptics through the pad's USB sound card, Bluetooth exposes no such card, and the pad's own HID haptics reports are a path this build does not implement yet. README.md, host/haptic.h and host/haptic.c say exactly that now.
The DLL can be checked without the machine that built it. hird_party/renhdrum/renhdrum.dll.sha256 pins the bytes (414,208 B, sha256 3f3cfa95...5f7940), and scripts/renhdrum.py verifies a copy against the pin, its PE imports (so one linked against VCRUNTIME140.dll is refused) and its five exports; or builds one from a RenHDrum checkout at v0.3.0, or takes one from a url and refuses it unless it matches. RELEASING.md says where it goes in the release's third_party archive. ests/test_renhdrum_pin.py covers all of that on a synthetic PE.
Testing. Windows: built and played here, the numbers above. Linux: not built on this host (no WSL, no container); host/haptic.c keeps every Win32 and RenHDrum symbol inside #ifdef _WIN32 and the file falls back to the standard rumble there, but that is a reading, not a build, so CI is the first real check. A Switch Pro Controller and a Joy-Con were not on this desk; their HD Rumble path is untouched.
Could a maintainer add the esting wanted label? Test request T-2ZP5 is open and nobody but a collaborator can claim it until then. Still WIP on purpose.
Two corrections after re-reading the run's log line by line, and a re-run of the suite on
dd6cd26.dd6cd26: 487 passed, 6 failed, 70 skipped. The six are the same ones a worktree of main @0caf0618fails in this environment ( est_bug_report reads a Chinese-locale console as UTF-8, est_module_ids / est_pad_layout / est_oot_state_tap / est_release each want something this PC lacks, est_tested_configurations is a CRLF/LF difference in a piped read). One more check exists now: ests/test_renhdrum_pin.py.Hi maintainers, the DualSense haptics implementation and Windows hardware tests are complete. Test request T-2ZP5 has been submitted, but it cannot be claimed until the
testing wantedlabel is added.Could someone add that label and approve the pending CI workflows when convenient? Linux and additional controller hardware tests remain explicitly marked as unverified.
Thanks!
Update: points 1 and 2 below are superseded and I no longer recommend them. RenHDrum 0.4.0 plays a DualSense on Switch 2 games with one fixed law, explained here: #405 (comment). Its author tried a stronger lift of the small values and rejected it because it flattens the fine detail, so please follow that comment's suggestions instead. Point 3 is already covered by 0.4.0's 5.3 ms peak hold. Thank you for the explanation. The original text is kept below.
Hi! I did the DualSense voice-coil haptics for NX2navi, a static recompilation of the same game, also adapted from RenHDrum (https://github.com/tangerineturtle1/NX2navi/pull/892). A few things from mine that might help here:
A lift for the game's tiny amplitudes. This game's values are tuned for the Switch 2's actuators and most are under 0.05, so through a plain square root they come out weak. Mine puts each band through a piecewise curve first (0 at or below 0.001, then straight lines through 0.004 at 0.001, 0.55 at 0.1, 0.9 at 0.5 and 1 at 1), then takes the root. A single band at 0.06 then plays at about 0.57 instead of about 0.25, and one at 0.01 at about 0.23 instead of 0.10. A gain after the DSP (the Haptic strength row) raises everything by the same factor, so it can't lift the small values without over-driving the big ones.Sharper starts. The first two 5.3 ms slots of a vibration play at +3 dB and the next six at -3 dB, then as sent, so a hit feels like a hit rather than a swell.What this PR has that mine doesn't: the status row, the test buzz, the Bluetooth fallback and all 13 languages. Happy to help, here or as a follow-up once this lands.
dink221 referenced this pull request2026-11-15 10:10:32 +00:00
dd6cd2612d083f33b2d4Rebased onto
main @ aa95d35f(the branch was 58 commits behind), and the description is updated to match.What the rebase touched. Eight files conflicted, each settled by keeping both sides' meaning:
gpu/vk/vk_api.h— main'sHOS_OPT_RENDER_THREADtook 58 while this was open, so the four haptics ids moved to 63, 64, 65 and 66 rather than renumbering main's row.tools/check_opt_ids.pyover every id in the header, implicit enum sequences included, finds 39 ids and no value used twice.gpu/vk/vk_replay.cpp— main'sHOS_OPT_SKIP_TITLEandHOS_OPT_RENDER_THREADrows, then the four haptics.gpu/vk/vk_menu.cpp— three places: main'sweak_laptop_apply, main's richer prompt chain (with itsfieldandcheatbranches; this branch only addsis_haptic_test_rowto that chain's last arm), and main'sfield_rowkey branch beside the Test haptics one.nx2/build.py— therenhdrum.dllcopy kept; this branch'svk_prebuiltblock dropped, since main removed that mechanism.gpu/vk/menu_i18n.h— main's 13 language rows, counts recomputed: every table sorted, no repeats, all 17 new texts in all 13.README.md,RELEASING.md,docs/development.md— both sides' paragraphs.The audio path is untouched by the rebase:
host/haptic.c,host/nn_hid.c,host/hos_sdl.candhost/pad_sdl.cmerged without a conflict.Checks after it.
python -I -B tests/run_all.pyon083f33b(llvm-mingw 20261104,LONGSHOT_MINGWset, noCC/CXX): 504 passed, 19 failed, 72 skipped. The same suite on a worktree ofmain @ aa95d35f: 503 passed, 18 failed, 72 skipped, and its failure set is exactly this one's minustest_mesh_args_cleanup, which passes on its own here (a file-lock race under the runner's 20 parallel checks). So no check regressed.tests/test_haptics_menu.pyneeded two changes of its own, in this PR rather than weakened: its expected ids are 63-66 now, and the fixture no longer declareskVramMinimumOnly, a constant main'svk_menu.cppdoes not have (with-Werror, an unusedconstexpris an error).Build. Windows incremental, llvm-mingw:
the build took 0:15:36,longshot.exe462,629,888 bytes,renhdrum.dll414,208 beside it. A run of that build logslongshot 083f33b (source 256c3367),haptic: renhdrum.dll 0.3.0 loaded,haptic: audio endpoint "扬声器 (DualSense Wireless Controller)": 4 channels,haptic: worker startedand4-channel 48 kHz audio on the wire, so the chain is intact after the rebase.Still WIP: the checklist's "at least 2 interleaved runs each" and "builds on Linux" boxes are open, and both are named in the description with the reason. T-2ZP5 covers the hardware this PC does not have.
Assisted-by: DeepSeek Harness
Hi @Stellarznith, thank you for building this, and for crediting RenHDrum. The status row, the test buzz and the hot-plug handling are all things I'd like to copy.
A suggestion that may make the "weak vibration" question easier to settle: this PR pins RenHDrum v0.3.0, and 0.4.0 (released yesterday) changes how a DualSense plays Switch 2 games.
min(1, 3.5 * a^0.75). It is turned on by callingrenhdrum_set_nx2_mode(2)once. The 5.3 ms slot peak-hold (so single-update ticks survive) and the 4 ms smoothing are kept.I chose a fixed law over a per-game curve on purpose. I tested several variants by hand on Donkey Kong Bananza, Mario Kart World and Ocarina of Time. A stronger lift of the small values (like the curve @kendowashere describes) makes OoT louder, but it flattens Bananza's layers, where I could tell walking on dirt, water, metal and stone apart with the fixed law. A game-specific curve also means every new game needs its own tuning.
In my own play, OoT is clearly weaker than Bananza, and I think that is intended: the game does not turn everything into strength, it marks the difference between strong and weak spots with small, fine vibration (rain, stepping in water, a spider web). With 0.4.0 these stay distinct, while 0.3.0's square root made the web feel like a heavier hum. This is my own experience on one controller, not a measurement, so please judge it by feel.
So for this PR, I'd suggest:
third_party/renhdrumto v0.4.0.renhdrum_set_nx2_mode(2)when the title is a Switch 2 game, if the PR doesn't already.And if you later want to cover more pads or Bluetooth:
integrations/recomp-cin the RenHDrum repo is a small C bridge for static-recompilation hosts like this one. It drives a DualSense over USB and Bluetooth (report0x32), a Pro Controller, an Xbox One S and, experimentally, the Steam Controller (2026). It also shipsreference/longshot-reference.patch, a full patch against Longshot v3.0.0-rc.3. That is an older base thanmain, so it will need a rebase, and it is only there as reference code: take whatever is useful and ignore the rest. I'm happy to help check anything on the core side.View command line instructions
Manual merge helper
Use this merge commit message when completing the merge manually.
Checkout
From your project repository, check out a new branch and test the changes.