d6647b9183
The punktfunk-session Vulkan client (clients/linux-session, now clients/session) builds and runs on Windows; the WinUI shell spawns it for every stream. Verified live: 10-bit HEVC via Vulkan Video on both AMD (iGPU) and NVIDIA, 5120x1440 at 130 fps / 8 ms end-to-end on the RTX 4090. - pf-ffvk: Windows bindgen branch (FFMPEG_DIR + PF_FFVK_VULKAN_INCLUDE, no pkg-config); provisioning fetches Vulkan-Headers (pinned v1.4.309). - pf-client-core: builds on Windows — WASAPI audio (audio_wasapi.rs, cfg-swapped via #[path], same surface as the PipeWire twin), VAAPI/dmabuf gated inline (chain = vulkan -> software), trust reads the WinUI shell's %APPDATA% stores (parity tests pin both serialized shapes), Settings gains adapter/hdr_enabled (serde-defaulted; Linux stores unaffected). - pf-presenter: builds on Windows — dmabuf module Linux-gated; SDL keyboard grab while captured (Alt+Tab/Win reach the host); pick_device ranks discrete over integrated (device 0 was the iGPU on hybrid boxes — the silent footgun) and honors PUNKTFUNK_VK_ADAPTER (the Settings GPU pick, exported by the session). - run loop: block in one SDL wait woken by input AND decoded frames (a per- session forwarder pushes a FrameWake user event) instead of a 1 ms poll — measured 111%% -> 5%% of a core (NVIDIA), 86%% -> 3.5%% (AMD), stats unchanged. The pump's decode-fence wait became once-per-window sampling (no per-frame pipeline stall; the stat now shows true backlog). - pf-console-ui: builds on Windows (skia-safe msvc prebuilts); font lookup falls through fontconfig aliases to concrete DirectWrite families (Consolas/Segoe UI) — browse/coverflow works, verified against a live host. - WinUI shell: session-always via new src/spawn.rs (GTK spawn.rs port — CREATE_NO_WINDOW, stdout contract, kill handle); the Stream screen is a status card (chips + stage lines from the child's stats). The legacy in-process D3D11VA path stays behind Settings "Streaming engine" / PUNKTFUNK_BUILTIN_ STREAM=1 as the A/B baseline until Phase 8 deletes it. SessionParams.video_caps makes the HDR toggle real. - clients/linux-session renamed to clients/session (builds for both OSes). - CI/MSIX: both workflows build/test both bins with widened path filters; the MSIX ships punktfunk-session.exe. ARM64 session builds --no-default-features (rust-skia has no aarch64-pc-windows-msvc prebuilts; flip when it does). A/B on this box (5120x1440 HEVC vs home-worker-5): NVIDIA Vulkan 130 fps / 8 ms e2e / 1.6 ms decode — clearly better than the built-in path. The AMD iGPU VCN saturates at ~52 fps where its own D3D11VA does ~70 — Adrenalin Vulkan decode is slower on APU silicon; discrete RDNA validation gates Phase 8. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
190 lines
11 KiB
PowerShell
190 lines
11 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Assemble, pack and sign the punktfunk Windows client as a signed MSIX.
|
|
|
|
.DESCRIPTION
|
|
Builds a packaging layout from a release `cargo build` output (exe + the reactor/SDL3 auto-staged
|
|
DLLs + resources.pri + FFmpeg DLLs + the checked-in Assets + the manifest), runs makeappx, and
|
|
signs with signtool. Idempotent; safe to re-run.
|
|
|
|
Signing cert precedence:
|
|
1. -PfxBase64 / -PfxPassword (a real or shared code-signing cert, e.g. from CI secrets) — the
|
|
cert's subject DN MUST match -Publisher (which is stamped into the manifest Identity).
|
|
2. otherwise an EPHEMERAL self-signed code-signing cert with subject = -Publisher is generated
|
|
in-process. The package installs only where that cert is trusted, so the matching public
|
|
.cer is exported next to the .msix for the user to import (Trusted People) before install.
|
|
Swap in a real cert later with zero manifest changes — just pass -PfxBase64/-Publisher.
|
|
|
|
Run on the Windows runner (or the dev VM) with the MSVC/Windows SDK present.
|
|
|
|
.EXAMPLE
|
|
# x64 (default arch):
|
|
pwsh -File pack-msix.ps1 -Version 0.2.137.0 -TargetDir C:\t\x86_64-pc-windows-msvc\release -OutDir C:\t\msix
|
|
# arm64 (point -TargetDir + FFMPEG_DIR at the ARM64 build/tree):
|
|
$env:FFMPEG_DIR='C:\Users\Public\ffmpeg-arm64'
|
|
pwsh -File pack-msix.ps1 -Version 0.2.137.0 -Arch arm64 -TargetDir C:\t-a64\aarch64-pc-windows-msvc\release -OutDir C:\t-a64\msix
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Version, # 4-part numeric, e.g. 0.2.137.0
|
|
[Parameter(Mandatory = $true)][string]$TargetDir, # cargo --release output dir (has the exe)
|
|
[ValidateSet('x64', 'arm64')][string]$Arch = 'x64', # package ProcessorArchitecture + artifact suffix
|
|
[string]$FfmpegBin = $(if ($env:FFMPEG_DIR) { Join-Path $env:FFMPEG_DIR 'bin' } else { 'C:\Users\Public\ffmpeg\bin' }),
|
|
[string]$OutDir = (Join-Path $TargetDir 'msix'),
|
|
[string]$Publisher = 'CN=unom', # MUST equal the signing cert subject DN
|
|
[string]$PfxBase64 = $env:MSIX_CERT_PFX_B64, # optional: base64 of a code-signing .pfx
|
|
[string]$PfxPassword = $env:MSIX_CERT_PASSWORD
|
|
)
|
|
$ErrorActionPreference = 'Stop'
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
|
|
if ($Version -notmatch '^\d+\.\d+\.\d+\.\d+$') {
|
|
throw "Version must be 4-part numeric (Major.Minor.Build.Revision); got '$Version'."
|
|
}
|
|
|
|
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
$assets = Join-Path $here 'assets'
|
|
$manifestTemplate = Join-Path $here 'AppxManifest.xml'
|
|
|
|
# --- locate the Windows SDK tools (newest makeappx/signtool under the x64 kit bin) ---
|
|
function Find-SdkTool([string]$name) {
|
|
$root = 'C:\Program Files (x86)\Windows Kits\10\bin'
|
|
# match only versioned x64 kit bins (…\10\bin\10.0.NNNNN.N\x64\tool.exe) and pick the newest
|
|
$hit = Get-ChildItem -Path $root -Recurse -Filter $name -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.FullName -match '\\(10\.0\.\d+\.\d+)\\x64\\' } |
|
|
Sort-Object { [version]([regex]::Match($_.FullName, '\\(10\.0\.\d+\.\d+)\\x64\\').Groups[1].Value) } |
|
|
Select-Object -Last 1
|
|
if (-not $hit) { throw "$name not found under $root — install the Windows 10/11 SDK." }
|
|
$hit.FullName
|
|
}
|
|
$makeappx = Find-SdkTool 'makeappx.exe'
|
|
$signtool = Find-SdkTool 'signtool.exe'
|
|
Write-Host "makeappx: $makeappx"
|
|
Write-Host "signtool: $signtool"
|
|
|
|
# --- assemble the package layout ---
|
|
$layout = Join-Path $OutDir 'layout'
|
|
if (Test-Path $layout) { Remove-Item -Recurse -Force $layout }
|
|
New-Item -ItemType Directory -Force -Path (Join-Path $layout 'Assets') | Out-Null
|
|
|
|
# binaries + auto-staged runtime bits (reactor stages the App SDK bootstrap DLL + resources.pri,
|
|
# the sdl3 crate stages SDL3.dll — see crate build output). punktfunk-session.exe is the Vulkan
|
|
# session client the shell spawns for every stream (sibling resolution — see clients/windows/
|
|
# src/spawn.rs); Skia links statically and vulkan-1.dll is a GPU-driver component, so the session
|
|
# adds no DLLs of its own.
|
|
$required = @('punktfunk-client.exe', 'punktfunk-session.exe', 'Microsoft.WindowsAppRuntime.Bootstrap.dll', 'SDL3.dll', 'resources.pri')
|
|
foreach ($f in $required) {
|
|
$src = Join-Path $TargetDir $f
|
|
if (-not (Test-Path $src)) { throw "missing build artifact '$f' in $TargetDir (did 'cargo build --release' run?)" }
|
|
Copy-Item $src (Join-Path $layout $f) -Force
|
|
}
|
|
|
|
# FFmpeg runtime DLLs (the exe link-imports the decode set; copy them all — small and correct).
|
|
# These are unmodified BtbN *lgpl-shared* builds, linked dynamically (replaceable DLLs) — FFmpeg is
|
|
# used under the LGPL v2.1+; the license text + notice ship in licenses\ below.
|
|
$ff = Get-ChildItem -Path $FfmpegBin -Filter *.dll -ErrorAction SilentlyContinue
|
|
if (-not $ff) { throw "no FFmpeg DLLs in $FfmpegBin" }
|
|
$ff | ForEach-Object { Copy-Item $_.FullName (Join-Path $layout $_.Name) -Force }
|
|
|
|
# license/attribution payload (MSIX has no installer EULA page, so ship them as files): FFmpeg's LGPL
|
|
# notice + license text, the project's own MIT/Apache texts, and the generated third-party notices.
|
|
$licDir = Join-Path $layout 'licenses'
|
|
New-Item -ItemType Directory -Force -Path $licDir | Out-Null
|
|
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path
|
|
Copy-Item (Join-Path $repoRoot 'packaging\windows\licenses\FFmpeg-LGPL-NOTICE.txt') $licDir -Force -ErrorAction SilentlyContinue
|
|
foreach ($n in @('THIRD-PARTY-NOTICES.txt', 'LICENSE-MIT', 'LICENSE-APACHE')) {
|
|
$p = Join-Path $repoRoot $n
|
|
if (Test-Path $p) { Copy-Item $p $licDir -Force }
|
|
}
|
|
$ffRoot = Split-Path $FfmpegBin -Parent
|
|
foreach ($lic in @('LICENSE.txt', 'LICENSE', 'COPYING.LGPLv2.1', 'COPYING.LGPLv3', 'COPYING.txt')) {
|
|
$p = Join-Path $ffRoot $lic
|
|
if (Test-Path $p) { Copy-Item $p $licDir -Force }
|
|
}
|
|
|
|
# tile/store assets
|
|
Copy-Item (Join-Path $assets '*') (Join-Path $layout 'Assets') -Force
|
|
|
|
# manifest with version + publisher + architecture substituted
|
|
$manifest = (Get-Content -Raw $manifestTemplate).Replace('{VERSION}', $Version).Replace('{PUBLISHER}', $Publisher).Replace('{ARCH}', $Arch)
|
|
Set-Content -Path (Join-Path $layout 'AppxManifest.xml') -Value $manifest -Encoding UTF8
|
|
|
|
# --- resource index (resources.pri) ---
|
|
# The shell resolves the manifest's logo assets through MRT, so the qualified variants
|
|
# (Square44x44Logo.targetsize-*_altform-unplated.png — the alpha-transparent taskbar icons) only
|
|
# take effect if a pri indexes them; without one the taskbar falls back to plating the base
|
|
# 44x44 onto a solid square (the white-cornered icon). makepri's default config indexes the
|
|
# layout's asset files AND merges any existing .pri it finds (reactor's staged WinUI resources)
|
|
# via its PRI indexer, yielding one combined resources.pri. Output lands outside the layout
|
|
# first — the reactor pri is an input while indexing — then replaces it.
|
|
$makepri = Find-SdkTool 'makepri.exe'
|
|
$priconfig = Join-Path $OutDir 'priconfig.xml'
|
|
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
|
& $makepri createconfig /cf $priconfig /dq en-US /o
|
|
if ($LASTEXITCODE -ne 0) { throw "makepri createconfig failed ($LASTEXITCODE)" }
|
|
$priOut = Join-Path $OutDir 'resources.pri'
|
|
if (Test-Path $priOut) { Remove-Item $priOut -Force }
|
|
& $makepri new /pr $layout /cf $priconfig /mn (Join-Path $layout 'AppxManifest.xml') /of $priOut /o
|
|
if ($LASTEXITCODE -ne 0) { throw "makepri new failed ($LASTEXITCODE)" }
|
|
Move-Item $priOut (Join-Path $layout 'resources.pri') -Force
|
|
|
|
Write-Host "layout assembled at $layout :"
|
|
Get-ChildItem $layout -Recurse -File | ForEach-Object { " $($_.FullName.Substring($layout.Length + 1))" }
|
|
|
|
# --- pack ---
|
|
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
|
$msix = Join-Path $OutDir "punktfunk-client-windows_${Version}_${Arch}.msix"
|
|
& $makeappx pack /o /d $layout /p $msix
|
|
if ($LASTEXITCODE -ne 0) { throw "makeappx pack failed ($LASTEXITCODE)" }
|
|
|
|
# --- signing cert (supplied stable pfx OR ephemeral self-signed) ---
|
|
$pfxPath = Join-Path $OutDir 'signing.pfx'
|
|
$cerPath = Join-Path $OutDir "punktfunk-client-windows_${Version}_${Arch}.cer"
|
|
if ($PfxBase64) {
|
|
Write-Host "signing with supplied code-signing cert (MSIX_CERT_PFX_B64)"
|
|
[IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($PfxBase64))
|
|
} else {
|
|
Write-Host "no MSIX_CERT_PFX_B64 -> generating an ephemeral self-signed cert (subject $Publisher)"
|
|
if (-not $PfxPassword) { $PfxPassword = 'punktfunk' }
|
|
$tmp = New-SelfSignedCertificate -Type Custom -Subject $Publisher `
|
|
-KeyUsage DigitalSignature -FriendlyName 'punktfunk MSIX (self-signed)' `
|
|
-CertStoreLocation 'Cert:\CurrentUser\My' `
|
|
-TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3', '2.5.29.19={text}')
|
|
$sec = ConvertTo-SecureString -String $PfxPassword -Force -AsPlainText
|
|
Export-PfxCertificate -Cert "Cert:\CurrentUser\My\$($tmp.Thumbprint)" -FilePath $pfxPath -Password $sec | Out-Null
|
|
Remove-Item "Cert:\CurrentUser\My\$($tmp.Thumbprint)" -Force
|
|
}
|
|
|
|
# Always export the public .cer from the pfx. For a self-signed / private-trust cert it's the file
|
|
# users import once (Trusted People) — a STABLE cert (same pfx every build via the secret) means that
|
|
# import is a one-time, per-machine step that keeps working across upgrades. For a public-CA cert
|
|
# it's just an unused extra (harmless). The manifest Publisher must equal the cert's subject DN.
|
|
$pwsec = if ($PfxPassword) { ConvertTo-SecureString -String $PfxPassword -Force -AsPlainText } else { $null }
|
|
$pubCert = if ($pwsec) { Get-PfxCertificate -FilePath $pfxPath -Password $pwsec } else { Get-PfxCertificate -FilePath $pfxPath }
|
|
Export-Certificate -Cert $pubCert -FilePath $cerPath | Out-Null
|
|
Write-Host "signing cert subject=$($pubCert.Subject) thumbprint=$($pubCert.Thumbprint)"
|
|
if ($pubCert.Subject -ne $Publisher) {
|
|
Write-Warning "cert subject '$($pubCert.Subject)' != manifest Publisher '$Publisher' — Add-AppxPackage will reject the mismatch. Pass -Publisher '$($pubCert.Subject)'."
|
|
}
|
|
|
|
# --- sign (timestamp best-effort) ---
|
|
$signArgs = @('sign', '/fd', 'SHA256', '/f', $pfxPath)
|
|
if ($PfxPassword) { $signArgs += @('/p', $PfxPassword) }
|
|
& $signtool ($signArgs + @('/tr', 'http://timestamp.digicert.com', '/td', 'SHA256', $msix))
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Warning "timestamped sign failed — retrying without a timestamp"
|
|
& $signtool ($signArgs + @($msix))
|
|
if ($LASTEXITCODE -ne 0) { throw "signtool sign failed ($LASTEXITCODE)" }
|
|
}
|
|
Remove-Item $pfxPath -Force -ErrorAction SilentlyContinue
|
|
|
|
Write-Host ""
|
|
Write-Host "==> MSIX: $msix"
|
|
Write-Host "==> trust the cert once per machine (then it stays trusted across all future builds):"
|
|
Write-Host " Import-Certificate -FilePath '$cerPath' -CertStoreLocation Cert:\LocalMachine\TrustedPeople"
|
|
# emit paths for the workflow to publish (only under CI, where GITHUB_ENV is set)
|
|
if ($env:GITHUB_ENV) {
|
|
"MSIX_PATH=$msix" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
|
"MSIX_CER_PATH=$cerPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
|
}
|