I have a Razer Basilisk V3 35K. Its keys and DPI stages live in the mouse's on-board memory, so they keep working without any software. The lighting does not. Close Synapse and the effect you built in Chroma Studio is gone: the mouse goes back to spectrum cycling.
So if you want your own lighting, Synapse has to stay running. On my PC that means about 30 Razer processes and somewhere between 600 MB (in the tray) and 940 MB (window open) of memory. One machine, not a benchmark, but that is a lot for a few LEDs.
The choice: don't write another editor
The usual answer to this is a replacement for Synapse, with its own interface and its own effects. I didn't want that. The Chroma Studio editor is fine, and I had already built my effects in it. The heavy part is not the editing, it is keeping everything running afterwards.
So the split is this: Synapse stays the editor, and a small daemon takes over once Synapse is closed. Nothing to rebuild and nothing new to learn.
The effects are already on disk
Synapse writes a lot to its own log files, under %LOCALAPPDATA%\Razer\RazerAppEngine\User Data\Logs. Among other things:
- the layers of every Chroma Studio effect, with their parameters;
- which profile sits in which on-board slot of the mouse;
- the LED map: which cell of the Chroma Studio grid is which physical LED.
The daemon reads the logs from the oldest to the newest, so the last definition of each effect wins. That is the whole import. The price is an obvious one: Razer can change the format with any update. If that happens, the daemon keeps playing the last effects it read.
Matching a profile to an effect
Synapse has no link between an on-board profile and a Chroma Studio effect, so I needed a rule that asks nothing of the user. Names do the job: a profile called Gaming gets the effect Chroma Effect Gaming. One word in common is enough.
fn tokens(name: &str) -> HashSet<String> {
let name = name.to_lowercase();
let name = name.strip_prefix("chroma effect").unwrap_or(&name);
name.split(|c: char| !c.is_ascii_alphanumeric())
.filter(|t| !t.is_empty())
.map(String::from)
.collect()
}
The effect with the most words in common wins. A profile with no match keeps its lights off, and the log says so.
Talking to the mouse
None of this is documented. The commands were found by trying them on the mouse one at a time. Three of them carry the whole program:
- one register tells which on-board profile is active, so the daemon knows when you press the profile button under the mouse;
- one command sets every LED individually, and the daemon uses it to draw the effect frame by frame;
- one command jumps to a given slot, used to skip the slot the firmware reserves for itself, so that three profiles are three clean presses.
It is plain user-mode HID: no kernel driver, and it never sends keys or clicks.
A Synapse that gets killed leaves the mouse in "driver mode", where the profile button no longer switches slot. The daemon puts the mouse back in normal device mode at start.
Handing the mouse back and forth
Two programs cannot drive the same LEDs. When you launch Synapse, the daemon notices within about a second and a half, starts the Razer services, releases the mouse and waits. When you close Synapse, it reads the logs again, stops the services and takes the mouse back with your changes. You never touch a terminal or a config file.
From Python to Rust
The first version was Python: 15 MB of private memory and about 0.7 % of one core. The Rust port is a single 603 KB executable using about 2 MB.
One layer is expensive: Ambient Awareness, which follows the colours of the screen. Capturing the screen needs the GPU driver, about 55 MB more. To keep everyone else at 2 MB, the two DLLs are delay-loaded, so Windows maps them at the first call, which never comes if no active effect uses that layer:
for dll in ["d3d11.dll", "dxgi.dll"] {
println!("cargo:rustc-link-arg-bins=/DELAYLOAD:{dll}");
}
println!("cargo:rustc-link-arg-bins=delayimp.lib");
What it does not do
- Only one mouse is verified, the one I own. Others with on-board profiles and per-LED lighting should work, but nobody has tried.
- You still need Synapse installed, as the editor.
- It runs elevated and stops the Razer services while it owns the mouse.
- Chroma Generate effects are not rendered.
- The executable is not code-signed.
One mouse is not much
I can only test what is on my desk, so the program has a command for everyone else: demonnapse.exe check. With Synapse closed, it opens its own window and takes about two minutes. It names the model it found, reads the profiles, asks the mouse itself how many LEDs it has and in which zones, then lights them one at a time and asks you whether each one lit.
The same command recognises a Razer keyboard too. DemonNapse does not play effects on keyboards yet, and that part of the check has never been tried on a real one, so a report from a keyboard is exactly what is missing.
It needs no configuration and writes nothing to the mouse's memory. When it ends the LEDs stay off: they come back with the profile button or by opening Synapse again. The report is a text file saved next to the exe, and attached to a device report on GitHub it is what I need to add that model.
The code is on GitHub, GPL-3.0: https://github.com/gian0052/DemonNapse
It is an independent project, not affiliated with Razer.
This article was written with the help of AI.
Top comments (0)