mirror of
https://github.com/gwdevhub/gMod.git
synced 2026-07-16 15:39:31 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b809519f8 | |||
| c57a527b68 | |||
| f420bbd50a | |||
| e762e1b716 | |||
| 13abd4e546 | |||
| 92c6b115f3 | |||
| 2b87e0e53a | |||
| b04b4583f4 | |||
| 18d650a422 | |||
| 55e36db265 | |||
| 379878bd6d | |||
| ac39241ff9 |
@@ -0,0 +1,38 @@
|
||||
# Build output and artifacts
|
||||
build/
|
||||
bin/
|
||||
Debug/
|
||||
Release/
|
||||
MinSizeRel/
|
||||
RelWithDebInfo/
|
||||
win32/
|
||||
install/
|
||||
|
||||
# CMake generated directories
|
||||
*.dir/
|
||||
CMakeFiles/
|
||||
_deps/
|
||||
|
||||
# vcpkg vendored dependencies
|
||||
vcpkg_installed/
|
||||
|
||||
# Visual Studio files
|
||||
*.vcxproj
|
||||
*.vcxproj.filters
|
||||
*.vcxproj.user
|
||||
*.sln
|
||||
*.slnx
|
||||
*.sdf
|
||||
*.suo
|
||||
*.user
|
||||
*.aps
|
||||
|
||||
# CMake cache/generated
|
||||
CMakeCache.txt
|
||||
cmake_install.cmake
|
||||
ALL_BUILD*
|
||||
ZERO_CHECK*
|
||||
INSTALL*
|
||||
|
||||
# Version resource (generated)
|
||||
version.rc
|
||||
@@ -0,0 +1,2 @@
|
||||
# Shell scripts must stay LF or bash on the CI runner chokes on \r.
|
||||
*.sh text eol=lf
|
||||
@@ -0,0 +1,28 @@
|
||||
# Pre-configure SimplySign Desktop for unattended use:
|
||||
# - auto-show the login dialog on launch (so the TOTP keystrokes have a target)
|
||||
# - cache the smart-card PIN in the CSP, so signtool signs without a per-call
|
||||
# PIN prompt, and forget it again when the session disconnects.
|
||||
# Mirrors the settings used by the blinkdisk / docuscope CI signing setups.
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$RegistryPath = "HKCU:\Software\Certum\SimplySign"
|
||||
|
||||
$settings = [ordered]@{
|
||||
ShowLoginDialogOnStart = 1
|
||||
ShowLoginDialogOnAppRequest = 1
|
||||
RememberLastUserName = 0
|
||||
Autostart = 0
|
||||
UnregisterCertificatesOnDisconnect = 0
|
||||
RememberPINinCSP = 1
|
||||
ForgetPINinCSPonDisconnect = 1
|
||||
LangID = 9
|
||||
}
|
||||
|
||||
Write-Host "=== Configuring SimplySign Desktop registry ==="
|
||||
New-Item -Path $RegistryPath -Force | Out-Null
|
||||
foreach ($name in $settings.Keys) {
|
||||
Set-ItemProperty -Path $RegistryPath -Name $name -Value $settings[$name] -Type DWord
|
||||
Write-Host " $name = $($settings[$name])"
|
||||
}
|
||||
Write-Host "Done."
|
||||
@@ -0,0 +1,245 @@
|
||||
# Authenticate SimplySign Desktop non-interactively.
|
||||
#
|
||||
# Certum's SimplySign cloud has no headless login: the certificate only reaches
|
||||
# the Windows store after the GUI client authenticates. So we generate the
|
||||
# current TOTP from the otpauth:// secret (CERTUM_OTP_URI) and paste the
|
||||
# credentials into the login dialog. This needs an interactive desktop session,
|
||||
# which GitHub-hosted Windows runners provide.
|
||||
#
|
||||
# Based on https://www.devas.life/how-to-automate-signing-your-windows-app-with-certum/
|
||||
# and the refinements in blinkdisk's connect-simplysign.ps1.
|
||||
|
||||
param(
|
||||
[string]$OtpUri = $env:CERTUM_OTP_URI,
|
||||
[string]$UserId = $env:CERTUM_USERID,
|
||||
[string]$ExePath = $env:CERTUM_EXE_PATH
|
||||
)
|
||||
|
||||
if (-not $OtpUri) { Write-Host "ERROR: CERTUM_OTP_URI not provided"; exit 1 }
|
||||
if (-not $UserId) { Write-Host "ERROR: CERTUM_USERID not provided"; exit 1 }
|
||||
if (-not $ExePath) {
|
||||
$ExePath = "C:\Program Files\Certum\SimplySign Desktop\SimplySignDesktop.exe"
|
||||
}
|
||||
|
||||
Write-Host "=== SimplySign Desktop TOTP authentication ==="
|
||||
if (-not (Test-Path $ExePath)) {
|
||||
Write-Host "ERROR: SimplySign Desktop not found at $ExePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Parse the otpauth:// URI (works on PowerShell 5.1 and 7+) ---------------
|
||||
$uri = [Uri]$OtpUri
|
||||
try {
|
||||
$q = [System.Web.HttpUtility]::ParseQueryString($uri.Query)
|
||||
} catch {
|
||||
$q = @{}
|
||||
foreach ($part in $uri.Query.TrimStart('?') -split '&') {
|
||||
$kv = $part -split '=', 2
|
||||
if ($kv.Count -eq 2) { $q[$kv[0]] = [Uri]::UnescapeDataString($kv[1]) }
|
||||
}
|
||||
}
|
||||
|
||||
$Base32 = $q['secret']
|
||||
$Digits = if ($q['digits']) { [int]$q['digits'] } else { 6 }
|
||||
$Period = if ($q['period']) { [int]$q['period'] } else { 30 }
|
||||
$Algorithm = if ($q['algorithm']) { $q['algorithm'].ToUpper() } else { 'SHA256' }
|
||||
|
||||
if (-not $Base32) { Write-Host "ERROR: otpauth URI has no 'secret'"; exit 1 }
|
||||
if ($Algorithm -notin @('SHA1', 'SHA256', 'SHA512')) {
|
||||
Write-Host "ERROR: unsupported TOTP algorithm: $Algorithm"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- TOTP generator (RFC 6238), inline C# so there are no dependencies --------
|
||||
Add-Type -Language CSharp @"
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
public static class Totp
|
||||
{
|
||||
private const string B32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
|
||||
private static byte[] Base32Decode(string s)
|
||||
{
|
||||
s = s.TrimEnd('=').ToUpperInvariant();
|
||||
byte[] bytes = new byte[s.Length * 5 / 8];
|
||||
int bitBuffer = 0, bitsLeft = 0, idx = 0;
|
||||
foreach (char c in s)
|
||||
{
|
||||
int val = B32.IndexOf(c);
|
||||
if (val < 0) throw new ArgumentException("Invalid Base32 char: " + c);
|
||||
bitBuffer = (bitBuffer << 5) | val;
|
||||
bitsLeft += 5;
|
||||
if (bitsLeft >= 8) { bytes[idx++] = (byte)(bitBuffer >> (bitsLeft - 8)); bitsLeft -= 8; }
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static HMAC Hmac(string algorithm, byte[] key)
|
||||
{
|
||||
switch (algorithm.ToUpper())
|
||||
{
|
||||
case "SHA1": return new HMACSHA1(key);
|
||||
case "SHA256": return new HMACSHA256(key);
|
||||
case "SHA512": return new HMACSHA512(key);
|
||||
default: throw new ArgumentException("Unsupported algorithm: " + algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Now(string secret, int digits, int period, string algorithm)
|
||||
{
|
||||
byte[] key = Base32Decode(secret);
|
||||
long counter = DateTimeOffset.UtcNow.ToUnixTimeSeconds() / period;
|
||||
byte[] cnt = BitConverter.GetBytes(counter);
|
||||
if (BitConverter.IsLittleEndian) Array.Reverse(cnt);
|
||||
|
||||
byte[] hash;
|
||||
using (var h = Hmac(algorithm, key)) { hash = h.ComputeHash(cnt); }
|
||||
|
||||
int offset = hash[hash.Length - 1] & 0x0F;
|
||||
int binary =
|
||||
((hash[offset] & 0x7F) << 24) |
|
||||
((hash[offset + 1] & 0xFF) << 16) |
|
||||
((hash[offset + 2] & 0xFF) << 8) |
|
||||
(hash[offset + 3] & 0xFF);
|
||||
int otp = binary % (int)Math.Pow(10, digits);
|
||||
return otp.ToString(new string('0', digits));
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
function Find-UiByName($el, $walker, $name) {
|
||||
$c = $walker.GetFirstChild($el)
|
||||
while ($c) {
|
||||
if ($c.Current.Name -eq $name) { return $c }
|
||||
$r = Find-UiByName $c $walker $name
|
||||
if ($r) { return $r }
|
||||
$c = $walker.GetNextSibling($c)
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# An outdated SimplySign build pops a modal "New version found - download?" box
|
||||
# over the login form, which swallows the credential keystrokes. Decline it by
|
||||
# clicking "No" (Invoke / legacy default action / click the element's point).
|
||||
function Dismiss-UpdatePrompt {
|
||||
try { Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes -ErrorAction Stop } catch { return $false }
|
||||
if (-not ('Win32Mouse' -as [type])) {
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class Win32Mouse {
|
||||
[DllImport("user32.dll")] static extern bool SetCursorPos(int x, int y);
|
||||
[DllImport("user32.dll")] static extern void mouse_event(uint f, uint x, uint y, uint d, int e);
|
||||
public static void Click(int x, int y){ SetCursorPos(x,y); mouse_event(0x02,0,0,0,0); mouse_event(0x04,0,0,0,0); }
|
||||
}
|
||||
"@
|
||||
}
|
||||
$procIds = @((Get-Process -Name '*SimplySign*' -ErrorAction SilentlyContinue).Id)
|
||||
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
|
||||
$top = $walker.GetFirstChild([System.Windows.Automation.AutomationElement]::RootElement)
|
||||
while ($top) {
|
||||
if ($procIds -contains $top.Current.ProcessId) {
|
||||
$no = Find-UiByName $top $walker "No"
|
||||
if ($no) {
|
||||
Write-Host "Update prompt detected; declining (clicking 'No')."
|
||||
try { ($no.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern)).Invoke(); return $true } catch {}
|
||||
try { ($no.GetCurrentPattern([System.Windows.Automation.LegacyIAccessiblePattern]::Pattern)).DoDefaultAction(); return $true } catch {}
|
||||
try { $pt = $no.GetClickablePoint(); [Win32Mouse]::Click([int]$pt.X, [int]$pt.Y); return $true } catch { Write-Host "Click 'No' failed: $($_.Exception.Message)" }
|
||||
}
|
||||
}
|
||||
$top = $walker.GetNextSibling($top)
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Start from a clean slate so a stale window/session can't swallow the keystrokes.
|
||||
if ($existing = Get-Process -Name "SimplySignDesktop" -ErrorAction Ignore) {
|
||||
Write-Host "Killing existing SimplySignDesktop process..."
|
||||
$existing | Stop-Process -Force
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
|
||||
Write-Host "Launching SimplySign Desktop..."
|
||||
$proc = Start-Process -FilePath $ExePath -PassThru
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
$wshell = New-Object -ComObject WScript.Shell
|
||||
|
||||
Write-Host "Focusing the login window..."
|
||||
$focused = $wshell.AppActivate($proc.Id)
|
||||
if (-not $focused) { $focused = $wshell.AppActivate('SimplySign Desktop') }
|
||||
for ($i = 0; (-not $focused) -and ($i -lt 10); $i++) {
|
||||
Start-Sleep -Milliseconds 500
|
||||
$focused = $wshell.AppActivate($proc.Id) -or $wshell.AppActivate('SimplySign Desktop')
|
||||
}
|
||||
if (-not $focused) {
|
||||
Write-Host "ERROR: could not bring SimplySign Desktop to the foreground"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Decline the "new version available" modal if it's covering the login form.
|
||||
if (Dismiss-UpdatePrompt) { Start-Sleep -Milliseconds 800 }
|
||||
|
||||
# Re-assert focus: dismissing the dialog can move the foreground window, and
|
||||
# keystrokes sent to the wrong window are silently dropped.
|
||||
$wshell.AppActivate($proc.Id) | Out-Null
|
||||
$wshell.AppActivate('SimplySign Desktop') | Out-Null
|
||||
Start-Sleep -Milliseconds 400
|
||||
|
||||
# Paste rather than type: SendKeys mangles characters like + ^ % ( ) and can
|
||||
# drop characters; pasting delivers the exact string. Falls back to typing if
|
||||
# the clipboard is unavailable.
|
||||
function Set-Field([string]$text) {
|
||||
try {
|
||||
Set-Clipboard -Value $text -ErrorAction Stop
|
||||
Start-Sleep -Milliseconds 150
|
||||
$wshell.SendKeys("^v")
|
||||
} catch {
|
||||
Write-Host "Clipboard unavailable ($($_.Exception.Message)); typing instead."
|
||||
$wshell.SendKeys($text)
|
||||
}
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
|
||||
Write-Host "Injecting credentials..."
|
||||
Set-Field $UserId
|
||||
$wshell.SendKeys("{TAB}")
|
||||
Start-Sleep -Milliseconds 200
|
||||
|
||||
# Generate the code right before sending it so it can't expire while we focus.
|
||||
$otp = [Totp]::Now($Base32, $Digits, $Period, $Algorithm)
|
||||
Set-Field $otp
|
||||
Start-Sleep -Milliseconds 200
|
||||
$wshell.SendKeys("{ENTER}")
|
||||
|
||||
# Don't leave the OTP / username sitting on the clipboard afterwards.
|
||||
try { Set-Clipboard -Value " " -ErrorAction Stop } catch {}
|
||||
|
||||
Write-Host "Waiting for authentication to settle..."
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
if (-not (Get-Process -Id $proc.Id -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "ERROR: SimplySign Desktop exited - authentication failed."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Confirm the login actually mounted the signing certificate into the store.
|
||||
$thumb = if ($env:CERTUM_CERT_SHA1) { ($env:CERTUM_CERT_SHA1 -replace '\s', '').ToUpperInvariant() } else { $null }
|
||||
Write-Host "Verifying the signing certificate reached the store..."
|
||||
$deadline = (Get-Date).AddSeconds(60)
|
||||
$found = $false
|
||||
do {
|
||||
$certs = Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction SilentlyContinue
|
||||
if ($thumb) { $found = [bool]($certs | Where-Object { $_.Thumbprint -eq $thumb }) }
|
||||
else { $found = [bool]($certs | Where-Object { $_.Subject -like '*Certum*' -or $_.Issuer -like '*Certum*' }) }
|
||||
if ($found) { break }
|
||||
Start-Sleep -Seconds 3
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
|
||||
if ($found) {
|
||||
Write-Host "=== Authentication complete: signing certificate is available. ==="
|
||||
} else {
|
||||
Write-Host "ERROR: signing certificate did not appear after authentication."
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# Install Certum SimplySign Desktop on the (ephemeral) Windows runner.
|
||||
# SimplySign Desktop is what mounts the cloud signing certificate as a virtual
|
||||
# smart card into the Windows certificate store; signtool then selects it by
|
||||
# thumbprint. The version + checksum are pinned for reproducibility.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="9.3.4.72"
|
||||
URL="https://files.certum.eu/software/SimplySignDesktop/Windows/${VERSION}/SimplySignDesktop-${VERSION}-64-bit-en.msi"
|
||||
EXPECTED_SHA256="bd51ebbaaac20fc7d59ab7103b5ed532b7800586df4e31f6999d03a394f9c515"
|
||||
INSTALLER="SimplySignDesktop.msi"
|
||||
INSTALL_DIR="/c/Program Files/Certum/SimplySign Desktop"
|
||||
|
||||
echo "=== Installing Certum SimplySign Desktop ${VERSION} ==="
|
||||
|
||||
# Idempotent: a cached/pre-provisioned image may already have it.
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
echo "Already installed at: $INSTALL_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Downloading installer..."
|
||||
curl -L "$URL" -o "$INSTALLER" --fail --max-time 600
|
||||
|
||||
ACTUAL_SHA256=$(sha256sum "$INSTALLER" | awk '{print $1}')
|
||||
if [ "$EXPECTED_SHA256" != "$ACTUAL_SHA256" ]; then
|
||||
echo "ERROR: checksum mismatch"
|
||||
echo " expected: $EXPECTED_SHA256"
|
||||
echo " actual: $ACTUAL_SHA256"
|
||||
exit 1
|
||||
fi
|
||||
echo "Checksum verified."
|
||||
|
||||
echo "Running msiexec (silent)..."
|
||||
# Start-Process inherits this CWD, so the relative installer path resolves here.
|
||||
powershell -Command "Start-Process msiexec.exe -ArgumentList '/i','${INSTALLER}','/quiet','/norestart','/l*v','install.log','ALLUSERS=1','REBOOT=ReallySuppress' -Wait -NoNewWindow"
|
||||
|
||||
if [ ! -d "$INSTALL_DIR" ]; then
|
||||
echo "ERROR: installation directory not found after install"
|
||||
echo "Last 20 lines of install.log:"
|
||||
tail -20 install.log 2>/dev/null || echo "(no install.log)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "SimplySign Desktop installed."
|
||||
@@ -0,0 +1,66 @@
|
||||
# Sign the given files with the Certum cloud certificate (selected by SHA1
|
||||
# thumbprint from the store) and verify each signature. Assumes SimplySign
|
||||
# Desktop has already authenticated (connect-simplysign.ps1).
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Files,
|
||||
[string]$CertSha1 = $env:CERTUM_CERT_SHA1,
|
||||
[string]$TimestampUrl = "http://time.certum.pl"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not $CertSha1) { throw "CERTUM_CERT_SHA1 not provided." }
|
||||
$thumb = ($CertSha1 -replace '\s', '').ToUpperInvariant()
|
||||
|
||||
# Resolve the newest signtool.exe from the installed Windows SDK(s).
|
||||
$kitsBin = "C:\Program Files (x86)\Windows Kits\10\bin"
|
||||
$signtool = Get-ChildItem -Path $kitsBin -Recurse -Filter signtool.exe -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -like '*\x64\*' } |
|
||||
Sort-Object { [version]$_.Directory.Parent.Name } |
|
||||
Select-Object -Last 1 -ExpandProperty FullName
|
||||
if (-not $signtool) { throw "signtool.exe not found under $kitsBin" }
|
||||
Write-Host "Using signtool: $signtool"
|
||||
|
||||
# The SimplySign virtual smart card registers in the store a moment after the
|
||||
# desktop client authenticates; poll for our certificate before signing.
|
||||
Write-Host "Waiting for certificate $thumb to appear in the store..."
|
||||
$deadline = (Get-Date).AddSeconds(120)
|
||||
$cert = $null
|
||||
$elapsed = 0
|
||||
do {
|
||||
$cert = Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Thumbprint -eq $thumb } | Select-Object -First 1
|
||||
if ($cert) { break }
|
||||
Start-Sleep -Seconds 3
|
||||
$elapsed += 3
|
||||
Write-Host " ...still waiting (${elapsed}s)"
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
|
||||
if (-not $cert) {
|
||||
Write-Host "Certificate not found. CurrentUser\My + LocalMachine\My contents:"
|
||||
Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction SilentlyContinue |
|
||||
Select-Object Thumbprint, Subject, HasPrivateKey | Format-Table -AutoSize | Out-String | Write-Host
|
||||
Write-Host "SimplySign processes:"
|
||||
Get-Process -Name '*SimplySign*' -ErrorAction SilentlyContinue |
|
||||
Select-Object Name, Id, Responding, MainWindowTitle | Format-Table -AutoSize | Out-String | Write-Host
|
||||
throw "Certificate $thumb not found in the store - SimplySign authentication likely failed (see dump above)."
|
||||
}
|
||||
Write-Host "Found signing certificate: $($cert.Subject)"
|
||||
|
||||
$failed = @()
|
||||
foreach ($file in $Files) {
|
||||
if (-not (Test-Path $file)) { throw "File to sign not found: $file" }
|
||||
Write-Host "=== Signing $file ==="
|
||||
& $signtool sign /sha1 $thumb /fd sha256 /td sha256 /tr $TimestampUrl /v $file
|
||||
if ($LASTEXITCODE -ne 0) { $failed += $file; continue }
|
||||
& $signtool verify /pa /v $file
|
||||
if ($LASTEXITCODE -ne 0) { $failed += $file }
|
||||
}
|
||||
|
||||
if ($failed.Count -gt 0) {
|
||||
throw "Signing/verification failed for: $($failed -join ', ')"
|
||||
}
|
||||
Write-Host "All binaries signed and verified."
|
||||
@@ -16,9 +16,15 @@ jobs:
|
||||
|
||||
runs-on: windows-2025-vs2026
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
Configuration: Release
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }}
|
||||
CERTUM_USERID: ${{ secrets.CERTUM_USERID }}
|
||||
CERTUM_CERT_SHA1: ${{ secrets.CERTUM_CERT_SHA1 }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -46,7 +52,28 @@ jobs:
|
||||
|
||||
- name: Build binaries
|
||||
run: cmake --build build --config Release
|
||||
|
||||
|
||||
# Signing only runs when the Certum secrets are present (i.e. on the real
|
||||
# repo, not forks); without them the build still produces unsigned binaries.
|
||||
- name: Set up Certum SimplySign
|
||||
if: env.CERTUM_OTP_URI != ''
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x ./.github/scripts/install-simplysign.sh
|
||||
./.github/scripts/install-simplysign.sh
|
||||
powershell -ExecutionPolicy Bypass -File "./.github/scripts/configure-simplysign.ps1"
|
||||
|
||||
- name: Authenticate Certum SimplySign
|
||||
if: env.CERTUM_OTP_URI != ''
|
||||
shell: bash
|
||||
run: powershell -ExecutionPolicy Bypass -File "./.github/scripts/connect-simplysign.ps1"
|
||||
|
||||
- name: Sign release binaries
|
||||
if: env.CERTUM_OTP_URI != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
./.github/scripts/sign-certum.ps1 -Files @(".\bin\Release\gMod.dll", ".\bin\Release\TpfConvert.exe")
|
||||
|
||||
- name: Retrieve version
|
||||
id: set_version
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# gMod
|
||||
|
||||
A 32-bit Windows DLL that hooks Direct3D 9 to load texture mods for Guild Wars. Continuation of uMod. Integrated with [GW Launcher](https://github.com/gwdevhub/gwlauncher) and [Daybreak](https://github.com/gwdevhub/Daybreak).
|
||||
|
||||
## Project structure
|
||||
|
||||
- `header/` — header files (D3D9Hooks, D3D9State, Defines, Error, Main, Utils)
|
||||
- `source/` — implementation files (D3D9Hooks.cpp, dll_main.cpp, Error.cpp)
|
||||
- `modules/` — C++23 module files (ModfileLoader, ModfileLoader_TpfReader, TextureClient, TextureFunction)
|
||||
- `TpfConvert/` — standalone utility to fix broken .tpf files
|
||||
- `cmake/` — CMake helper scripts
|
||||
- `build/` — CMake build tree (generated, ignored)
|
||||
- `bin/` — compiled output (generated, ignored)
|
||||
|
||||
## Build
|
||||
|
||||
Requires Visual Studio 2022, CMake 3.29+, and vcpkg — all via the VS 2022 Developer PowerShell.
|
||||
|
||||
```
|
||||
cmake --preset=vcpkg
|
||||
cmake --open build
|
||||
# then compile inside Visual Studio, or:
|
||||
cmake --build build --config Release
|
||||
```
|
||||
|
||||
- Target: 32-bit only (`-A Win32` / `CMAKE_GENERATOR_PLATFORM=win32`)
|
||||
- Standard: C++23
|
||||
- Output: `bin/` (gMod.dll, TpfConvert.exe)
|
||||
- Version: set in `CMakeLists.txt` (`VERSION_MAJOR/MINOR/PATCH/TWEAK`)
|
||||
|
||||
> **Cannot be built or run in this agent environment** (Linux, no MSVC, no 32-bit
|
||||
> D3D9, no Guild Wars to inject into). Don't burn turns attempting `cmake`/builds
|
||||
> here — validate changes by reading and matching existing patterns, and lean on
|
||||
> Windows CI (`ci.yaml`) for the real build. When an actual compile/test is needed,
|
||||
> ask the user to do it in Visual Studio rather than trying locally.
|
||||
|
||||
## Key dependencies (via vcpkg)
|
||||
|
||||
- `minhook` — D3D9 function hooking
|
||||
- `libzippp` / `libzip` / `zlib` — reading .tpf/.zip mod files
|
||||
- `directxtex` — texture format conversion
|
||||
- `Microsoft.GSL` — Guidelines Support Library
|
||||
|
||||
## How it works
|
||||
|
||||
1. `dll_main.cpp` — DLL entry point, hooks d3d9.dll via MinHook
|
||||
2. `D3D9Hooks.cpp` — intercepts `Direct3DCreate9` and IDirect3DDevice9 methods
|
||||
3. `ModfileLoader` — reads `modlist.txt`, loads .tpf/.zip mod archives
|
||||
4. `TextureClient` / `TextureFunction` — matches and replaces textures by hash at runtime
|
||||
|
||||
### Hooking model (read before touching D3D9 code)
|
||||
|
||||
gMod uses **MinHook vtable hooks**, not proxy/subclass wrappers (the old uMod model
|
||||
was replaced — see `git log` "Replace D3D9 proxy wrappers with vtable hooks"). The
|
||||
game keeps the **real** `IDirect3D9` / `IDirect3DDevice9` / texture objects byte-for-byte
|
||||
untouched; gMod patches only the individual vtable slots it cares about (CreateDevice,
|
||||
CreateTexture/Volume/Cube, UpdateTexture, BeginScene, Set/GetTexture, Release).
|
||||
|
||||
- Per-texture side-state lives **out of band** in `header/D3D9State.h` (`TexState`,
|
||||
keyed by the real texture pointer), so reverting the hooks fully detaches gMod.
|
||||
- `RemoveAllD3D9Hooks()` must leave the host process pristine — preserve that invariant.
|
||||
- A vtable is shared by all instances of a class, so each slot is hooked once; texture
|
||||
Release slots are hooked lazily on first instance. Don't double-hook.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Formatting is CI-enforced** by clang-format `22.1.5` (`.clang-format`, applied by
|
||||
`lint.yaml` on push to `dev`). Don't hand-reformat or "tidy" whole files — it just
|
||||
churns the diff. Notable rules: `ColumnLimit: 0` (never reflow long D3D signatures /
|
||||
vtable typedefs), `SortIncludes: Never` (include order is load-bearing in Windows
|
||||
headers — never reorder), 4-space indent, braces on their own line after functions.
|
||||
- **C++23 named modules** (`modules/*.ixx`). Editing a module *interface* triggers wide
|
||||
rebuilds; prefer changing implementation over interface when possible.
|
||||
- **Public exported API is a fixed surface** — four `extern "C" __declspec(dllexport)`
|
||||
functions in `dll_main.cpp`: `SetDevice`, `AddFile`, `RemoveFile`, `GetFiles`. These
|
||||
are consumed by GW Launcher and Daybreak; do **not** change their names, signatures,
|
||||
or calling convention (`__cdecl`) without coordinating downstream.
|
||||
- **Commits**: short imperative summaries, lowercase, no trailing period
|
||||
(e.g. "Keep loaded_files in load order so GetFiles returns priority order").
|
||||
- **Branching**: `dev` is the integration branch; PRs target `master`. CI builds on
|
||||
push to `dev` and on PRs to `master`.
|
||||
|
||||
### Where things live (by size / how often they change)
|
||||
|
||||
- `modules/TextureClient.ixx` (~670 lines) — texture match/replace bookkeeping, hot
|
||||
- `source/D3D9Hooks.cpp` (~580) — the vtable detours and install/remove
|
||||
- `modules/TextureFunction.ixx` (~380) — texture creation/conversion helpers
|
||||
- `source/dll_main.cpp` (~345) — entry point + exported C API
|
||||
- `modules/ModfileLoader*.ixx` — .tpf/.zip parsing
|
||||
- `header/Defines.h` — shared structs/constants (`TextureFileStruct`, `HashTuple`, etc.)
|
||||
|
||||
## Notes
|
||||
|
||||
- Must be injected before d3d9.dll loads, or renamed to d3d9.dll in the GW folder
|
||||
- Reshade > 5.0.1 causes glitches; recommend 5.0.1 or 4.9.1
|
||||
- `modlist.txt` contains full paths to .tpf/.zip mod files, one per line
|
||||
+1
-1
@@ -10,7 +10,7 @@ if(NOT(CMAKE_SIZEOF_VOID_P EQUAL 4))
|
||||
endif()
|
||||
|
||||
set(VERSION_MAJOR 1)
|
||||
set(VERSION_MINOR 9)
|
||||
set(VERSION_MINOR 10)
|
||||
set(VERSION_PATCH 0)
|
||||
set(VERSION_TWEAK 0)
|
||||
|
||||
|
||||
@@ -6,4 +6,8 @@ void InstallD3D9Hooks(IDirect3D9* d3d9, bool is_ex);
|
||||
|
||||
void RemoveAllD3D9Hooks();
|
||||
|
||||
// Delete every per-device TextureClient, releasing its replacement textures. Only safe
|
||||
// on a real FreeLibrary (device still alive), not during process termination.
|
||||
void DestroyAllTextureClients();
|
||||
|
||||
bool RegisterExistingDevice(IDirect3DDevice9* device);
|
||||
|
||||
@@ -159,10 +159,15 @@ TextureClient::~TextureClient()
|
||||
std::lock_guard lk(registry_mutex);
|
||||
shutting_down = true; // the texture Release hook now no-ops for our textures
|
||||
|
||||
// Drop side-state but don't Release the textures: the game already released
|
||||
// its originals (and our fakes with them), and the device may be gone.
|
||||
for (const auto state : fakes | std::views::values) {
|
||||
delete state;
|
||||
// Release replacements we still own (still partnered) so they aren't leaked when
|
||||
// torn down with the device alive (FreeLibrary). Orphaned fakes were already
|
||||
// released; a device-release teardown leaves the maps empty, so this is a no-op.
|
||||
for (const auto fake : fakes | std::views::values) {
|
||||
if (fake->partner != nullptr) {
|
||||
fake->partner->partner = nullptr; // detach the original's back-pointer
|
||||
if (fake->real) fake->real->Release();
|
||||
}
|
||||
delete fake;
|
||||
}
|
||||
fakes.clear();
|
||||
for (const auto state : originals | std::views::values) {
|
||||
|
||||
+71
-20
@@ -2,6 +2,7 @@
|
||||
#include "D3D9Hooks.h"
|
||||
#include "MinHook.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -31,6 +32,10 @@ namespace {
|
||||
// --- IDirect3D*Texture9 (IUnknown layout) ---------------------------
|
||||
constexpr int kResource_Release = 2;
|
||||
|
||||
// Bytes to flush from a hook target / trampoline so the x86-on-ARM64 emulator re-JITs
|
||||
// it. Covers MinHook's max prologue patch and its 64-byte trampoline slot.
|
||||
constexpr SIZE_T kFlushSpan = 64;
|
||||
|
||||
void** GetVTable(void* com_object)
|
||||
{
|
||||
return *static_cast<void***>(com_object);
|
||||
@@ -72,6 +77,9 @@ namespace {
|
||||
|
||||
std::vector<void*> g_hooked_targets;
|
||||
|
||||
// Set when teardown begins; a detour still reached after this just calls the original.
|
||||
std::atomic<bool> g_unhooked{false};
|
||||
|
||||
// device -> owning TextureClient
|
||||
std::mutex g_devices_mutex;
|
||||
std::unordered_map<IDirect3DDevice9*, TextureClient*> g_devices;
|
||||
@@ -94,6 +102,13 @@ namespace {
|
||||
Warning("D3D9Hooks: MH_EnableHook failed for %p\n", target);
|
||||
return false;
|
||||
}
|
||||
// Under the x86-on-ARM64 emulator, patched code is only re-translated when its
|
||||
// instruction cache is flushed. MinHook flushes just the 5 patched bytes, which can
|
||||
// leave a stale/guarded JIT block and trap; flush the whole prologue and the freshly
|
||||
// built trampoline so the emulator re-JITs both. No-op on native x86.
|
||||
const HANDLE proc = GetCurrentProcess();
|
||||
FlushInstructionCache(proc, target, kFlushSpan);
|
||||
if (original && *original) FlushInstructionCache(proc, *original, kFlushSpan);
|
||||
g_hooked_targets.push_back(target);
|
||||
return true;
|
||||
}
|
||||
@@ -174,6 +189,7 @@ namespace {
|
||||
D3DPRESENT_PARAMETERS* pPresentationParameters, IDirect3DDevice9** ppReturnedDeviceInterface)
|
||||
{
|
||||
const HRESULT hr = o_CreateDevice(self, Adapter, DeviceType, hFocusWindow, BehaviorFlags, pPresentationParameters, ppReturnedDeviceInterface);
|
||||
if (g_unhooked) return hr;
|
||||
if (SUCCEEDED(hr) && ppReturnedDeviceInterface && *ppReturnedDeviceInterface) {
|
||||
OnDeviceCreated(*ppReturnedDeviceInterface);
|
||||
}
|
||||
@@ -184,6 +200,7 @@ namespace {
|
||||
D3DPRESENT_PARAMETERS* pPresentationParameters, D3DDISPLAYMODEEX* pFullscreenDisplayMode, IDirect3DDevice9Ex** ppReturnedDeviceInterface)
|
||||
{
|
||||
const HRESULT hr = o_CreateDeviceEx(self, Adapter, DeviceType, hFocusWindow, BehaviorFlags, pPresentationParameters, pFullscreenDisplayMode, ppReturnedDeviceInterface);
|
||||
if (g_unhooked) return hr;
|
||||
if (SUCCEEDED(hr) && ppReturnedDeviceInterface && *ppReturnedDeviceInterface) {
|
||||
OnDeviceCreated(*ppReturnedDeviceInterface);
|
||||
}
|
||||
@@ -193,6 +210,7 @@ namespace {
|
||||
ULONG STDMETHODCALLTYPE h_DeviceRelease(IDirect3DDevice9* self)
|
||||
{
|
||||
const ULONG count = o_DeviceRelease(self);
|
||||
if (g_unhooked) return count;
|
||||
if (count == 0) {
|
||||
TextureClient* client = nullptr;
|
||||
{
|
||||
@@ -212,6 +230,7 @@ namespace {
|
||||
IDirect3DTexture9** ppTexture, HANDLE* pSharedHandle)
|
||||
{
|
||||
const HRESULT hr = o_CreateTexture(self, Width, Height, Levels, Usage, Format, Pool, ppTexture, pSharedHandle);
|
||||
if (g_unhooked) return hr;
|
||||
if (SUCCEEDED(hr) && ppTexture && *ppTexture) {
|
||||
InstallTextureReleaseHook(*ppTexture, TexType::Tex2D);
|
||||
if (auto* client = ClientFor(self))
|
||||
@@ -224,6 +243,7 @@ namespace {
|
||||
IDirect3DVolumeTexture9** ppVolumeTexture, HANDLE* pSharedHandle)
|
||||
{
|
||||
const HRESULT hr = o_CreateVolumeTexture(self, Width, Height, Depth, Levels, Usage, Format, Pool, ppVolumeTexture, pSharedHandle);
|
||||
if (g_unhooked) return hr;
|
||||
if (SUCCEEDED(hr) && ppVolumeTexture && *ppVolumeTexture) {
|
||||
InstallTextureReleaseHook(*ppVolumeTexture, TexType::Volume);
|
||||
if (auto* client = ClientFor(self))
|
||||
@@ -236,6 +256,7 @@ namespace {
|
||||
IDirect3DCubeTexture9** ppCubeTexture, HANDLE* pSharedHandle)
|
||||
{
|
||||
const HRESULT hr = o_CreateCubeTexture(self, EdgeLength, Levels, Usage, Format, Pool, ppCubeTexture, pSharedHandle);
|
||||
if (g_unhooked) return hr;
|
||||
if (SUCCEEDED(hr) && ppCubeTexture && *ppCubeTexture) {
|
||||
InstallTextureReleaseHook(*ppCubeTexture, TexType::Cube);
|
||||
if (auto* client = ClientFor(self))
|
||||
@@ -248,6 +269,7 @@ namespace {
|
||||
{
|
||||
// Pass the real textures through, then re-evaluate mods on the changed destination.
|
||||
const HRESULT hr = o_UpdateTexture(self, pSourceTexture, pDestinationTexture);
|
||||
if (g_unhooked) return hr;
|
||||
if (SUCCEEDED(hr)) {
|
||||
if (auto* client = ClientFor(self))
|
||||
client->OnUpdateTexture(pSourceTexture, pDestinationTexture);
|
||||
@@ -257,6 +279,7 @@ namespace {
|
||||
|
||||
HRESULT STDMETHODCALLTYPE h_BeginScene(IDirect3DDevice9* self)
|
||||
{
|
||||
if (g_unhooked) return o_BeginScene(self);
|
||||
if (auto* client = ClientFor(self))
|
||||
client->OnBeginScene();
|
||||
return o_BeginScene(self);
|
||||
@@ -264,6 +287,7 @@ namespace {
|
||||
|
||||
HRESULT STDMETHODCALLTYPE h_SetTexture(IDirect3DDevice9* self, DWORD Stage, IDirect3DBaseTexture9* pTexture)
|
||||
{
|
||||
if (g_unhooked) return o_SetTexture(self, Stage, pTexture);
|
||||
IDirect3DBaseTexture9* bind = pTexture;
|
||||
if (auto* client = ClientFor(self))
|
||||
bind = client->ResolveBinding(pTexture); // substitute the fake when modded
|
||||
@@ -273,6 +297,7 @@ namespace {
|
||||
HRESULT STDMETHODCALLTYPE h_GetTexture(IDirect3DDevice9* self, DWORD Stage, IDirect3DBaseTexture9** ppTexture)
|
||||
{
|
||||
const HRESULT hr = o_GetTexture(self, Stage, ppTexture);
|
||||
if (g_unhooked) return hr;
|
||||
if (SUCCEEDED(hr) && ppTexture && *ppTexture) {
|
||||
if (auto* client = ClientFor(self)) {
|
||||
if (auto* original = client->ResolveOriginalFromFake(*ppTexture)) {
|
||||
@@ -295,6 +320,7 @@ namespace {
|
||||
ULONG STDMETHODCALLTYPE h_Tex2DRelease(IUnknown* self)
|
||||
{
|
||||
const ULONG count = o_Tex2DRelease(self);
|
||||
if (g_unhooked) return count;
|
||||
ReleaseTextureCleanup(self, count);
|
||||
return count;
|
||||
}
|
||||
@@ -302,6 +328,7 @@ namespace {
|
||||
ULONG STDMETHODCALLTYPE h_VolumeRelease(IUnknown* self)
|
||||
{
|
||||
const ULONG count = o_VolumeRelease(self);
|
||||
if (g_unhooked) return count;
|
||||
ReleaseTextureCleanup(self, count);
|
||||
return count;
|
||||
}
|
||||
@@ -309,6 +336,7 @@ namespace {
|
||||
ULONG STDMETHODCALLTYPE h_CubeRelease(IUnknown* self)
|
||||
{
|
||||
const ULONG count = o_CubeRelease(self);
|
||||
if (g_unhooked) return count;
|
||||
ReleaseTextureCleanup(self, count);
|
||||
return count;
|
||||
}
|
||||
@@ -317,6 +345,7 @@ namespace {
|
||||
void InstallD3D9Hooks(IDirect3D9* d3d9, const bool is_ex)
|
||||
{
|
||||
if (d3d9 == nullptr || g_d3d9_hooks_installed) return;
|
||||
g_unhooked = false; // re-arm in case a prior teardown left the flag set
|
||||
void** vt = GetVTable(d3d9);
|
||||
Hook(vt[kIDirect3D9_CreateDevice], &h_CreateDevice, reinterpret_cast<void**>(&o_CreateDevice));
|
||||
if (is_ex) {
|
||||
@@ -333,15 +362,21 @@ bool RegisterExistingDevice(IDirect3DDevice9* device)
|
||||
if (g_devices.contains(device)) return false; // already known
|
||||
if (!g_devices.empty()) return false; // gMod supports a single device at a time
|
||||
}
|
||||
g_unhooked = false; // re-arm in case a prior teardown left the flag set
|
||||
OnDeviceCreated(device); // installs device hooks + TextureClient (idempotent)
|
||||
return ClientFor(device) != nullptr;
|
||||
}
|
||||
|
||||
void RemoveAllD3D9Hooks()
|
||||
{
|
||||
// Signal first: a detour reached after this (e.g. a surviving patch) falls through.
|
||||
g_unhooked = true;
|
||||
|
||||
const HANDLE proc = GetCurrentProcess();
|
||||
for (void* target : g_hooked_targets) {
|
||||
MH_DisableHook(target);
|
||||
MH_RemoveHook(target);
|
||||
FlushInstructionCache(proc, target, kFlushSpan); // re-JIT the restored prologue under emulation
|
||||
}
|
||||
g_hooked_targets.clear();
|
||||
|
||||
@@ -351,19 +386,23 @@ void RemoveAllD3D9Hooks()
|
||||
g_volume_release_installed = false;
|
||||
g_cube_release_installed = false;
|
||||
|
||||
o_CreateDevice = nullptr;
|
||||
o_CreateDeviceEx = nullptr;
|
||||
o_DeviceRelease = nullptr;
|
||||
o_CreateTexture = nullptr;
|
||||
o_CreateVolumeTexture = nullptr;
|
||||
o_CreateCubeTexture = nullptr;
|
||||
o_UpdateTexture = nullptr;
|
||||
o_BeginScene = nullptr;
|
||||
o_SetTexture = nullptr;
|
||||
o_GetTexture = nullptr;
|
||||
o_Tex2DRelease = nullptr;
|
||||
o_VolumeRelease = nullptr;
|
||||
o_CubeRelease = nullptr;
|
||||
// Leave the o_* trampoline pointers intact so a surviving detour can still call through.
|
||||
}
|
||||
|
||||
void DestroyAllTextureClients()
|
||||
{
|
||||
// Collect under the lock, delete outside it (~TextureClient takes its own locks).
|
||||
std::vector<TextureClient*> clients;
|
||||
{
|
||||
std::lock_guard lk(g_devices_mutex);
|
||||
for (const auto& entry : g_devices) {
|
||||
clients.push_back(entry.second);
|
||||
}
|
||||
g_devices.clear();
|
||||
}
|
||||
for (auto* client : clients) {
|
||||
delete client;
|
||||
}
|
||||
}
|
||||
|
||||
// All three read state->real directly; gMod never swaps the underlying resource.
|
||||
@@ -400,6 +439,11 @@ namespace {
|
||||
return {};
|
||||
}
|
||||
|
||||
// The surface GetRenderTargetData reads from: either the level itself, or a
|
||||
// non-multisampled resolve of it. Non-owning; pSurfaceLevel_orig and
|
||||
// pResolvedSurface each own a ref and are released exactly once below.
|
||||
IDirect3DSurface9* pCopySource = pSurfaceLevel_orig;
|
||||
|
||||
if (desc.MultiSampleType != D3DMULTISAMPLE_NONE) {
|
||||
if (D3D_OK != device->CreateRenderTarget(desc.Width, desc.Height, desc.Format, D3DMULTISAMPLE_NONE, 0, FALSE, &pResolvedSurface, nullptr)) {
|
||||
pSurfaceLevel_orig->Release();
|
||||
@@ -407,31 +451,39 @@ namespace {
|
||||
return {};
|
||||
}
|
||||
if (D3D_OK != device->StretchRect(pSurfaceLevel_orig, nullptr, pResolvedSurface, nullptr, D3DTEXF_NONE)) {
|
||||
pResolvedSurface->Release();
|
||||
pResolvedSurface = nullptr;
|
||||
pSurfaceLevel_orig->Release();
|
||||
Warning("GetTextureHash(2D) Failed: StretchRect (D3DPOOL_DEFAULT)\n");
|
||||
return {};
|
||||
}
|
||||
pSurfaceLevel_orig = pResolvedSurface;
|
||||
pCopySource = pResolvedSurface;
|
||||
}
|
||||
|
||||
if (D3D_OK != device->CreateOffscreenPlainSurface(desc.Width, desc.Height, desc.Format, D3DPOOL_SYSTEMMEM, &pOffscreenSurface, nullptr)) {
|
||||
pSurfaceLevel_orig->Release();
|
||||
if (pResolvedSurface != nullptr) pResolvedSurface->Release();
|
||||
pSurfaceLevel_orig->Release();
|
||||
Warning("GetTextureHash(2D) Failed: CreateOffscreenPlainSurface (D3DPOOL_DEFAULT)\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (D3D_OK != device->GetRenderTargetData(pSurfaceLevel_orig, pOffscreenSurface)) {
|
||||
pSurfaceLevel_orig->Release();
|
||||
if (pResolvedSurface != nullptr) pResolvedSurface->Release();
|
||||
if (D3D_OK != device->GetRenderTargetData(pCopySource, pOffscreenSurface)) {
|
||||
pOffscreenSurface->Release();
|
||||
if (pResolvedSurface != nullptr) pResolvedSurface->Release();
|
||||
pSurfaceLevel_orig->Release();
|
||||
Warning("GetTextureHash(2D) Failed: GetRenderTargetData (D3DPOOL_DEFAULT)\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
// Copy is done; both source surfaces are finished with. Null pResolvedSurface
|
||||
// so the shared cleanup below treats this as the pOffscreenSurface case.
|
||||
if (pResolvedSurface != nullptr) {
|
||||
pResolvedSurface->Release();
|
||||
pResolvedSurface = nullptr;
|
||||
}
|
||||
pSurfaceLevel_orig->Release();
|
||||
|
||||
if (pOffscreenSurface->LockRect(&d3dlr, nullptr, D3DLOCK_READONLY) != D3D_OK) {
|
||||
if (pResolvedSurface != nullptr) pResolvedSurface->Release();
|
||||
pOffscreenSurface->Release();
|
||||
Warning("GetTextureHash(2D) Failed: LockRect (D3DPOOL_DEFAULT)\n");
|
||||
return {};
|
||||
@@ -456,7 +508,6 @@ namespace {
|
||||
if (pOffscreenSurface != nullptr) {
|
||||
pOffscreenSurface->UnlockRect();
|
||||
pOffscreenSurface->Release();
|
||||
if (pResolvedSurface != nullptr) pResolvedSurface->Release();
|
||||
}
|
||||
else if (pResolvedSurface != nullptr) {
|
||||
pResolvedSurface->UnlockRect();
|
||||
|
||||
+30
-5
@@ -2,10 +2,11 @@
|
||||
#include <Psapi.h>
|
||||
#include "MinHook.h"
|
||||
#include "D3D9Hooks.h"
|
||||
#include <atomic>
|
||||
|
||||
import TextureClient;
|
||||
|
||||
void ExitInstance();
|
||||
void ExitInstance(bool is_unloading);
|
||||
void InitInstance(HINSTANCE hModule);
|
||||
|
||||
namespace {
|
||||
@@ -179,8 +180,6 @@ HRESULT APIENTRY Direct3DCreate9Ex(UINT SDKVersion, IDirect3D9Ex** ppD3D)
|
||||
|
||||
BOOL WINAPI DllMain(HINSTANCE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
UNREFERENCED_PARAMETER(lpReserved);
|
||||
|
||||
switch (ul_reason_for_call) {
|
||||
case DLL_PROCESS_ATTACH: {
|
||||
#ifdef _DEBUG
|
||||
@@ -196,7 +195,12 @@ BOOL WINAPI DllMain(HINSTANCE hModule, DWORD ul_reason_for_call, LPVOID lpReserv
|
||||
break;
|
||||
}
|
||||
case DLL_PROCESS_DETACH: {
|
||||
ExitInstance();
|
||||
// Process exit (lpReserved != nullptr): other threads are gone and the OS
|
||||
// reclaims everything, so touching MinHook/the device here only risks a hang.
|
||||
if (lpReserved != nullptr)
|
||||
break;
|
||||
// FreeLibrary unload: the device is still live, so tear down cleanly.
|
||||
ExitInstance(true);
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
@@ -248,6 +252,10 @@ void InitInstance(HINSTANCE hModule)
|
||||
if (GetProcAddress_fn) {
|
||||
MH_CreateHook(GetProcAddress_fn, OnGetProcAddress, (void**)&GetProcAddress_ret);
|
||||
MH_EnableHook(GetProcAddress_fn);
|
||||
// Re-JIT the patched prologue + trampoline under the x86-on-ARM64 emulator (no-op native).
|
||||
const HANDLE proc = GetCurrentProcess();
|
||||
FlushInstructionCache(proc, (void*)GetProcAddress_fn, 64);
|
||||
if (GetProcAddress_ret) FlushInstructionCache(proc, (void*)GetProcAddress_ret, 64);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,13 +322,30 @@ extern "C" __declspec(dllexport) int __cdecl GetFiles(wchar_t* buffer, const siz
|
||||
}
|
||||
}
|
||||
|
||||
void ExitInstance()
|
||||
// Optional clean-shutdown entry: call this from the host (on a normal thread) BEFORE
|
||||
// FreeLibrary so teardown runs off the loader lock, where MinHook can safely suspend
|
||||
// the render thread. A later FreeLibrary then unloads with nothing left to undo.
|
||||
extern "C" __declspec(dllexport) void __cdecl Shutdown()
|
||||
{
|
||||
ExitInstance(true);
|
||||
}
|
||||
|
||||
void ExitInstance(bool is_unloading)
|
||||
{
|
||||
// Teardown must run exactly once, whether reached via Shutdown() or DllMain detach.
|
||||
static std::atomic<bool> torn_down{false};
|
||||
if (torn_down.exchange(true)) return;
|
||||
|
||||
DISABLE_HOOK(GetProcAddress_fn);
|
||||
|
||||
// Revert every D3D9 vtable hook so the original objects are left pristine.
|
||||
RemoveAllD3D9Hooks();
|
||||
|
||||
// On a real unload the device is still alive; release our replacement textures.
|
||||
if (is_unloading) {
|
||||
DestroyAllTextureClients();
|
||||
}
|
||||
|
||||
MH_Uninitialize();
|
||||
|
||||
if (gMod_Loaded_d3d9_Module_Handle)
|
||||
|
||||
Reference in New Issue
Block a user