feat(host/windows,packaging): installer overhaul - branding, VB-CABLE, GameStream choice, driver uninstall
ci / docs-site (push) Successful in 1m3s
android / android (push) Successful in 3m34s
decky / build-publish (push) Successful in 11s
apple / swift (push) Successful in 1m7s
ci / rust (push) Successful in 1m36s
ci / web (push) Successful in 49s
apple / screenshots (push) Successful in 5m20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
windows-host / package (push) Successful in 6m41s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m17s
ci / bench (push) Successful in 4m41s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m22s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m37s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m8s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m13s
docker / deploy-docs (push) Successful in 16s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m0s
deb / build-publish (push) Successful in 3m6s
- Modern branded wizard: WizardStyle=modern dynamic windows11 (Inno >= 6.6,
plain-modern fallback for older compilers; CI provisioning upgrades a
pre-6.6 Inno). Brand-mark wizard side panels + header tiles (100-200% DPI)
and a multi-size punktfunk.ico (SetupIconFile + Apps & Features), generated
AND committed by branding/gen-branding.ps1 from the canonical brand geometry.
Gotcha encoded in the script: ISCC rejects all-PNG icons, so entries <= 64px
are classic DIBs (PNG only at 128/256), and the ICO is load-verified.
- VB-CABLE actually ships now: windows-host.yml never set VBCABLE_DIR, so every
published installer silently omitted the virtual mic (broken mic passthrough
in the field). CI provisions the pinned, SHA-256-verified official Pack45
(provision-windows-punktfunk-extras.ps1) and the pack now FAILS on a
supplied-but-invalid dir instead of shipping mic-less again. Attribution per
VB-Audio's bundling grant surfaced in the visible wizard task text (vendor,
vb-cable.com, donationware) on top of the licenses notice.
- GameStream (Moonlight) compat is a wizard task (checked by default) ->
service install --gamestream=on|off writes PUNKTFUNK_HOST_CMD=
serve[ --gamestream] into host.env. Only the two canonical values are ever
rewritten - a hand-customized command line survives upgrades. Silent
installs: /MERGETASKS="!gamestream".
- Driver uninstall (field report: our virtual-device drivers survived
uninstall): new `driver uninstall [--gamepad]` removes the pf-vdisplay
device node(s) + the pf-vdisplay/pf-dualsense/pf-xusb driver-store packages,
wired into [UninstallRun] after service uninstall. Locale-safe by
construction: devices matched on unlocalized VALUES (never pnputil's
localized labels), packages found by INF content scan - validated against a
German-locale box ("Instanz-ID:" parse; 7/7 punktfunk INFs matched, no
foreign hits). VB-CABLE is deliberately left installed (shared third-party
component with its own uninstaller).
Installer compile, cargo check/clippy/fmt, and the ASCII locale gate are green;
the wizard look + uninstall flow still need one on-glass pass on a disposable
box (this box runs the live host).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -0,0 +1,235 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Generate the punktfunk host installer branding assets (wizard BMPs + setup .ico).
|
||||
|
||||
.DESCRIPTION
|
||||
Renders the punktfunk brand mark - the two overlapping circles ("lens") from
|
||||
web/src/components/brand-mark.tsx (the canonical flattened geometry, shared with the Apple icon,
|
||||
the marketing site and the docs) - into the assets Inno Setup consumes:
|
||||
|
||||
wizard-image-*.bmp welcome/finish page side panel (164x314 base, 100..200% DPI variants);
|
||||
dark violet gradient panel + the mark + the lowercase wordmark. The panel
|
||||
is self-contained dark, so it reads correctly in BOTH the light and dark
|
||||
(WizardStyle=dynamic) wizard appearances.
|
||||
wizard-small-*.bmp header tile on the inner pages (55x55 base, 100..200% DPI variants);
|
||||
the square brand tile (mark on #1C1530), matching the MSIX client tile.
|
||||
punktfunk.ico multi-size icon (16..256, PNG-compressed entries - Vista+ format, we
|
||||
require Windows 10) for SetupIconFile + the Apps & Features entry.
|
||||
|
||||
Outputs are COMMITTED next to this script (like include/punktfunk_core.h, generated-but-checked-in);
|
||||
re-run only when the brand changes. Everything is drawn 4x supersampled and downscaled
|
||||
(System.Drawing regions/clips do not antialias), so edges stay clean at every size.
|
||||
|
||||
.EXAMPLE
|
||||
pwsh -File packaging/windows/branding/gen-branding.ps1
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param([string]$OutDir = $PSScriptRoot)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
# --- brand constants (colors from brand-mark.tsx; tile background from the MSIX assets) -------
|
||||
$colLight = [System.Drawing.Color]::FromArgb(255, 0xA7, 0x9F, 0xF8) # large circle
|
||||
$colDeep = [System.Drawing.Color]::FromArgb(255, 0x6C, 0x5B, 0xF3) # small circle
|
||||
$colHi = [System.Drawing.Color]::FromArgb(255, 0xD2, 0xC9, 0xFB) # lens overlap highlight
|
||||
$colTile = [System.Drawing.Color]::FromArgb(255, 0x1C, 0x15, 0x30) # brand tile background
|
||||
$colPanelTop = [System.Drawing.Color]::FromArgb(255, 0x27, 0x1E, 0x46) # wizard panel gradient
|
||||
$colPanelBot = [System.Drawing.Color]::FromArgb(255, 0x11, 0x0D, 0x1F)
|
||||
$colText = [System.Drawing.Color]::FromArgb(255, 0xEA, 0xE6, 0xFB) # wordmark on the panel
|
||||
|
||||
# Mark geometry in the 1000-unit viewbox of brand-mark.tsx: two r=194.41 circles at (403.04,597.26)
|
||||
# (light, behind) and (597.81,402.85) (deep, in front), their intersection filled as the highlight.
|
||||
$R = 194.41
|
||||
$c1x = 403.037; $c1y = 597.262
|
||||
$c2x = 597.8075; $c2y = 402.8525
|
||||
# Mark bounding box -> center/span, so callers can place it by center + size.
|
||||
$bbMinX = $c1x - $R; $bbMaxX = $c2x + $R
|
||||
$bbMinY = $c2y - $R; $bbMaxY = $c1y + $R
|
||||
$markCx = ($bbMinX + $bbMaxX) / 2.0
|
||||
$markCy = ($bbMinY + $bbMaxY) / 2.0
|
||||
$markSpan = $bbMaxX - $bbMinX # == $bbMaxY - $bbMinY (the bbox is square)
|
||||
|
||||
# Draw the mark onto $g centered at ($cx,$cy) with bounding-box size $size (device pixels).
|
||||
function Draw-Mark([System.Drawing.Graphics]$g, [double]$cx, [double]$cy, [double]$size) {
|
||||
$s = $size / $markSpan
|
||||
function ellRect([double]$ecx, [double]$ecy) {
|
||||
$r = $R * $s
|
||||
[System.Drawing.RectangleF]::new(
|
||||
[float]($cx + ($ecx - $markCx) * $s - $r), [float]($cy + ($ecy - $markCy) * $s - $r),
|
||||
[float](2 * $r), [float](2 * $r))
|
||||
}
|
||||
$r1 = ellRect $c1x $c1y
|
||||
$r2 = ellRect $c2x $c2y
|
||||
$b = New-Object System.Drawing.SolidBrush($colLight)
|
||||
$g.FillEllipse($b, $r1); $b.Dispose()
|
||||
$b = New-Object System.Drawing.SolidBrush($colDeep)
|
||||
$g.FillEllipse($b, $r2); $b.Dispose()
|
||||
# Highlight = intersection: clip to circle 1, fill circle 2. The clip edge is not antialiased,
|
||||
# but every caller renders 4x supersampled and downscales, which smooths it.
|
||||
$p1 = New-Object System.Drawing.Drawing2D.GraphicsPath
|
||||
$p1.AddEllipse($r1)
|
||||
$g.SetClip($p1)
|
||||
$b = New-Object System.Drawing.SolidBrush($colHi)
|
||||
$g.FillEllipse($b, $r2); $b.Dispose()
|
||||
$g.ResetClip(); $p1.Dispose()
|
||||
}
|
||||
|
||||
# New 32bpp canvas + antialiased Graphics.
|
||||
function New-Canvas([int]$w, [int]$h) {
|
||||
$bmp = New-Object System.Drawing.Bitmap($w, $h, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
|
||||
$g.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAlias
|
||||
@($bmp, $g)
|
||||
}
|
||||
|
||||
# Downscale $src to $w x $h (high-quality bicubic) - the supersample resolve.
|
||||
function Resize-Bitmap([System.Drawing.Bitmap]$src, [int]$w, [int]$h) {
|
||||
$dst = New-Object System.Drawing.Bitmap($w, $h, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||
$g = [System.Drawing.Graphics]::FromImage($dst)
|
||||
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
||||
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
||||
$g.DrawImage($src, (New-Object System.Drawing.Rectangle(0, 0, $w, $h)),
|
||||
0, 0, $src.Width, $src.Height, [System.Drawing.GraphicsUnit]::Pixel)
|
||||
$g.Dispose()
|
||||
$dst
|
||||
}
|
||||
|
||||
# Save as 24bpp BMP (opaque - what Inno's wizard image loader expects by default).
|
||||
function Save-Bmp24([System.Drawing.Bitmap]$bmp, [string]$path) {
|
||||
$b24 = $bmp.Clone((New-Object System.Drawing.Rectangle(0, 0, $bmp.Width, $bmp.Height)),
|
||||
[System.Drawing.Imaging.PixelFormat]::Format24bppRgb)
|
||||
$b24.Save($path, [System.Drawing.Imaging.ImageFormat]::Bmp)
|
||||
$b24.Dispose()
|
||||
Write-Host " wrote $path ($($bmp.Width)x$($bmp.Height))"
|
||||
}
|
||||
|
||||
$SS = 4 # supersample factor
|
||||
|
||||
# --- wizard side panel (welcome/finish page): gradient + mark + wordmark ----------------------
|
||||
# Base size 164x314 (Inno's classic canvas); DPI variants via the wizard-image-*.bmp wildcard.
|
||||
foreach ($pct in 100, 125, 150, 175, 200) {
|
||||
$w = [int][Math]::Round(164 * $pct / 100.0)
|
||||
$h = [int][Math]::Round(314 * $pct / 100.0)
|
||||
$bmp, $g = New-Canvas ($w * $SS) ($h * $SS)
|
||||
$rect = New-Object System.Drawing.Rectangle(0, 0, ($w * $SS), ($h * $SS))
|
||||
$grad = New-Object System.Drawing.Drawing2D.LinearGradientBrush($rect, $colPanelTop, $colPanelBot, 90.0)
|
||||
$g.FillRectangle($grad, $rect); $grad.Dispose()
|
||||
# Mark: 58% of the panel width, centered horizontally, optical center at ~40% height.
|
||||
Draw-Mark $g ($w * $SS / 2.0) ($h * $SS * 0.40) ($w * $SS * 0.58)
|
||||
# Wordmark: lowercase brand name under the mark.
|
||||
$font = New-Object System.Drawing.Font('Segoe UI Semibold', [float](13.0 * $SS * $pct / 100.0), [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Pixel)
|
||||
$tb = New-Object System.Drawing.SolidBrush($colText)
|
||||
$fmt = New-Object System.Drawing.StringFormat
|
||||
$fmt.Alignment = [System.Drawing.StringAlignment]::Center
|
||||
$g.DrawString('punktfunk', $font, $tb,
|
||||
(New-Object System.Drawing.PointF([float]($w * $SS / 2.0), [float]($h * $SS * 0.60))), $fmt)
|
||||
$fmt.Dispose(); $tb.Dispose(); $font.Dispose(); $g.Dispose()
|
||||
$out = Resize-Bitmap $bmp $w $h
|
||||
$bmp.Dispose()
|
||||
Save-Bmp24 $out (Join-Path $OutDir ("wizard-image-{0}.bmp" -f $pct))
|
||||
$out.Dispose()
|
||||
}
|
||||
|
||||
# --- wizard header tile (inner pages): the square brand tile --------------------------------
|
||||
# Base size 55x55; DPI variants via the wizard-small-*.bmp wildcard. Opaque square (BMP has no
|
||||
# alpha here): the same full-bleed dark tile as the client's MSIX logo assets.
|
||||
foreach ($pct in 100, 125, 150, 175, 200) {
|
||||
$sz = [int][Math]::Round(55 * $pct / 100.0)
|
||||
$bmp, $g = New-Canvas ($sz * $SS) ($sz * $SS)
|
||||
$b = New-Object System.Drawing.SolidBrush($colTile)
|
||||
$g.FillRectangle($b, 0, 0, ($sz * $SS), ($sz * $SS)); $b.Dispose()
|
||||
Draw-Mark $g ($sz * $SS / 2.0) ($sz * $SS / 2.0) ($sz * $SS * 0.74)
|
||||
$g.Dispose()
|
||||
$out = Resize-Bitmap $bmp $sz $sz
|
||||
$bmp.Dispose()
|
||||
Save-Bmp24 $out (Join-Path $OutDir ("wizard-small-{0}.bmp" -f $pct))
|
||||
$out.Dispose()
|
||||
}
|
||||
|
||||
# --- punktfunk.ico: rounded brand tile at 16..256 --------------------------------------------
|
||||
# Small sizes are classic 32bpp DIB entries (Inno's SetupIconFile resource updater and older shell
|
||||
# consumers reject an all-PNG icon); only 128/256 use PNG compression (the standard Vista+ layout).
|
||||
function New-IconTile([int]$sz) {
|
||||
$bmp, $g = New-Canvas ($sz * $SS) ($sz * $SS)
|
||||
# Rounded-rect tile (22% corner radius - the Windows 11 app-icon look).
|
||||
$S = $sz * $SS; $rad = [int]($S * 0.22)
|
||||
$path = New-Object System.Drawing.Drawing2D.GraphicsPath
|
||||
$path.AddArc(0, 0, 2 * $rad, 2 * $rad, 180, 90)
|
||||
$path.AddArc($S - 2 * $rad, 0, 2 * $rad, 2 * $rad, 270, 90)
|
||||
$path.AddArc($S - 2 * $rad, $S - 2 * $rad, 2 * $rad, 2 * $rad, 0, 90)
|
||||
$path.AddArc(0, $S - 2 * $rad, 2 * $rad, 2 * $rad, 90, 90)
|
||||
$path.CloseFigure()
|
||||
$b = New-Object System.Drawing.SolidBrush($colTile)
|
||||
$g.FillPath($b, $path); $b.Dispose(); $path.Dispose()
|
||||
Draw-Mark $g ($S / 2.0) ($S / 2.0) ($S * 0.74)
|
||||
$g.Dispose()
|
||||
$out = Resize-Bitmap $bmp $sz $sz
|
||||
$bmp.Dispose()
|
||||
$out
|
||||
}
|
||||
|
||||
# PNG-compressed entry payload (used for the 128/256 entries). The leading comma keeps the byte[]
|
||||
# a single pipeline object (PowerShell would otherwise unroll it into individual bytes).
|
||||
function ConvertTo-IconPng([System.Drawing.Bitmap]$tile) {
|
||||
$ms = New-Object System.IO.MemoryStream
|
||||
$tile.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
return , $ms.ToArray()
|
||||
}
|
||||
|
||||
# Classic ICO DIB entry payload: BITMAPINFOHEADER (height doubled) + bottom-up 32bpp BGRA XOR data
|
||||
# + an all-zero 1bpp AND mask (32bpp icons carry transparency in the alpha channel).
|
||||
function ConvertTo-IconDib([System.Drawing.Bitmap]$tile) {
|
||||
$s = $tile.Width
|
||||
$rect = New-Object System.Drawing.Rectangle(0, 0, $s, $s)
|
||||
$data = $tile.LockBits($rect, [System.Drawing.Imaging.ImageLockMode]::ReadOnly,
|
||||
[System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||
$px = New-Object byte[] ($data.Stride * $s)
|
||||
[System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $px, 0, $px.Length)
|
||||
$tile.UnlockBits($data)
|
||||
$maskStride = [int][Math]::Ceiling($s / 32.0) * 4 # 1bpp rows padded to 32 bits
|
||||
$ms = New-Object System.IO.MemoryStream
|
||||
$w = New-Object System.IO.BinaryWriter($ms)
|
||||
$w.Write([uint32]40); $w.Write([int]$s); $w.Write([int]($s * 2)) # biSize, biWidth, biHeight (XOR+AND)
|
||||
$w.Write([uint16]1); $w.Write([uint16]32); $w.Write([uint32]0) # biPlanes, biBitCount, BI_RGB
|
||||
$w.Write([uint32]($s * $s * 4 + $maskStride * $s)) # biSizeImage
|
||||
$w.Write([int]0); $w.Write([int]0); $w.Write([uint32]0); $w.Write([uint32]0)
|
||||
for ($y = $s - 1; $y -ge 0; $y--) { $w.Write($px, $y * $data.Stride, $s * 4) } # XOR, bottom-up
|
||||
$w.Write((New-Object byte[] ($maskStride * $s))) # AND mask: all opaque
|
||||
$w.Flush()
|
||||
$bytes = $ms.ToArray()
|
||||
$w.Dispose(); $ms.Dispose()
|
||||
return , $bytes # leading comma: emit the byte[] as ONE object, not unrolled bytes
|
||||
}
|
||||
|
||||
$icoSizes = 16, 20, 24, 32, 40, 48, 64, 128, 256
|
||||
$pngs = @(foreach ($s in $icoSizes) {
|
||||
$tile = New-IconTile $s
|
||||
if ($s -ge 128) { ConvertTo-IconPng $tile } else { ConvertTo-IconDib $tile }
|
||||
$tile.Dispose()
|
||||
})
|
||||
$ico = New-Object System.IO.MemoryStream
|
||||
$bw = New-Object System.IO.BinaryWriter($ico)
|
||||
# ICONDIR
|
||||
$bw.Write([uint16]0); $bw.Write([uint16]1); $bw.Write([uint16]$icoSizes.Count)
|
||||
# ICONDIRENTRYs (width/height byte 0 means 256)
|
||||
$offset = 6 + 16 * $icoSizes.Count
|
||||
for ($i = 0; $i -lt $icoSizes.Count; $i++) {
|
||||
$s = $icoSizes[$i]
|
||||
$dim = if ($s -ge 256) { 0 } else { $s }
|
||||
$bw.Write([byte]$dim); $bw.Write([byte]$dim) # width, height
|
||||
$bw.Write([byte]0); $bw.Write([byte]0) # colors, reserved
|
||||
$bw.Write([uint16]1); $bw.Write([uint16]32) # planes, bitcount
|
||||
$bw.Write([uint32]$pngs[$i].Length); $bw.Write([uint32]$offset)
|
||||
$offset += $pngs[$i].Length
|
||||
}
|
||||
foreach ($p in $pngs) { $bw.Write([byte[]]$p) }
|
||||
$bw.Flush()
|
||||
$icoPath = Join-Path $OutDir 'punktfunk.ico'
|
||||
[IO.File]::WriteAllBytes($icoPath, $ico.ToArray())
|
||||
$bw.Dispose(); $ico.Dispose()
|
||||
# Self-check: a malformed container here would only surface later as ISCC "Icon file is invalid".
|
||||
$probe = New-Object System.Drawing.Icon($icoPath)
|
||||
$probe.Dispose()
|
||||
Write-Host " wrote $icoPath ($($icoSizes -join ',')) - verified loadable"
|
||||
Write-Host "==> branding assets generated in $OutDir"
|
||||
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 340 KiB |
|
After Width: | Height: | Size: 464 KiB |
|
After Width: | Height: | Size: 604 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 36 KiB |