HKLM and Window Scaler wrapper
Wrapper for proxying and redirecting HKLM registry queries to local sqlite storage, and upscaling window mode renderers (mainly via dgVoodoo)
twinshim.exe: GUI launcher/injector (no console window when launched from GUI tools).twinshim_cli.exe: console launcher/injector (recommended for--debugfrom cmd/PowerShell).twinshim_shim.dll: hooked registry + scaling layer.hklmreg.exe: CLI for local DB add/delete/export/import/dump.
Default DB name: HKLM.sqlite (in the current directory).
Use the workspace file that matches your task:
TwinShim-native.code-workspace- macOS/Linux native development and fast unit-test iteration.
TwinShim-windows-mingw.code-workspace- Win32-target IntelliSense/cross-build on macOS/Linux. Cross-build doesn't build dgvoodoo features.
Practical flow:
- Work mostly in the native workspace.
- Switch to the MinGW workspace when editing Win32 shim/hooking code.
- Validate runtime behavior natively on Windows before release.
This repo uses CMake presets and vcpkg (sqlite3 comes from vcpkg.json).
Build only the local hklmreg CLI on macOS (no wrapper/shim/injector targets):
cmake --preset macos-hklmreg-release
cmake --build --preset macos-hklmreg-releaseHelper script:
./scripts/build-macos-hklmreg.shExpected artifact:
build/macos-hklmreg-release/hklmreg
Install host tools:
- macOS:
brew install cmake ninja git pkg-config mingw-w64 - Ubuntu/Debian:
sudo apt-get update && sudo apt-get install -y build-essential cmake ninja-build git curl zip unzip tar pkg-config mingw-w64
Set up vcpkg:
git clone https://github.com/microsoft/vcpkg.git ~/vcpkg
~/vcpkg/bootstrap-vcpkg.sh
export VCPKG_ROOT=~/vcpkgConfigure/build:
cmake --preset windows-x86-mingw-release
cmake --build --preset windows-x86-mingw-releaseStaging output:
cmake --preset windows-x86-mingw-release-stage
cmake --build --preset windows-x86-mingw-release-stage
cmake --build --preset windows-x86-mingw-release-stage-installExpected runtime artifacts in stage/bin:
twinshim.exetwinshim_cli.exetwinshim_shim.dllhklmreg.exe
Helper scripts:
scripts\cmake-msvc-x86.cmd --preset windows-x86-msvc-release
scripts\cmake-msvc-x86.cmd --build --preset windows-x86-msvc-releaseStaging helper:
scripts\build-windows-msvc-x86.cmdIf third_party/dgvoodoo_addon_sdk is present (with Inc/Addon headers and Lib/x86/dgVoodooAddon.lib), the staging helper auto-enables the dgVoodoo AddOn build and stages SampleAddon.dll into stage/bin.
cmake --preset native-tests
cmake --build --preset native-tests
ctest --preset native-testsscripts\cmake-msvc-x86.cmd --preset native-tests-windows
scripts\cmake-msvc-x86.cmd --build --preset native-tests-windows
ctest --preset native-tests-windowsOne-shot helper:
scripts\test-windows-msvc-x86.cmdOverride the base directory by setting TWINSHIM_TEST_TMP_BASE (or the legacy HKLM_WRAPPER_TEST_TMP_BASE).
This suite includes a Windows-only workflow test that launches twinshim_cli.exe --debug all around a probe process and verifies both hook debug trace output and persisted SQLite-backed registry data.
Usage:
twinshim_cli.exe [--db <path>] [--debug <api1,api2,...|all>] [--readthrough] [--scale <1.1-100>] [--scale-method <point|bilinear|bicubic|cr|catmull-rom|lanczos|lanczos3>] <target_exe> [target arguments...]
Use twinshim.exe for normal GUI-driven launches.
Use twinshim_cli.exe when launching from cmd/PowerShell, especially with --debug, so the shell blocks until the wrapped process finishes.
Examples:
twinshim.exe C:\Path\To\TargetApp.exe
twinshim.exe --db .\HKLM.sqlite C:\Path\To\TargetApp.exe
twinshim.exe --readthrough C:\Path\To\TargetApp.exe
twinshim_cli.exe --debug RegOpenKey,RegQueryValue C:\Path\To\TargetApp.exe
twinshim_cli.exe --debug all C:\Path\To\TargetApp.exe
Registry virtualization scope:
- Only
HKEY_LOCAL_MACHINEpaths are virtualized. Other root hives pass through to the real registry unchanged. - By default,
HKLMreads and writes stay inside the local SQLite-backed store. --readthroughchanges reads to overlay mode: consult the local store first, then fall through to the realHKLMkey/value data when the local store misses. Local tombstones still hide the real registry.
This repo has two scaling approaches:
- Shim hooks (default): best-effort surface scaling for native D3D9 and system DirectDraw paths.
- dgVoodoo AddOn (recommended for dgVoodoo): intended path when running under dgVoodoo, where the wrapper may render through non-D3D9 backends (e.g. D3D12) and backbuffer/swapchain hooking is fragile.
When --scale is active, the injected shim also installs a small mouse-coordinate mapping layer so that client-space mouse positions and mouse messages are translated back into the app's pre-scale coordinate space. This avoids the common "cursor moves too fast / hits the edge early" symptom when the window is physically resized but the game still clamps input to its native render size.
Debugging:
- Set
TWINSHIM_MOUSE_DEBUG=1(or legacyHKLM_WRAPPER_MOUSE_DEBUG=1) to emit limited trace output about scale mapping registration and coordinate transforms. - The shim remaps
GetCursorPoswhile the cursor is inside the scaled window client area (common for games that poll cursor position instead of usingWM_MOUSEMOVE).
https://github.com/dege-diosg/dgVoodoo2/releases
Example: dgVoodooAPI_287.zip
- Unpack the dgVoodoo SDK under:
third_party/dgvoodoo_addon_sdk/(expected layout includesInc/AddonandLib/x86)
- Configure with:
-DHKLM_WRAPPER_ENABLE_DGVOODOO_ADDON=ON
(automatically done if using build script.)
If your SDK is in a different location or you need a different arch library path, set:
-DDGVOODOO_ADDON_SDK_DIR=<path>-DDGVOODOO_ADDON_LIB_DIR=<path-to-Lib/x86>
The AddOn implementation scaffold lives in src/dgvoodoo_addon/addon_main.cpp.
dgVoodoo currently loads an add-on DLL only by the fixed name SampleAddon.dll, typically from the same directory as the dgVoodoo graphics DLLs. The build produces SampleAddon.dll when the add-on target is enabled.
hklmreg add HKLM\Software\MyApp /v Test /t REG_SZ /d hello /f
hklmreg delete HKLM\Software\MyApp /v Test /f
hklmreg export out.reg HKLM\Software\MyApp
hklmreg dump HKLM\Software\MyApp > out.reg
hklmreg import out.reg
(Optional override)
hklmreg --db .\SomeOther.sqlite dump HKLM\Software\MyApp
The local store uses two tables:
Tracks key-level existence/tombstones.
CREATE TABLE keys(
key_path TEXT PRIMARY KEY,
is_deleted INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
);key_path: canonical key path (for exampleHKLM\\Software\\MyApp).is_deleted: soft-delete flag (0active,1deleted/tombstone).updated_at: Unix epoch seconds.
Stores registry values and value-level tombstones.
CREATE TABLE values_tbl(
key_path TEXT NOT NULL,
value_name TEXT NOT NULL,
type INTEGER NOT NULL,
data BLOB,
is_deleted INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL,
PRIMARY KEY(key_path, value_name)
);
CREATE INDEX idx_values_key ON values_tbl(key_path);value_name: empty string means the default value ((Default)).type: Win32 registry type value (common:REG_SZ=1,REG_BINARY=3,REG_DWORD=4,REG_QWORD=11).data: raw bytes for that type:REG_SZ: UTF-16LE bytes including terminating NUL.REG_DWORD: 4-byte little-endian integer.REG_QWORD: 8-byte little-endian integer.
is_deleted/updated_at: same semantics askeys.
Deletes are modeled as tombstones (is_deleted=1) rather than hard row removal.
Registry key paths and value names are case-insensitive in the Windows registry, and TwinShim follows the same convention. The store layer uses COLLATE NOCASE for all lookups, so reads work regardless of the casing you use.
However, the underlying SQLite PRIMARY KEY columns use case-sensitive comparison by default. The store normalizes key-path casing internally (new values inherit the key_path spelling established by the first PutKey call for that logical key), but if you write directly to the database you must take care to use consistent casing for key_path across both tables.
Inconsistent casing (e.g. HKLM\Software\App in one row and HKLM\SOFTWARE\App in another) will not break reads, but may produce unexpected ordering in .reg exports.
Best practice when writing SQL directly: query the existing key_path spelling first, then use that exact form for all subsequent operations on the same key:
-- Find the canonical spelling already in the keys table:
SELECT key_path FROM keys
WHERE key_path = 'HKLM\Software\MyApp' COLLATE NOCASE;
-- Use that exact spelling for all INSERT/UPDATE operations on values_tbl.Examples below use the sqlite3 CLI directly.
INSERT INTO keys(key_path, is_deleted, updated_at)
VALUES ('HKLM\\Software\\MyApp', 0, strftime('%s','now'))
ON CONFLICT(key_path) DO UPDATE SET
is_deleted=0,
updated_at=excluded.updated_at;
INSERT INTO values_tbl(key_path, value_name, type, data, is_deleted, updated_at)
VALUES (
'HKLM\\Software\\MyApp',
'Test',
1,
X'680065006C006C006F000000',
0,
strftime('%s','now')
)
ON CONFLICT(key_path, value_name) DO UPDATE SET
type=excluded.type,
data=excluded.data,
is_deleted=0,
updated_at=excluded.updated_at;SELECT
key_path,
value_name,
type,
hex(data) AS data_hex,
datetime(updated_at, 'unixepoch') AS updated_utc
FROM values_tbl
WHERE key_path = 'HKLM\\Software\\MyApp'
AND is_deleted = 0
ORDER BY value_name;123 decimal = 0x7B, stored little-endian as X'7B000000'.
INSERT INTO values_tbl(key_path, value_name, type, data, is_deleted, updated_at)
VALUES ('HKLM\\Software\\MyApp', 'Flags', 4, X'7B000000', 0, strftime('%s','now'))
ON CONFLICT(key_path, value_name) DO UPDATE SET
type=excluded.type,
data=excluded.data,
is_deleted=0,
updated_at=excluded.updated_at;
SELECT
value_name,
hex(data) AS dword_le_hex
FROM values_tbl
WHERE key_path='HKLM\\Software\\MyApp'
AND value_name='Flags'
AND type=4
AND is_deleted=0;- Wrapper and shim DLL bitness must match target process bitness.
- For native MSVC x86 builds, use
scripts\cmake-msvc-x86.cmd(orscripts\build-windows-msvc-x86.cmd) so the Visual Studio toolchain is initialized with-arch=x86. Running CMake directly from an x64 dev shell can produce x64 binaries, which will fail to inject into x86 targets. - Native MSVC builds default to static runtime (
/MT) viaHKLM_WRAPPER_MSVC_STATIC_RUNTIME=ONto avoid conflicts with app-localMSVCP140.dll/vcruntimein injected target processes. - Hook mode is runtime-selectable with
TWINSHIM_HOOK_MODE(or legacyHKLM_WRAPPER_HOOK_MODE):- default (unset) /
all/full/extended: enable full ANSI + wide hook set core/minimal/wide/unicode: wide-only core + legacy/key-info/enum hooksoff/none/disabled: inject shim but skip hook installation (diagnostics/fallback)
- default (unset) /
- Optional windowed scaling (Direct3D9 and some DirectDraw paths) is controlled by target command-line options:
--scale <1.1-100>: scaling factor (e.g.--scale 2for 2x)--scale-method <point|bilinear|bicubic>: sampling method (default:point)- Use
twinshim_cli.exe --debug (-|all) ...to see[shim:d3d9]/[shim:ddraw]probe output.
- macOS/Linux cross-build validates compile/link only for non-dgVoodoo functionality; runtime injection/hooking must be validated natively on Windows.
- Hooks a small set of APIs (both
*Wand*Awhere applicable):- Open/create keys:
RegOpenKey(Ex),RegCreateKey(Ex) - Close key handles:
RegCloseKey - Read/write values:
RegQueryValue(Ex),RegSetValue(Ex),RegSetKeyValue - Delete keys/values:
RegDeleteValue,RegDeleteKey(andRegDeleteKeyExif present) - Enumerate/query metadata:
RegEnumValue,RegEnumKey(Ex),RegQueryInfoKey
- Open/create keys:
- No ACL/security descriptor handling.
- In
--debugmode, tracing stays active for the full target process lifetime (wrapper waits for target exit).