How to set up a development environment on a fresh Windows machine, clone the source, and build MDropDX12.
For a fresh Windows machine or Hyper-V VM, two scripts are available in install/:
| Script | Installs | Use case |
|---|---|---|
setup-dev.ps1 |
Git, VS Build Tools, VSCodium, clone + build | Full dev environment |
install-noide.ps1 |
Git, VS Build Tools, clone + build | Build-only (no editor) |
# Full setup with VSCodium
powershell -ExecutionPolicy Bypass -File install/setup-dev.ps1
# Build-only (no IDE)
powershell -ExecutionPolicy Bypass -File install/install-noide.ps1Both scripts install Git, VS 2022 Build Tools (MSVC v143, MSBuild, Windows 11 SDK), clone the repo, and run a Release x64 build. The VS Build Tools install typically takes 15-30 minutes.
Note: Windows Sandbox is not suitable for development — the VS Build Tools installer hangs during the Windows SDK install due to Sandbox I/O constraints. Use a Hyper-V VM or a fresh Windows install instead.
Install the following tools via winget from a PowerShell terminal. On fresh installs where winget is not available, install it first:
# Install winget (App Installer) if not present
Add-AppxPackage -RegisterByFamilyName Microsoft.DesktopAppInstaller_8wekyb3d8bbweIf that doesn't work, download the latest .msixbundle from github.com/microsoft/winget-cli/releases and install it manually:
# Download and install winget + dependencies
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri https://aka.ms/getwinget -OutFile winget.msixbundle
Add-AppxPackage -Path winget.msixbundle
Remove-Item winget.msixbundleAfter installing, close and reopen your PowerShell terminal so winget is on your PATH.
winget install Git.GitOr download from git-scm.com. After installing, restart your terminal so git is on your PATH.
MDropDX12 uses the MSVC v143 compiler and MSBuild. You need the Build Tools package (not the full Visual Studio IDE).
winget install Microsoft.VisualStudio.2022.BuildToolsOr download the installer from aka.ms/vs/17/release/vs_BuildTools.exe.
When the Visual Studio Installer opens, select:
- Workload: "Desktop development with C++"
- Individual Components (verify these are checked):
- MSVC v143 - VS 2022 C++ x64 build tools
- Windows 11 SDK (10.0.26100.0)
winget install VSCodium.VSCodiumOr download from vscodium.com.
After installing, open VSCodium and install a C/C++ extension:
- Open the Extensions panel (Ctrl+Shift+X)
- Search for
clangdand installllvm-vs-code-extensions.vscode-clangd
Note: The Microsoft
ms-vscode.cpptoolsextension is not available on Open VSX (VSCodium's default registry). Theclangdextension provides equivalent IntelliSense and navigation.
git clone https://github.com/shanevbg/MDropDX12.git
cd MDropDX12No manual setup commands are needed. The application self-bootstraps on first run:
- Build the project (see below)
- Run the exe — it detects the empty environment and creates
resources/presets/,resources/textures/, etc. - A Welcome window appears with:
- Browse for Resources Folder... — point it at an existing folder containing presets (
.milkfiles) and textures - Open Shader Import... — import Shadertoy shaders directly
- Open Settings... — configure paths, audio, display options
- Browse for Resources Folder... — point it at an existing folder containing presets (
The settings.ini file is created automatically with defaults when the app first writes settings. No template INI files need to be copied.
Legacy: A
Developer-Setup.cmdscript exists inRelease/for backward compatibility. It copiesconfig/*.iniandresources/into the working directory, but this is no longer required.
The Spout2 SDK (the only external dependency) is automatically fetched by the build script on the first build — no manual setup needed.
powershell -ExecutionPolicy Bypass -File build.ps1 Release x64Arguments: [Debug|Release] [x64] [Clean]
Pass Clean as the third argument to do a full clean + rebuild (deletes all .obj files first).
The build script will:
- Locate MSBuild via
vswhere.exe - Clone the Spout2 SDK into
external/Spout2/if not already present - Kill any running
MDropDX12.exeinstance (the linker overwrites the exe) - Invoke MSBuild with the specified configuration and platform
The project includes pre-configured build tasks in .vscode/tasks.json.
- Ctrl+Shift+B -- runs the default build task (Debug|x64)
- Ctrl+Shift+P > "Tasks: Run Task" -- shows all available tasks:
- Build Visualizer (Debug)
- Build Visualizer (Release)
- Build RELEASE (builds + copies files to Release folder)
- Clean
Press F5 in VSCodium to build and launch the debugger. The launch configuration (.vscode/launch.json) builds the Debug configuration, then runs the exe with Release/ as the working directory.
The exe self-bootstraps on first run — no manual setup of the Release/ folder is needed.
After a Release build, the executable is in the build output directory (see table below). To create a portable release zip:
powershell -ExecutionPolicy Bypass -File release.ps1This builds Release x64, stages the exe + docs into a zip (MDropDX12-v{VERSION}-Portable.zip), validates the zip contents, and updates Release/MDropDX12.exe. The version is read automatically from version.h.
Options:
-SkipBuild— package from an existing build without rebuilding-DryRun— show what would be packaged without creating the zip-GitHubRelease— also create a GitHub release with the zip attached (requiresghCLI)
To test a release build manually, run Release/MDropDX12.exe after the release script updates it.
MDropDX12/
build.ps1 -- Build script (locates MSBuild, fetches Spout2, builds)
release.ps1 -- Release script (build + package + validate + optional GitHub release)
MDropDX12.sln -- Visual Studio solution file
CLAUDE.md -- AI assistant project context and critical warnings
src/
mDropDX12/ -- Main visualizer engine (C++17, DX12, Win32 API)
engine.vcxproj -- MSBuild project file
engine.cpp/h -- Core engine, render loop, state management
dxcontext.cpp/h -- DX12 device, swap chain, descriptor heaps
milkdropfs.cpp -- Preset shader generation (warp/comp/blur)
engine_settings_ui.cpp -- Settings window (5-tab UI: General, Tools, System, Files, About)
engine_input.cpp -- Keyboard/mouse/hotkey handlers
engine_presets.cpp -- Preset loading, parsing, cross-fading
state.cpp/h -- Preset state and parameter storage
textmgr.cpp/h -- DX12 font atlas text rendering (GDI atlas + sprite quads)
...
ns-eel2/ -- Expression evaluator (Cockos WDL ns-eel2, x64 JIT)
external/ -- Auto-fetched dependencies (Spout2 SDK)
resources/
presets/ -- Preset collections (.milk files)
textures/ -- Texture files referenced by presets
config/ -- Default config files and build helpers
docs/ -- Documentation
install/ -- NSIS installer script
.vscode/ -- VSCodium/VS Code IDE configuration
Release/ -- Runtime working directory (populated by Developer-Setup.cmd)
| Configuration | Output Directory |
|---|---|
| Debug|x64 | src/mDropDX12/Debug_x64/ |
| Release|x64 | src/mDropDX12/Release_x64/ |
The build script uses vswhere.exe to locate MSBuild. Ensure:
- Visual Studio 2022 Build Tools is installed
- The "Desktop development with C++" workload is selected
- Restart your terminal after installing
build.ps1 runs git clone https://github.com/leadedge/Spout2.git into external/Spout2/. If this fails:
- Verify Git is installed and on your PATH (
git --version) - Check your internet connection
- Try cloning manually:
git clone --depth 1 https://github.com/leadedge/Spout2.git external/Spout2
If IntelliSense shows false errors or missing includes:
- Open Command Palette (Ctrl+Shift+P) > "C/C++: Reset IntelliSense Database"
- Verify the C/C++ extension is installed and active
- Check that
.vscode/c_cpp_properties.jsonpaths match your SDK installation
The project targets Windows SDK 10.0.26100.0. If you have a different SDK version:
- Open Visual Studio Installer
- Select "Modify" on Build Tools 2022
- Go to "Individual Components"
- Install "Windows 11 SDK (10.0.26100.0)"
- The exe self-bootstraps — it creates required directories on first run
- All shaders are embedded in the exe — no external
resources/data/directory is needed - For Debug builds, the working directory is
Release/(set in.vscode/launch.json) - Check
debug.login the working directory for error details
MDropDX12 has a leveled logging system controlled via settings.ini:
| INI Key | Values | Default | Description |
|---|---|---|---|
LogLevel |
0–4 | 3 | 0=Off, 1=Error, 2=Warn, 3=Info, 4=Verbose |
LogOutput |
1–3 | 3 | 1=File only, 2=OutputDebugString only, 3=Both |
These can also be changed at runtime via Settings -> About tab (log level radio buttons and output checkboxes).
Log output goes to debug.log in the base directory (rotated to debug.prev.log on startup).
Code uses DLOG_ERROR, DLOG_WARN, DLOG_INFO, DLOG_VERBOSE macros (defined in utility.h) which skip all formatting work when the current log level would suppress the message. Prefer these over raw DebugLogA/DebugLogAFmt calls.
DLOG_VERBOSE("DX12: PSO created for %s, slots=%d", name, count);When LogLevel=4 (Verbose), diagnostic dump files are written to the base directory:
| File | Contents |
|---|---|
diag_comp_shader.txt |
Assembled comp/Image shader HLSL sent to compiler |
diag_warp_shader.txt |
Assembled warp shader HLSL sent to compiler |
diag_converter_image.txt |
GLSL->HLSL converter output for Image pass |
diag_converter_bufferA.txt |
GLSL->HLSL converter output for Buffer A pass |
diag_cacheparams.txt |
Shader constant/sampler binding diagnostics |
diag_bindings.txt |
Resource binding slot assignments |
Error files (diag_*_shader_error.txt) are always written on compile failure regardless of log level — these are needed by the Import UI error display.
When debugging Shadertoy imports, the project name from the loaded .json file is included in diagnostic headers (project=filename.json) and the preset description (m_szDesc) is updated to match, so diag files clearly indicate which import is being debugged rather than showing the last regular preset name.
The Shader Import window (opened from Settings) converts Shadertoy GLSL to HLSL for live preview:
- Load Import — loads a
.jsonproject file containing GLSL source for each pass - Convert — runs the GLSL->HLSL converter pipeline
- Apply — compiles HLSL and activates the Shadertoy render path
- Save — exports as
.milk3(Shadertoy preset) or.milk(legacy) - Save Import — saves the project back to
.jsonfor later editing
Import projects (.json) store raw GLSL, channel mappings, and notes. The .milk3 preset format stores converted HLSL and is what the visualizer loads at runtime.
See docs/GLSL_importing.md for details on the GLSL->HLSL conversion pipeline.
Engine source files follow a split pattern:
engine_<feature>.cpp— Engine:: business logic (persistence, dispatch, lifecycle)engine_<feature>_ui.cpp— ToolWindow subclass +Open<Feature>Window/Close<Feature>Windowbridge methods only
Examples: engine_midi.cpp (MIDI persistence, device lifecycle, knob/button dispatch) vs engine_midi_ui.cpp (MidiWindow ToolWindow subclass and open/close bridges).
| Domain | File |
|---|---|
| DX12 rendering, shader passes | milkdropfs.cpp |
| Preset loading/parsing/blending | engine_presets.cpp |
| Shader compilation, text assembly | engine_shaders.cpp |
| GLSL->HLSL conversion | engine_shader_import_ui.cpp |
| Settings persistence, theme, user defaults, folder picker | engine_config.cpp |
| Sprite lifecycle, INI I/O | engine_sprites.cpp |
| MIDI persistence, device lifecycle, dispatch | engine_midi.cpp |
| Display outputs, Spout sender | engine_displays.cpp |
| Keyboard/mouse input, hotkey dispatch | engine_input.cpp |
| Texture loading, fallback logic | engine_textures.cpp |
| Message/supertext system | engine_messages.cpp |
| Text animation profiles | engine_textanim.cpp / engine_textanim_ui.cpp |
| UI window for feature X | engine_<x>_ui.cpp |
All methods are declared in engine.h but defined across 40+ .cpp files. When looking for a method's implementation, check the file matching the method's domain (above table), not just engine.cpp.
Standalone helpers used across multiple engine_*.cpp files live in engine_helpers.h as inline functions or extern declarations:
FormatSpriteSection/FormatSpriteSectionA— sprite INI section name formattingMakeRelativeSpritePath— convert absolute texture paths to relativeStripNamedGroups— regex named capture group strippingReadFileToString,StripComments,ConvertLLCto1310— text processingg_settingsDesc[],SettingDesc,SettingType— settings screen types
All ToolWindow controls use BS_OWNERDRAW. This means:
IsDlgButtonChecked()/CheckDlgButton()/BM_GETCHECKsilently return 0 — they don't work with owner-draw buttons- Use
ToolWindow::IsChecked(id)/SetChecked(id, bool)instead - Checkboxes are auto-toggled by the base class
WndProcbeforeDoCommandis called - Radio groups must be toggled manually in
DoCommand(the base class doesn't know group membership)
See docs/tool_window.md for the full ToolWindow reference.
HWND_NOTOPMOST has one T — never spell it HWND_NOTTOPMOST. The compiler won't catch this because it's a #define value.
All file paths use wchar_t / std::wstring. Never use char* for paths — Windows APIs return wide strings and non-ASCII characters in filenames will be silently corrupted.
Font atlas SRV slots must be allocated before m_srvSlotBaseline is set. ResetDynamicDescriptors() rewinds to baseline on resize — anything allocated after baseline gets reclaimed.
- Font atlases are permanent (allocated before baseline)
- Render targets (VS[0], VS[1], blur) are dynamic (allocated after baseline)
AllocateDX9Stuff()builds font atlases first, advances baseline, then creates render targetsResetBufferAndFonts()must rebuild font atlases afterCleanUpFonts()(they're destroyed)
- Use
std::atomicfor cross-thread flags (e.g.,m_bScreenshotRequested,m_bMirrorStylesDirty) - Render thread owns DX12 resources and all HUD text rendering (CTextManager)
- Message pump thread owns HWNDs
- IPC thread runs a hidden 1x1 window for
WM_COPYDATAfrom Milkwave Remote - Use the
RenderCommandqueue (EnqueueRenderCmd) for cross-thread communication - ToolWindows run on their own threads — don't access DX12 from ToolWindow code
4 shared SamplerState objects (s0–s3) cover all preset sampling needs. Textures use Texture2D t-registers (~128 limit). #define tex2D macro routes through _samp_lw. Special-mode samplers use text substitution in LoadShaderFromMemory.
s0= LINEAR + WRAPs1= LINEAR + CLAMPs2= POINT + CLAMPs3= POINT + WRAP- Blur uses
_samp_lc(CLAMP) via text substitution
Use m_bLoadingShadertoyMode (not m_bShadertoyMode) in LoadShaderFromMemory. The latter isn't set until after shader compilation — too late for shader text generation. m_bLoadingShadertoyMode is set in LoadPreset before the async thread starts.
MDropDX12 uses Named Pipe IPC (\\.\pipe\Milkwave_<PID>) for communication with Milkwave Remote, the MCP server, and other clients. The pipe server (pipe_server.cpp) runs on a dedicated thread with duplex message-mode, multi-instance support (each client gets its own handler thread).
Message format: Raw wide strings, newline-delimited. Two dispatch paths:
-
Signals — prefixed with
SIGNAL|, dispatched viaDispatchSignal()which posts aWM_APP + offsetmessage to the render window. Signal table is inpipe_server.cpp:Signal Description SIGNAL|NEXT_PRESETNext preset SIGNAL|PREV_PRESETPrevious preset SIGNAL|CAPTUREScreenshot SIGNAL|WATERMARKToggle watermark mode SIGNAL|FULLSCREENToggle fullscreen SIGNAL|BORDERLESS_FSToggle borderless fullscreen SIGNAL|STRETCHToggle stretch SIGNAL|MIRRORToggle mirror SIGNAL|MIRROR_WMToggle mirror watermark SIGNAL|SPRITE_MODEToggle sprite mode SIGNAL|MESSAGE_MODEToggle message mode SIGNAL|COVER_CHANGEDNotify cover art changed SIGNAL|SHOW_COVERShow cover art SIGNAL|SETVIDEODEVICE=NSet video device index SIGNAL|ENABLEVIDEOMIX=0|1Enable/disable video mix SIGNAL|ENABLESPOUTMIX=0|1Enable/disable Spout mix SIGNAL|SET_INPUTMIX_OPACITY=NSet input mix opacity SIGNAL|SET_INPUTMIX_ONTOP=0|1Set input mix on-top SIGNAL|SET_INPUTMIX_LUMAKEY=threshold|softnessSet luma key params To add a new signal: add an entry to
s_signalTable[]inpipe_server.cpp, define aWM_MW_*message inengine_helpers.h, handle it inApp.cpp's message pump. -
Commands — everything else is forwarded to
LaunchMessage()inengine_messages.cpp. Commands are matched viawcsncmpand include:Command Description PRESET=pathLoad preset by path OPACITY=0.0-1.0Set window opacity STATEQuery current state (returns JSON-like response) CAPTUREScreenshot to file SHUTDOWNClean shutdown SET_LOGLEVEL=0-4Set log level GET_LOGLEVELQuery log level CLEAR_LOGSDelete all log files SET_DIR=pathChange preset directory LOAD_LIST=pathLoad a preset list ENUM_LISTSList available preset lists TRACK|title|artist|albumSet track info MSG|textDisplay overlay message COL_HUE=0.0-1.0Set color hue COL_SATURATION=0.0-1.0Set saturation COL_BRIGHTNESS=-1.0-1.0Set brightness FFT_ATTACK=valueSet FFT attack FFT_DECAY=valueSet FFT decay SET_DEVICE_VOLUME=0.0-1.0Set Windows device volume GET_DEVICE_VOLUMEQuery device volume and mute state SET_DEVICE_MUTE=0|1Set device mute state TOGGLE_DEVICE_MUTEToggle device mute SHADER_IMPORT=pathImport shader from file GET_RENDER_DIAGGet render diagnostics GET_AUDIO_DIAGGet audio diagnostics GET_EEL_STATEGet expression evaluator state MOVE_TO_DISPLAY=NMove to display index SET_WINDOW=x,y,w,hSet window position/size To add a new command: add an
else if (wcsncmp(sMessage, L"MY_CMD", 6) == 0)block inLaunchMessage(). Send responses viag_pipeServer.Send(response).
The TCP server (tcp_server.cpp) provides network-based remote control for LAN clients. It handles only TCP-specific concerns (framing, authentication, connection management) and forwards all commands through the same IPC dispatch path as the Named Pipe.
- Port: 9270 (default, configurable via INI)
- Framing: 4-byte little-endian length prefix + UTF-8 payload
- Authentication: Clients must send
AUTH|<pin>|<deviceId>|<deviceName>first. Authorized devices are stored in INI[AuthorizedDevices]section. Clients must re-sendAUTHon every new TCP connection (including reconnects after app backgrounding). - Responses:
AUTH_OK,AUTH_PENDING,AUTH_FAIL|<reason>,AUTH_REQUIRED(sent once when an unauthenticated client sends a non-AUTH command — client should re-sendAUTH),PONG(forPING), or command-specific responses routed to the requesting client viag_respondingTcpClient - Reconnect: On
AUTH, the server evicts prior connections from the samedeviceIdand from the same client IP (single-phone assumption). Stale unauthenticated sockets from the same IP are dropped immediately on accept. - Limits: 16 concurrent clients, 15-second unauthenticated / 120-second authenticated inactivity timeout, 64KB receive buffer
- mDNS: Service advertised as
_milkwave._tcpfor automatic discovery
Command routing (App.cpp onMessage handler): TCP does NOT process commands itself — it forwards them to the existing IPC path:
SIGNAL|*messages →g_pipeServer.DispatchSignal()(same as Named Pipe)- All other messages →
PostMessage(WM_MW_IPC_MESSAGE)→LaunchMessage()(same as Named Pipe)
After authentication, clients send the exact same commands and signals as Named Pipe clients. TCP is a transport layer, not a command handler.
LAN discovery uses two mechanisms in parallel (mdns_advertiser.cpp):
-
mDNS (DNS-SD) — registers as
MDropDX12-<hostname>._milkwave._tcp.localvia Windows DNS-SD APIs (DnsServiceRegister). Loaded dynamically fromdnsapi.dllso the binary runs on older Windows builds where the APIs aren't available. TXT record carriesversion=1andpid=<process_id>. -
UDP broadcast beacon — reliable fallback that always works regardless of mDNS support. Sends
MDROP_BEACON|<name>|<tcpPort>|<pid>to255.255.255.255on port 9271 every 3 seconds. Clients listen on UDP 9271 to discover instances, then connect to the advertised TCP port.
Both start in App.cpp after the TCP server is running:
g_mdns.Register("MDropDX12-" + hostname, g_tcpServer.GetPort(), GetCurrentProcessId());Both are stopped on shutdown via g_mdns.Unregister() which joins the beacon thread and deregisters from DNS-SD.
Key files: mdns_advertiser.h (class declaration), mdns_advertiser.cpp (implementation), App.cpp (startup/shutdown calls).
Ports:
| Port | Protocol | Purpose |
|---|---|---|
| 9270 | TCP | Remote control (commands, auth) |
| 9271 | UDP | Broadcast beacon (discovery only) |
ToolWindows are standalone windows that run on their own threads with independent always-on-top, position persistence, and dark theme support. All controls use BS_OWNERDRAW (see docs/tool_window.md).
-
Declare the class in
tool_window.h:class MyFeatureWindow : public ToolWindow { public: MyFeatureWindow(Engine* pEngine); protected: TOOLWINDOW_META(L"My Feature", L"MyFeatureWndClass", L"MyFeature", IDC_MY_PIN, IDC_MY_FONT_PLUS, IDC_MY_FONT_MINUS, 400, 300) void DoBuildControls() override; LRESULT DoCommand(HWND hWnd, int id, int code, LPARAM lParam) override; // Optional overrides: // LRESULT DoHScroll(HWND hCtrl, int code, int pos) override; // LRESULT DoNotify(NMHDR* pNMHDR) override; // void DoDestroy() override; };
TOOLWINDOW_METAgenerates boilerplate: window title, class name, INI section, control IDs for pin/font buttons, default width/height. -
Implement in
engine_myfeature_ui.cpp:#include "tool_window.h" #include "engine.h" #include "engine_helpers.h" MyFeatureWindow::MyFeatureWindow(Engine* pEngine) : ToolWindow(pEngine, 400, 300) {} void MyFeatureWindow::DoBuildControls() { BuildBaseControls(); // creates pin, font+/- buttons int y = 40; CreateLabel(m_hWnd, L"My Label:", 10, y, 80, 20); CreateCheck(m_hWnd, IDC_MY_CHECK, L"Enable", 100, y, 100, 20); TrackControl(GetDlgItem(m_hWnd, IDC_MY_CHECK)); // register for dark theme } LRESULT MyFeatureWindow::DoCommand(HWND hWnd, int id, int code, LPARAM lParam) { switch (id) { case IDC_MY_CHECK: m_pEngine->m_bMyFeature = IsChecked(IDC_MY_CHECK); return 0; } return -1; // not handled }
-
Register in
engine.h:std::unique_ptr<MyFeatureWindow> m_myFeatureWindow; void OpenMyFeatureWindow() { OpenToolWindow(m_myFeatureWindow); } void CloseMyFeatureWindow() { CloseToolWindow(m_myFeatureWindow); }
-
Initialize in
engine.cppconstructor:m_myFeatureWindow = std::make_unique<MyFeatureWindow>(this); -
Add to vcxproj: Include
engine_myfeature_ui.cppin the project's ClCompile list.
Key rules:
- Use
IsChecked(id)/SetChecked(id, bool)— neverIsDlgButtonChecked() - Checkboxes auto-toggle before
DoCommand; radio groups must be toggled manually - Don't access DX12 resources from ToolWindow code (wrong thread)
- See
engine_colors_ui.cppfor a simple example,engine_midi_ui.cppfor a complex one
The ButtonBoard's right-click "Assign Action..." submenu is built by BuildActionSubMenu() in engine_board_ui.cpp. It auto-populates from the hotkey table, grouped by category (HKCAT_NAVIGATION, HKCAT_WINDOW, HKCAT_VISUAL, etc.).
Adding IPC signal actions (actions that aren't hotkeys):
In BuildActionSubMenu(), append items to the relevant category submenu after the hotkey loop, using IDs starting at 2000:
if (cat == HKCAT_WINDOW) {
AppendMenuW(hCatMenu, MF_SEPARATOR, 0, NULL);
AppendMenuW(hCatMenu, MF_STRING, 2000, L"Watermark");
itemCount++;
}Then handle the ID in ShowSlotContextMenu():
if (cmd == 2000) {
s.action = ButtonAction::ScriptCommand;
s.payload = L"SIGNAL|WATERMARK";
s.label = L"Watermark";
m_pPanel->Invalidate();
SaveBoard();
}Reordering hotkey-based items: Items appear in the order hotkeys are defined in InitHotkeyDefaults() (engine_hotkeys.cpp). Move the HK_DEF call to change the item's position within its category.
Reordering categories: The loop iterates HKCAT_NAVIGATION through HKCAT_MISC. To change the category order in the menu, modify the loop range or iterate a custom order array.
Categories are defined in hotkeys.h:
| Enum | Menu Name | Description |
|---|---|---|
HKCAT_NAVIGATION |
Navigation | Preset navigation, browser, save |
HKCAT_VISUAL |
Visual | Opacity, wave mode, zoom (auto-grouped by prefix) |
HKCAT_MEDIA |
Media | Track info, song display |
HKCAT_WINDOW |
Window | Fullscreen, always-on-top, FPS, borderless |
HKCAT_TOOLS |
Tools | Open tool windows |
HKCAT_SHADER |
Shader | Inject effects, quality |
HKCAT_MISC |
Misc | Miscellaneous actions |
HKCAT_SCRIPT |
Script | User-defined script commands |
HKCAT_LAUNCH |
Launch | Launch external applications |
The Visual category is special — BuildActionSubMenu() auto-groups items by their first word (e.g., all "Opacity ..." actions become an "Opacity" submenu).
Not all actions need a hotkey binding. For actions triggered only from UI (ButtonBoard, context menus, IPC), use ButtonAction::ScriptCommand with a pipe command payload (e.g., SIGNAL|WATERMARK or OPACITY=0.5). The button's ExecuteSlot() dispatches it via PostIPCMessage() which routes through LaunchMessage().
Two render targets (VS[0] and VS[1]) ping-pong each frame:
- Warp pass: Reads VS[0] -> applies warp mesh distortion -> writes to VS[1]
- Shape/wave injection: Custom shapes and waves drawn directly into VS[1]
- Comp pass: Reads VS[1] -> applies comp mesh + comp shader -> writes to backbuffer
BINDING_BLOCK_SIZE = 32descriptors per texture setPASSES_PER_FRAME = 4(warp + bufferA + bufferB + comp)- Static samplers: s0=LINEAR+WRAP, s1=LINEAR+CLAMP, s2=POINT+CLAMP, s3=POINT+WRAP
- No projection matrix: DX12 vertex shaders output directly to clip space
- No half-texel offset: DX12 pixel centers are at integer+0.5 (DX9 at integers)
- No Y-flip compensation: DX9 used
OrthoLH(2,-2)which negated Y; DX12 passthrough VS doesn't flip - Post-processing via shader:
RenderInjectEffect()handles brighten/darken/solarize/invert via pixel shader (not blend states)