Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ jobs:
# the release gate. Hardware-dependent tests retain explicit opt-in gates.
run: dotnet test .\DS4WindowsTests\DS4WindowsTests.csproj -c Release -p:Platform=x64 --verbosity normal

- name: Test numeric release identity and legacy publication policy
shell: pwsh
run: .\utils\test-release-identity.ps1

- name: Test renamed executable setup process guards
shell: powershell
run: .\utils\test-renamed-executable-setup.ps1
Expand Down
70 changes: 49 additions & 21 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
workflow_dispatch:
inputs:
tag:
description: Existing named VIIPERRC prerelease draft to build without publishing
description: Existing numeric release draft to build without publishing (for example 5.0.14)
required: true
type: string

Expand All @@ -20,7 +20,7 @@ jobs:
outputs:
tag: ${{ steps.release.outputs.tag }}
release_id: ${{ steps.release.outputs.release_id }}
unsigned_rc: ${{ steps.release.outputs.unsigned_rc }}
unsigned_release: ${{ steps.release.outputs.unsigned_release }}
verify_existing: ${{ steps.release.outputs.verify_existing }}
steps:
- name: Verify release identity and signing policy
Expand Down Expand Up @@ -66,8 +66,13 @@ jobs:
$isPrerelease = $release.prerelease.ToString().ToLowerInvariant()
$unsignedRc = $isPrerelease -ceq 'true' -and
$tag -cmatch '^VIIPERRC[0-9]+(\.[0-9]+){0,3}\z'
if ($dispatch -and -not $unsignedRc) {
throw 'Draft dispatch is limited to named RC prereleases; other releases retain the signed published-event path.'
$numericStable = $env:GITHUB_REPOSITORY -ceq 'Pav-Osmolski/DS4Windows' -and
$isPrerelease -ceq 'false' -and
$tag -cmatch '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\z' -and
([version]$tag) -ge ([version]'5.0.14')
$unsignedRelease = $unsignedRc -or $numericStable
if ($dispatch -and -not $numericStable) {
throw 'New draft builds require a numeric non-prerelease tag of 5.0.14 or newer in this fork.'
}
if ($dispatch) {
# Saving a new draft does not create its Git tag. Materialize only
Expand All @@ -82,14 +87,21 @@ jobs:
gh api --method POST "repos/$env:GITHUB_REPOSITORY/git/refs" `
-f "ref=refs/tags/$tag" -f "sha=$($target.sha)"
}
$tagged = gh api "repos/$env:GITHUB_REPOSITORY/commits/$tag" | ConvertFrom-Json
if ($tagged.sha -cne $env:GITHUB_SHA) {
throw 'Draft builds require the tag and selected workflow commit to match.'
}
}
"tag=$tag" >> $env:GITHUB_OUTPUT
"release_id=$($release.id)" >> $env:GITHUB_OUTPUT
"unsigned_rc=$($unsignedRc.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT
"verify_existing=$(((-not $dispatch) -and $unsignedRc).ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT
"unsigned_release=$($unsignedRelease.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT
"verify_existing=$(((-not $dispatch) -and $unsignedRelease).ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT
if ($unsignedRc) {
Write-Warning 'Unsigned named release candidate; not a signed stable release.'
'Unsigned named release candidate; Authenticode signing intentionally omitted.' >> $env:GITHUB_STEP_SUMMARY
} elseif ($numericStable) {
Write-Warning 'Unsigned numeric release; no Authenticode signature is claimed.'
'Unsigned numeric release; exact source and asset integrity checks remain mandatory.' >> $env:GITHUB_STEP_SUMMARY
} else {
Write-Host 'Approved certificate, identity and timestamp gates remain mandatory.'
}
Expand All @@ -99,10 +111,10 @@ jobs:
runs-on: windows-2022
env:
RELEASE_TAG: ${{ needs.identity.outputs.tag }}
UNSIGNED_RC_RELEASE: ${{ needs.identity.outputs.unsigned_rc }}
UNSIGNED_RELEASE: ${{ needs.identity.outputs.unsigned_release }}
RELEASE_ID: ${{ needs.identity.outputs.release_id }}
# Release publication follows independently checked CI. Named RC prereleases
# may be unsigned; stable and all other releases still require signing.
# Build and test a draft before publication. This fork permits unsigned
# numeric Latest releases; other release types retain signing gates.
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -129,6 +141,9 @@ jobs:
PROJECT_VERSION=$(python -c "import xml.etree.ElementTree as ET; print(ET.parse('DS4Windows/DS4WinWPF.csproj').find('.//Version').text)")
if [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){1,3}(-.*)?$ ]]; then
BINARY_VERSION=${VERSION%%-*}
if [[ "$BINARY_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
BINARY_VERSION="$BINARY_VERSION.0"
fi
PACKAGE_VERSION=$VERSION
else
BINARY_VERSION=$PROJECT_ASSEMBLY_VERSION
Expand All @@ -150,7 +165,7 @@ jobs:
"-p:InformationalVersion=$env:VERSION" `
-o .\bin\x64\Release\output
- name: Sign and verify release binaries
if: env.UNSIGNED_RC_RELEASE != 'true'
if: env.UNSIGNED_RELEASE != 'true'
shell: pwsh
env:
DS4W_SIGN_CERT_BASE64: ${{ secrets.DS4W_SIGN_CERT_BASE64 }}
Expand Down Expand Up @@ -196,8 +211,8 @@ jobs:
- name: Build standard installer X64
shell: pwsh
env:
DS4W_SIGN_CERT_PASSWORD: ${{ env.UNSIGNED_RC_RELEASE != 'true' && secrets.DS4W_SIGN_CERT_PASSWORD || '' }}
DS4W_SIGN_EXPECTED_THUMBPRINT: ${{ env.UNSIGNED_RC_RELEASE != 'true' && secrets.DS4W_SIGN_EXPECTED_THUMBPRINT || '' }}
DS4W_SIGN_CERT_PASSWORD: ${{ env.UNSIGNED_RELEASE != 'true' && secrets.DS4W_SIGN_CERT_PASSWORD || '' }}
DS4W_SIGN_EXPECTED_THUMBPRINT: ${{ env.UNSIGNED_RELEASE != 'true' && secrets.DS4W_SIGN_EXPECTED_THUMBPRINT || '' }}
run: |
$parameters = @{
PublishRoot = ".\bin\x64\Release\DS4Windows"
Expand All @@ -206,7 +221,7 @@ jobs:
DisplayVersion = $env:VERSION
OutputDirectory = ".\bin\x64\Release\installer"
SkipApplicationPublish = $true
RequireSigning = $env:UNSIGNED_RC_RELEASE -ne 'true'
RequireSigning = $env:UNSIGNED_RELEASE -ne 'true'
}
.\installer\build-installer.ps1 @parameters
- name: Name release asset
Expand Down Expand Up @@ -286,7 +301,7 @@ jobs:
$noticeNames = @('VIIPER-0.1.9-rc4.6.6-LICENSES.txt', 'VIIPER-0.1.9-rc4.6.6-PROVENANCE.txt',
'VIIPER-0.1.9-rc4.6.6-BUILD-NOTES.txt', 'VIIPER-SYSTRAY-LICENSE.txt', 'VIIPER-SYSTRAY-NOTICE.md', 'VIIPER-WDL-NOTICE.txt', 'LICENSE.txt')
foreach ($name in $noticeNames) { Copy-Item -LiteralPath (Join-Path $publish "extras\$name") -Destination $records }
$signing = if ($env:UNSIGNED_RC_RELEASE -ceq 'true') { 'Unsigned named release candidate' } else { 'Required Authenticode signing verified' }
$signing = if ($env:UNSIGNED_RELEASE -ceq 'true') { 'Unsigned release (no Authenticode signature)' } else { 'Required Authenticode signing verified' }
@("DS4Windows release: $env:RELEASE_TAG", "DS4Windows source commit: $sourceCommit",
"VIIPER source commit: $brokerCommit", "VIIPER binary SHA-256: $brokerHash",
"Signing: $signing", "GitHub Actions run: $env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/actions/runs/$env:GITHUB_RUN_ID") |
Expand All @@ -300,7 +315,8 @@ jobs:
$receipt = @{
schema = 1; repository = $env:GITHUB_REPOSITORY; tag = $env:RELEASE_TAG
sourceCommit = $sourceCommit; releaseId = $env:RELEASE_ID; runId = $env:GITHUB_RUN_ID
unsignedRc = ($env:UNSIGNED_RC_RELEASE -ceq 'true'); binaryVersion = $env:BINARY_VERSION
unsignedRc = ($env:RELEASE_TAG -cmatch '^VIIPERRC'); unsignedRelease = ($env:UNSIGNED_RELEASE -ceq 'true')
prerelease = ($env:RELEASE_TAG -cmatch '^VIIPERRC'); binaryVersion = $env:BINARY_VERSION
brokerSourceName = $brokerSourceName; brokerCommit = $brokerCommit
assets = @($assets | ForEach-Object { @{ name = [IO.Path]::GetFileName($_); sha256 = (Get-FileHash -LiteralPath $_ -Algorithm SHA256).Hash } })
}
Expand Down Expand Up @@ -363,12 +379,12 @@ jobs:
if (Test-Path -LiteralPath $certificatePath -PathType Leaf) {
Remove-Item -LiteralPath $certificatePath -Force
}
verify_published_rc:
verify_published_release:
needs: identity
if: needs.identity.outputs.verify_existing == 'true'
runs-on: windows-2022
steps:
- name: Verify published RC bytes from the successful draft build
- name: Verify published release bytes from the successful draft build
shell: pwsh
env:
GITHUB_TOKEN: ${{ github.TOKEN }}
Expand All @@ -378,7 +394,7 @@ jobs:
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
$release = gh api "repos/$env:GITHUB_REPOSITORY/releases/tags/$env:RELEASE_TAG" | ConvertFrom-Json
$records = Join-Path $env:RUNNER_TEMP 'verify-published-rc'
$records = Join-Path $env:RUNNER_TEMP 'verify-published-release'
New-Item -ItemType Directory -Path $records | Out-Null
$receiptAsset = @($release.assets | Where-Object { $_.name -ceq 'RELEASE-BUILD.json' })
if ($receiptAsset.Count -ne 1 -or $receiptAsset[0].uploader.login -cne 'github-actions[bot]') {
Expand All @@ -389,7 +405,8 @@ jobs:
$tagCommit = (gh api "repos/$env:GITHUB_REPOSITORY/commits/$env:RELEASE_TAG" --jq '.sha').Trim()
if ($receipt.schema -ne 1 -or $receipt.repository -cne $env:GITHUB_REPOSITORY -or
$receipt.tag -cne $env:RELEASE_TAG -or "$($receipt.releaseId)" -cne $env:RELEASE_ID -or
$receipt.sourceCommit -cne $tagCommit -or $receipt.unsignedRc -ne $true -or
$receipt.sourceCommit -cne $tagCommit -or
(-not $receipt.unsignedRc -and ($receipt.unsignedRelease -ne $true -or $receipt.prerelease -ne $false)) -or
$receipt.brokerSourceName -cnotmatch '^[0-9A-Za-z][0-9A-Za-z._-]+-SOURCE\.zip\z' -or
"$($receipt.runId)" -cnotmatch '^[0-9]+\z') { throw 'RC receipt identity mismatch.' }
$run = gh api "repos/$env:GITHUB_REPOSITORY/actions/runs/$($receipt.runId)" | ConvertFrom-Json
Expand All @@ -407,10 +424,21 @@ jobs:
throw "Published RC asset hash mismatch: $($asset.name)"
}
}
foreach ($required in @('DS4Windows_VIIPER_x64.zip', "DS4Windows_$($receipt.binaryVersion)_Setup_x64.exe",
$portableName = if ($receipt.unsignedRc) { 'DS4Windows_VIIPER_x64.zip' } else { "DS4Windows_$($env:RELEASE_TAG)_x64.zip" }
if (-not $receipt.unsignedRc -and ($release.prerelease -or $receipt.binaryVersion -cne "$($env:RELEASE_TAG).0")) {
throw 'Numeric release channel or Windows version mismatch.'
}
foreach ($required in @($portableName, "DS4Windows_$($receipt.binaryVersion)_Setup_x64.exe",
"DS4Windows-$($env:RELEASE_TAG)-SOURCE.zip", $receipt.brokerSourceName, 'SHA256SUMS.txt', 'SOURCE-REVISIONS.txt',
'VIIPER-0.1.9-rc4.6.6-LICENSES.txt', 'VIIPER-0.1.9-rc4.6.6-PROVENANCE.txt',
'VIIPER-0.1.9-rc4.6.6-BUILD-NOTES.txt', 'VIIPER-SYSTRAY-LICENSE.txt', 'VIIPER-SYSTRAY-NOTICE.md', 'VIIPER-WDL-NOTICE.txt', 'LICENSE.txt')) {
if (-not $seen.Contains($required)) { throw "Incomplete RC receipt: $required" }
}
Write-Host 'Published unsigned RC matches its successful draft build; no assets rebuilt or overwritten.'
Write-Host 'Published release matches its successful draft build; no assets rebuilt or overwritten.'
if (-not $receipt.unsignedRc) {
gh api --method PATCH "repos/$env:GITHUB_REPOSITORY/releases/$env:RELEASE_ID" -F prerelease=false -f make_latest=true
$latest = gh api "repos/$env:GITHUB_REPOSITORY/releases/latest" | ConvertFrom-Json
if ("$($latest.id)" -cne $env:RELEASE_ID -or $latest.prerelease -or $latest.draft) {
throw 'Published numeric release was not selected as Latest.'
}
}
31 changes: 27 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,31 @@
Release history for the Pav-Osmolski DS4Windows fork. Downloads, source
archives, checksums, and build records are available on [Releases](https://github.com/Pav-Osmolski/DS4Windows/releases).

## Unreleased — VIIPERRC4.6.8
## Unreleased — 5.0.14

### Fixed

- Preserve the previous profile when a save is interrupted ([upstream #129](https://github.com/hbashton/DS4Windows/pull/129), meiameiameia).
- Keep virtual output slots registered when disconnect fails, allowing a retry without reusing the occupied slot ([upstream #130](https://github.com/hbashton/DS4Windows/pull/130), meiameiameia).
- Respect touchpad-click remapping and passthrough settings in emulated DS4, DualSense and DualSense Edge reports ([upstream #131](https://github.com/hbashton/DS4Windows/pull/131), meiameiameia).
- Remove the controller properly after a Bluetooth disconnect special action, allowing reconnection without Stop/Start ([upstream #132](https://github.com/hbashton/DS4Windows/pull/132), meiameiameia).
- Reuse the profile XML serializer to avoid repeatedly generating assemblies and reduce profile-switch overhead ([upstream #134](https://github.com/hbashton/DS4Windows/pull/134), petemess95).

### Changed

- Retired RC release names. New releases use numeric versions and are published as **Latest**, with the prerelease flag cleared.
- App and installer versions advance to **5.0.14.0**; release and package version **5.0.14**. Installer: `DS4Windows_5.0.14.0_Setup_x64.exe`; portable package: `DS4Windows_5.0.14_x64.zip`.
- Continue unsigned releases with exact-source build records and checksum verification. See the [release process](docs/release-process.md).

### Upgrade notes

DS4Updater 2.0.9 already supports numeric releases and the schema 1 build
receipt. Existing fork builds with fork update support can update to 5.0.14.
RC4.6.7 users still need one manual upgrade because that build checks upstream.
The pinned VIIPER and USB/IP packages are unchanged. Every new release must
increment the numeric app and installer version.

## VIIPERRC4.6.8 — tested build

### Changed

Expand All @@ -20,9 +44,8 @@ manually once. Replacing DS4Updater alone does not change RC4.6.7's upstream
update checks. Later updates from the new app use this fork.

DS4Updater [2.0.9](https://github.com/Pav-Osmolski/DS4Updater/releases/tag/v2.0.9)
is published. RC4.6.8 remains a draft for testing. The pinned VIIPER and USB/IP
packages are unchanged from RC4.6.7. Each new RC must increment the numeric
app and installer version.
is published. RC4.6.8 was tested successfully. Its changes are included in
5.0.14. The pinned VIIPER and USB/IP packages are unchanged from RC4.6.7.

## [VIIPERRC4.6.7](https://github.com/Pav-Osmolski/DS4Windows/releases/tag/VIIPERRC4.6.7) — 2026-10-09

Expand Down
11 changes: 11 additions & 0 deletions DS4Windows/DS4Control/DTOXml/ProfileDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3976,6 +3976,17 @@ public void PostProcessXml()
//OutputContDevice +Done
}

// The framework only caches XmlSerializer(Type) and XmlSerializer(Type, string).
// Constructing one with XmlAttributeOverrides generates and loads a new dynamic
// assembly every time (~100 ms with first use) and that assembly never unloads.
// Serialize/Deserialize are thread-safe, so one shared instance is used instead.
private static readonly Lazy<XmlSerializer> serializer =
new Lazy<XmlSerializer>(
() => new XmlSerializer(typeof(ProfileDTO), GetAttributeOverrides()),
System.Threading.LazyThreadSafetyMode.ExecutionAndPublication);

internal static XmlSerializer Serializer => serializer.Value;

public static XmlAttributeOverrides GetAttributeOverrides()
{
XmlAttributeOverrides xmlOverrides = new XmlAttributeOverrides();
Expand Down
2 changes: 1 addition & 1 deletion DS4Windows/DS4Control/Mapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1253,7 +1253,7 @@
65, // DS4Controls.Switch2JoyConRightSL
66, // DS4Controls.Switch2JoyConRightSR
};
private static int macroEndIndex = DS4_CONTROL_MACRO_ARRAY_LEN - 1;

Check warning on line 1256 in DS4Windows/DS4Control/Mapping.cs

View workflow job for this annotation

GitHub Actions / build (x64)

The field 'Mapping.macroEndIndex' is assigned but its value is never used

Check warning on line 1256 in DS4Windows/DS4Control/Mapping.cs

View workflow job for this annotation

GitHub Actions / build (x64)

The field 'Mapping.macroEndIndex' is assigned but its value is never used

Check warning on line 1256 in DS4Windows/DS4Control/Mapping.cs

View workflow job for this annotation

GitHub Actions / build (x64)

The field 'Mapping.macroEndIndex' is assigned but its value is never used

Check warning on line 1256 in DS4Windows/DS4Control/Mapping.cs

View workflow job for this annotation

GitHub Actions / build (x64)

The field 'Mapping.macroEndIndex' is assigned but its value is never used

// Special macros
static bool altTabDone = true;
Expand Down Expand Up @@ -5302,7 +5302,7 @@
//bool exclusive = /*tempBool =*/ d.isExclusive();
if (deviceConn == ConnectionType.BT)
{
d.DisconnectBT();
d.DisconnectBT(callRemoval: true);
ReleaseActionKeys(action, device);
return;
}
Expand Down
18 changes: 11 additions & 7 deletions DS4Windows/DS4Control/OutputSlotManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,17 +299,19 @@ public void DeferredRemoval(OutputDevice outputDevice, int inIdx,
{
ControlService.StartupDiag($"OutputSlotManager.DeferredRemoval found slot={slot + 1} type={outputDevice.GetDeviceType()}");
//int slot = revDeviceDict[outputDevice];
outputDevices[slot] = null;
deviceDict.Remove(slot);
revDeviceDict.Remove(outputDevice);

ControlService.StartupDiag($"OutputSlotManager.RemoveFeedbacks begin slot={slot + 1}");
outputDevice.RemoveFeedbacks();
ControlService.StartupDiag($"OutputSlotManager.RemoveFeedbacks end slot={slot + 1}");
ControlService.StartupDiag($"OutputSlotManager.Disconnect begin slot={slot + 1}");
outputDevice.Disconnect();
ControlService.StartupDiag($"OutputSlotManager.Disconnect end slot={slot + 1}");

// Failed teardown must retain the exact registration so
// this slot cannot be reused and removal can be retried.
outputDevices[slot] = null;
deviceDict.Remove(slot);
revDeviceDict.Remove(outputDevice);

if (inIdx != -1)
{
outdevs[inIdx] = null;
Expand Down Expand Up @@ -549,8 +551,8 @@ internal void RetireHeldOutput(OutputDevice output, OutputDevice[] bound, int in
!revDeviceDict.TryGetValue(output, out int slot) ||
!IsExactAttachedSlotNoLock(outputSlots[slot], output) || outputSlots[slot].InputIndex != index)
throw new InvalidOperationException("The retained output no longer has its exact reservation.");
// Unlike legacy deferred removal, keep the exact manager record
// until Disconnect succeeds. A failure remains retryable at Stop.
// Keep the exact manager record until Disconnect succeeds.
// A failure remains retryable at Stop.
output.RemoveFeedbacks();
output.Disconnect();
outputDevices[slot] = null;
Expand Down Expand Up @@ -587,9 +589,11 @@ public void UnplugRemainingControllers(bool immediate=false)
{
if (device.OutputDevice != null)
{
outputDevices[slotIdx] = null;
device.OutputDevice.Disconnect();

outputDevices[slotIdx] = null;
deviceDict.Remove(slotIdx);
revDeviceDict.Remove(device.OutputDevice);
device.DetachDevice();
SlotUnassigned?.Invoke(this, slotIdx, outputSlots[slotIdx]);
//if (!immediate)
Expand Down
3 changes: 1 addition & 2 deletions DS4Windows/DS4Control/PreparedProfileLoad.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ internal static bool TryPrepare(string path, int device,
}
}

var serializer = new XmlSerializer(typeof(ProfileDTO),
ProfileDTO.GetAttributeOverrides());
var serializer = ProfileDTO.Serializer;
using var reader = new StringReader(xml);
var candidate = serializer.Deserialize(reader) as ProfileDTO;
if (candidate == null)
Expand Down
Loading
Loading