diff --git a/Tools/windows/Invoke-UiCapture.ps1 b/Tools/windows/Invoke-UiCapture.ps1 index f412bd6b..337f0a8f 100644 --- a/Tools/windows/Invoke-UiCapture.ps1 +++ b/Tools/windows/Invoke-UiCapture.ps1 @@ -64,6 +64,7 @@ $lease = $null $job = $null $preflight = $null $supervisorIdentity = $null +$dpiGeometryAdmission = $null $thisProcess = [Diagnostics.Process]::GetCurrentProcess() $supervisorExecutable = $thisProcess.MainModule.FileName $startedUtc = [DateTime]::UtcNow @@ -78,6 +79,16 @@ function Write-CapSupervisorLog { Write-Output ('{0:o} {1}' -f [DateTime]::UtcNow, $Message) } +function Test-CapMatchedAdapter { + return $job.PSObject.Properties['sceneAdapter'] -and + $job.sceneAdapter -cin @('S_MATCHED_V12_V1','S_MATCHED_V12_DPI_V1') +} + +function Test-CapMatchedDpiAdapter { + return $job.PSObject.Properties['sceneAdapter'] -and + $job.sceneAdapter -ceq 'S_MATCHED_V12_DPI_V1' +} + function Assert-CapSupervisorPath { param([Parameter(Mandatory)][string]$Path) if (Get-Command Write-CapTrace -ErrorAction SilentlyContinue) { Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters } @@ -145,7 +156,8 @@ function Assert-CapGreenJob { before = @((Join-Path $root 'inputs\graphcode-before.exe'), $job.binaries.before.sha256) after = @((Join-Path $root 'inputs\graphcode-after.exe'), $job.binaries.after.sha256) } - if ($job.sceneAdapter -ceq 'S_MATCHED_V12_V1') { + if ($job.PSObject.Properties['sceneAdapter'] -and + $job.sceneAdapter -cin @('S_MATCHED_V12_V1','S_MATCHED_V12_DPI_V1')) { $inputHashes.daemon=@((Join-Path $root 'runtime\graphcoded.exe'),$job.binaries.daemon.sha256) $inputHashes.backendFixture=@((Join-Path $root 'runtime\copilot.exe'),$job.binaries.backendFixture.sha256) $inputHashes.backendSource=@((Join-Path $root 'inputs\offline-backend.c'),$job.binaries.backendFixture.sourceSha256) @@ -256,9 +268,12 @@ function Assert-CapPreflightReport { throw 'Real seed preflight case does not bind the current pinned provider.' } } - if ($job.PSObject.Properties['sceneAdapter'] -and $job.sceneAdapter -ceq 'S_MATCHED_V12_V1') { - if ($preflight.executed -ne 180 -or $preflight.skipped -isnot [long] -or $preflight.skipped -ne 0) { - throw 'Matched scene preflight requires exactly 180 passing current contracts and zero skips.' + if ($job.PSObject.Properties['sceneAdapter'] -and + $job.sceneAdapter -cin @('S_MATCHED_V12_V1','S_MATCHED_V12_DPI_V1')) { + $expectedMatchedPreflight = 184L + if ($preflight.executed -ne $expectedMatchedPreflight -or + $preflight.skipped -isnot [long] -or $preflight.skipped -ne 0) { + throw "Matched scene preflight requires exactly $expectedMatchedPreflight passing current contracts and zero skips." } foreach ($name in @( 'matched v12 fixture binds exact node type wiring reason and outcome fields', @@ -274,6 +289,16 @@ function Assert-CapPreflightReport { )) { if (@($preflight.cases | Where-Object name -CEQ $name).Count -ne 1) {throw "Matched scene preflight lacks its unique group: $name"} } + foreach ($name in @( + 'DPI matched geometry derives exact 96 and 192 physical frames', + 'DPI matched geometry refuses insufficient work area and invalid scaling', + 'DPI matched logical zoom normalizes physical UIA width', + 'DPI matched manifest preserves raw logical physical metadata without resampling' + )) { + if (@($preflight.cases | Where-Object name -CEQ $name).Count -ne 1) { + throw "DPI matched preflight lacks its unique group: $name" + } + } } return } @@ -690,6 +715,14 @@ function Assert-CapMatchedWorkerManifest { foreach ($field in @('cleanupFailures','ownedProcessSurvivors','pendingLaunchesRemaining')) { if ($manifest.$field -isnot [array] -or $manifest.$field.Count -ne 0) {throw "Matched manifest has unproved $field."} } + $isDpi=Test-CapMatchedDpiAdapter + if ($isDpi -and ($manifest.evidenceClassification -cne 'C8R_RDP_DPI192_QUALIFIED' -or + $manifest.geometrySchemaVersion -ne 2 -or + ($manifest.logicalClientWH -join ',') -cne ($job.captureContract.logicalClientWH -join ',') -or + $manifest.expectedWindowDpi -ne $job.captureContract.expectedWindowDpi -or + $manifest.rawCaptureResamplingAllowed -cne $false)) { + throw 'DPI-parametric matched manifest classification or geometry contract differs.' + } foreach ($image in $manifest.images) { $binary=$job.binaries.($image.phase) if ($image.sceneAdapter -cne $job.sceneAdapter -or $image.sourceHead -cne $binary.source -or @@ -697,10 +730,15 @@ function Assert-CapMatchedWorkerManifest { $image.fixtureSha256 -ne $fixtureHashExpected -or $image.fixtureSourceSha256 -ne $job.fixture.sha256 -or $image.fixtureMappingSha256 -ne $fixtureMappingHashExpected -or $image.privacyStatus -cne 'CAPTURED_PENDING_INDIVIDUAL_REVIEW' -or - ($image.clientBounds -join ',') -cne '0,0,1440,900' -or $image.dpi -ne 96 -or $image.sampledColors -lt 4 -or $image.nonMagentaSamples -lt 20 -or $image.width -ne ($image.outerBounds[2]-$image.outerBounds[0]) -or $image.height -ne ($image.outerBounds[3]-$image.outerBounds[1])) {throw 'Matched image geometry, state or provenance differs.'} + if ($isDpi) { + $contract=$job.captureContract + $null=Assert-CapMatchedDpiImage -Image $image -Contract $contract + } elseif (($image.clientBounds -join ',') -cne '0,0,1440,900' -or $image.dpi -ne 96) { + throw 'Matched V1 image geometry differs.' + } if ($image.scene -cmatch '^s-(?:goal-unwired|main-cycle|main-stopped|goal-done)' -and ($null -eq $image.zoom -or [Math]::Abs($image.zoom.ZoomPercent-100) -gt 1)) {throw 'Matched card scene lacks actual-size evidence.'} $path=Assert-CapSupervisorPath $image.path @@ -764,7 +802,7 @@ function Assert-CapWorkerManifest { } return } - if ($job.sceneAdapter -ceq 'S_MATCHED_V12_V1') { + if (Test-CapMatchedAdapter) { Assert-CapMatchedWorkerManifest return } @@ -996,6 +1034,21 @@ try { if (-not $rehearsal) { $native = @(Assert-CapLiveNativeBindings) } + if (-not $rehearsal -and (Test-CapMatchedDpiAdapter)) { + Add-Type -AssemblyName System.Windows.Forms + $area=[Windows.Forms.Screen]::PrimaryScreen.WorkingArea + $contract=$job.captureContract + $workArea=@($area.Left,$area.Top,$area.Right,$area.Bottom) + if (($area.Width -lt $contract.minimumPhysicalWorkAreaWH[0]) -or + ($area.Height -lt $contract.minimumPhysicalWorkAreaWH[1])) { + throw "DPI-parametric physical work area is insufficient: $($area.Width)x$($area.Height)." + } + $dpiGeometryAdmission=Get-CapDpiGeometry -LogicalClientWH @($contract.logicalClientWH) ` + -ExpectedDpi ([uint32]$contract.expectedWindowDpi) ` + -WindowStyle ([long]$contract.windowStyleForAdjustment) ` + -WindowExStyle ([long]$contract.windowExStyle) -HasMenu ([bool]$contract.hasMenu) ` + -WorkArea $workArea -ExpectedAdjustedOuterWH @($contract.expectedAdjustedOuterWH) + } if (@(Get-CapPendingLaunches).Count -ne 0) { throw 'Unreturned capture worker launches remain before run authorization.' } $shortLease = $lease.leaseId.Replace('-', '').Substring(0, 8) @@ -1048,6 +1101,7 @@ try { fixtureMappingSha256 = $fixtureMappingHashExpected sourceRoot = $sourceRoot jobIdentity = $jobIdentity + dpiGeometryAdmission = $dpiGeometryAdmission runDirectory = $runDirectory outputDirectory = $outputDirectory processJournalPath = $processJournalPath diff --git a/Tools/windows/Invoke-UiCaptureWorker.ps1 b/Tools/windows/Invoke-UiCaptureWorker.ps1 index 4924b19c..966b77c7 100644 --- a/Tools/windows/Invoke-UiCaptureWorker.ps1 +++ b/Tools/windows/Invoke-UiCaptureWorker.ps1 @@ -63,6 +63,16 @@ function Write-CapWorkerLog { if ($workerLog) { [IO.File]::AppendAllText($workerLog, $line + [Environment]::NewLine, [Text.UTF8Encoding]::new($false)) } } +function Test-CapMatchedAdapter { + return $job.PSObject.Properties['sceneAdapter'] -and + $job.sceneAdapter -cin @('S_MATCHED_V12_V1','S_MATCHED_V12_DPI_V1') +} + +function Test-CapMatchedDpiAdapter { + return $job.PSObject.Properties['sceneAdapter'] -and + $job.sceneAdapter -ceq 'S_MATCHED_V12_DPI_V1' +} + function Write-CapJsonAtomic { param([Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)]$Value) Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters @@ -237,7 +247,7 @@ function Assert-CapAcceptedInputs { zmx = (Join-Path $root 'runtime\zmx.exe'); before = (Join-Path $root 'inputs\graphcode-before.exe') after = (Join-Path $root 'inputs\graphcode-after.exe') } - if ($job.sceneAdapter -ceq 'S_MATCHED_V12_V1') { + if (Test-CapMatchedAdapter) { foreach ($item in @(@('daemon','graphcoded.exe'),@('backendFixture','copilot.exe'))) { $expectedHashes[$item[0]]=$job.binaries.($item[0]).sha256 $paths[$item[0]]=Join-Path $root "runtime\$($item[1])" @@ -315,7 +325,7 @@ function Get-CapBaseEnvironment { ) Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters - if ($job.PSObject.Properties['sceneAdapter'] -and $job.sceneAdapter -ceq 'S_MATCHED_V12_V1') { + if (Test-CapMatchedAdapter) { $project=Assert-CapOwnedPath (Join-Path $root 'fixtures\projects\Matched') $null=New-Item -ItemType Directory -Path $project -Force return @{ @@ -410,7 +420,7 @@ function Start-CapOfflineSeed { Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters $isD=$job.sceneAdapter -ceq 'D_QUICK_CHAT_V1' - $isS=$job.sceneAdapter -ceq 'S_MATCHED_V12_V1' + $isS=Test-CapMatchedAdapter $arguments=if ($isS) { $fixture=Get-Content -LiteralPath $fixturePath -Raw | ConvertFrom-Json -DateKind String $null=Assert-CapMatchedFixture -Fixture $fixture -ProjectRoot (Join-Path $root 'fixtures\projects') @@ -625,7 +635,11 @@ function Measure-CapSceneZoom { $automationIdPrefix = $probeMatch.Groups['AutomationIdPrefix'].Value $element = Wait-CapVisibleElement -Window $AppRun.Window -ProcessIdentity $AppRun.Run.Identity ` -Name $name -AutomationIdPrefix $automationIdPrefix - return Measure-CapZoom -Element $element -NaturalWidth $NaturalWidth + $dpi=if ($AppRun.PSObject.Properties['Placement'] -and + $AppRun.Placement.PSObject.Properties['ActualDpi']) { + [uint32]$AppRun.Placement.ActualDpi + } else {96} + return Measure-CapZoom -Element $element -NaturalWidth $NaturalWidth -Dpi $dpi } function Save-CapSceneImage { @@ -641,10 +655,18 @@ function Save-CapSceneImage { $metrics = Get-CapWindowMetrics -Window $AppRun.Window -ProcessIdentity $AppRun.Run.Identity $expectedOuter = @(30, 30, 1486, 989) $isD=$job.PSObject.Properties['sceneAdapter'] -and $job.sceneAdapter -ceq 'D_QUICK_CHAT_V1' + $isSDpi=Test-CapMatchedDpiAdapter if ($isD) {$expectedOuter=@($AppRun.Placement.outerBounds)} - $height=if ($isD) {952} else {900} - if (($expectedOuter -join ',') -cne (@($metrics.OuterBounds) -join ',') -or $metrics.Dpi -ne 96 -or - ($metrics.ClientBounds -join ',') -cne "0,0,1440,$height") { + if ($isSDpi) {$expectedOuter=@($AppRun.Placement.OuterBounds)} + $expectedDpi=if ($isSDpi) {$AppRun.Placement.ActualDpi} else {96} + $expectedClient=if ($isSDpi) { + "0,0,$($AppRun.Placement.PhysicalClientWH[0]),$($AppRun.Placement.PhysicalClientWH[1])" + } else { + $height=if ($isD) {952} else {900} + "0,0,1440,$height" + } + if (($expectedOuter -join ',') -cne (@($metrics.OuterBounds) -join ',') -or + $metrics.Dpi -ne $expectedDpi -or ($metrics.ClientBounds -join ',') -cne $expectedClient) { throw "Unexpected window geometry for ${Phase}/${SceneName}: $(ConvertTo-Json -InputObject $metrics -Compress)." } if ($ZoomEvidence -and [Math]::Abs([double]$ZoomEvidence.ZoomPercent - 100.0) -gt 1.0) { @@ -693,11 +715,35 @@ function Save-CapSceneImage { $record | Add-Member -NotePropertyName sceneLabel -NotePropertyValue $(if ($SceneName -ceq 'd-chat-history-roles') {'Observed production-restored history chat Shell state; actual baseline UIA role recorded'} else {$SceneName}) $AppRun.DActions.Clear() } - if ($job.PSObject.Properties['sceneAdapter'] -and $job.sceneAdapter -ceq 'S_MATCHED_V12_V1') { + if (Test-CapMatchedAdapter) { $record | Add-Member -NotePropertyName sceneAdapter -NotePropertyValue $job.sceneAdapter $record | Add-Member -NotePropertyName observedState -NotePropertyValue $AppRun.SObservation $record | Add-Member -NotePropertyName actions -NotePropertyValue @($AppRun.SActions) $record | Add-Member -NotePropertyName captureUtc -NotePropertyValue ([DateTime]::UtcNow.ToString('o')) + if ($isSDpi) { + $geometry=$AppRun.Placement + $record | Add-Member -NotePropertyMembers @{ + evidenceClassification='C8R_RDP_DPI192_QUALIFIED' + geometrySchemaVersion=2L + geometryDerivation=$geometry.GeometryDerivation + logicalClientWH=@($geometry.LogicalClientWH) + physicalClientBounds=@($metrics.ClientBounds) + physicalOuterBounds=@($metrics.OuterBounds) + nonClientWH=@($geometry.NonClientWH) + rawPixelWH=@([long]$capture.Width,[long]$capture.Height) + actualWindowDpi=[long]$metrics.Dpi + dpiBase=[long]$geometry.DpiBase + dpiScaleNumerator=[long]$geometry.DpiScaleNumerator + dpiScaleDenominator=[long]$geometry.DpiScaleDenominator + windowStyle=[long]$metrics.Style + windowStyleForAdjustment=[long]$geometry.WindowStyle + windowExStyle=[long]$metrics.ExStyle + hasMenu=[bool]$metrics.HasMenu + physicalWorkAreaBounds=@($geometry.PhysicalWorkAreaBounds) + placement=$geometry.Placement + resampled=$false + } + } $AppRun.SActions.Clear() } Write-CapWorkerJournalSnapshot -Kind 'Progress' @@ -749,10 +795,17 @@ function New-CapManifest { @{name='missing-bound-chat-workspace-tabs';status='NotExecuted';reason='Production events route away; component coverage only'}, @{name='saved-Agent-Split-role-labels';status='NotExecuted';reason='Production listing prunes absent panes; only two chat seeds authorized'}) } - if ($job.PSObject.Properties['sceneAdapter'] -and $job.sceneAdapter -ceq 'S_MATCHED_V12_V1') { + if (Test-CapMatchedAdapter) { $result.sceneAdapter=$job.sceneAdapter;$result.lane='S';$result.requiredSceneNames=@(Get-CapMatchedSceneNames) $result.g11Qualification='ASSESSABLE_SOURCE_CAPABILITY_NOT_RUNTIME_VALIDATED' $result.backendTurns=0;$result.realRestarts=@($realRestarts) + if (Test-CapMatchedDpiAdapter) { + $result.evidenceClassification='C8R_RDP_DPI192_QUALIFIED' + $result.geometrySchemaVersion=2L + $result.logicalClientWH=@($job.captureContract.logicalClientWH) + $result.expectedWindowDpi=[long]$job.captureContract.expectedWindowDpi + $result.rawCaptureResamplingAllowed=$false + } } return $result } @@ -803,7 +856,7 @@ function Start-CapGraphCodeApp { $arguments = if ($rehearsal) { [string[]]@($job.rehearsalTarget.arguments) } else { [string[]]@() } if ($rehearsal) { $role = 'rehearsal-target' } $isD=$job.PSObject.Properties['sceneAdapter'] -and $job.sceneAdapter -ceq 'D_QUICK_CHAT_V1' - $isS=$job.PSObject.Properties['sceneAdapter'] -and $job.sceneAdapter -ceq 'S_MATCHED_V12_V1' + $isS=Test-CapMatchedAdapter $workingDirectory = if ($rehearsal) {$Roots.Peer} elseif ($isS) {Join-Path $root 'fixtures\projects\Matched'} elseif ($isD) {Join-Path $root 'fixtures\projects\D Offline Capture Fixture'} else {Join-Path $root 'fixtures\projects\Alpha'} if (-not $rehearsal) { $shellSession = 'graphcode-' + $SessionPrefix + '-' + [guid]::Empty.ToString('D') @@ -853,10 +906,44 @@ function Start-CapGraphCodeApp { Set-CapWindowPosition -Window $window -ProcessIdentity $run.Identity -X $placement.x -Y $placement.y -Width $placement.width -Height $placement.height $appRun | Add-Member -NotePropertyName Placement -NotePropertyValue $placement $appRun | Add-Member -NotePropertyName Roots -NotePropertyValue $Roots + } elseif (Test-CapMatchedDpiAdapter) { + Add-Type -AssemblyName System.Windows.Forms + $area=[Windows.Forms.Screen]::FromHandle($window).WorkingArea + $beforeMetrics=Get-CapWindowMetrics -Window $window -ProcessIdentity $run.Identity + $contract=$job.captureContract + if ($beforeMetrics.Dpi -ne $contract.expectedWindowDpi -or + $beforeMetrics.Style -ne $contract.expectedRuntimeWindowStyle -or + $beforeMetrics.ExStyle -ne $contract.windowExStyle -or + $beforeMetrics.HasMenu -ne $contract.hasMenu) { + throw "DPI-parametric runtime window identity differs: $(ConvertTo-Json $beforeMetrics -Compress)." + } + $geometry=Get-CapDpiGeometry -LogicalClientWH @($contract.logicalClientWH) ` + -ExpectedDpi ([uint32]$contract.expectedWindowDpi) ` + -WindowStyle ([long]$contract.windowStyleForAdjustment) ` + -WindowExStyle ([long]$beforeMetrics.ExStyle) -HasMenu ([bool]$beforeMetrics.HasMenu) ` + -WorkArea @($area.Left,$area.Top,$area.Right,$area.Bottom) ` + -ExpectedAdjustedOuterWH @($contract.expectedAdjustedOuterWH) + Set-CapWindowPosition -Window $window -ProcessIdentity $run.Identity ` + -X $geometry.OuterBounds[0] -Y $geometry.OuterBounds[1] ` + -Width $geometry.OuterWH[0] -Height $geometry.OuterWH[1] + $appRun | Add-Member -NotePropertyName Placement -NotePropertyValue $geometry + $appRun | Add-Member -NotePropertyName Roots -NotePropertyValue $Roots } else {Set-CapWindowPosition -Window $window -ProcessIdentity $run.Identity} Start-Sleep -Milliseconds 500 $metrics = Get-CapWindowMetrics -Window $window -ProcessIdentity $run.Identity - if ($metrics.Dpi -ne 96) { throw "Actual app DPI is $($metrics.Dpi), expected 96." } + if (Test-CapMatchedDpiAdapter) { + $geometry=$appRun.Placement + if ($metrics.Dpi -ne $geometry.ActualDpi -or + ($metrics.ClientBounds -join ',') -cne "0,0,$($geometry.PhysicalClientWH[0]),$($geometry.PhysicalClientWH[1])" -or + ($metrics.OuterBounds -join ',') -cne ($geometry.OuterBounds -join ',') -or + $metrics.Style -ne $job.captureContract.expectedRuntimeWindowStyle -or + $metrics.ExStyle -ne $geometry.WindowExStyle -or + $metrics.HasMenu -ne $geometry.HasMenu) { + throw "DPI-parametric window remeasurement differs: $(ConvertTo-Json $metrics -Compress)." + } + } elseif ($metrics.Dpi -ne 96) { + throw "Actual app DPI is $($metrics.Dpi), expected 96." + } return $appRun } @@ -1966,7 +2053,7 @@ try { Invoke-CapRealSeedRehearsal Invoke-CapRealLaunchRehearsal } else { - if ($job.sceneAdapter -ceq 'S_MATCHED_V12_V1') { + if (Test-CapMatchedAdapter) { foreach ($name in @('GH_TOKEN','GITHUB_TOKEN','COPILOT_GITHUB_TOKEN','COPILOT_API_KEY','ANTHROPIC_API_KEY', 'OPENAI_API_KEY','AZURE_OPENAI_API_KEY','CODEX_API_KEY','SSH_AUTH_SOCK','SSH_AGENT_PID', 'AWS_ACCESS_KEY_ID','AWS_SECRET_ACCESS_KEY','AWS_SESSION_TOKEN')) { @@ -1977,12 +2064,12 @@ try { } foreach ($phase in @('before', 'after')) { Assert-CapWorkerDeadline "starting phase $phase" - if ($job.sceneAdapter -ceq 'S_MATCHED_V12_V1') {Invoke-CapMatchedScenes -Phase $phase} + if (Test-CapMatchedAdapter) {Invoke-CapMatchedScenes -Phase $phase} elseif ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {Invoke-CapDScenes -Phase $phase} else {Invoke-CapNormalScenes -Phase $phase;Invoke-CapActionsScene -Phase $phase} } - $expectedImages=if ($job.sceneAdapter -ceq 'S_MATCHED_V12_V1') {36} elseif ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {20} else {10} - $expectedSeeds=if ($job.sceneAdapter -ceq 'S_MATCHED_V12_V1') {22} elseif ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {4} else {2} + $expectedImages=if (Test-CapMatchedAdapter) {36} elseif ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {20} else {10} + $expectedSeeds=if (Test-CapMatchedAdapter) {22} elseif ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {4} else {2} if ($images.Count -ne $expectedImages) { throw "Expected $expectedImages images across before/after; captured $($images.Count)." } if ($zmxSeedReceipts.Count -ne $expectedSeeds -or @($zmxSeedReceipts | Where-Object { $_.exitCode -ne 0 -or -not $_.daemonReady -or $_.cmdCount -lt 1 -or @@ -2061,7 +2148,7 @@ try { if ($manifestPath -and $job -and $auth -and $journalState) { $finalStatus = if (-not $primaryFailure -and $cleanupFailures.Count -eq 0 -and (($rehearsal -and $rehearsalLaunches.Count -eq 1 -and $images.Count -eq 0) -or - (-not $rehearsal -and $images.Count -eq $(if ($job.sceneAdapter -ceq 'S_MATCHED_V12_V1') {36} else {10})))) { + (-not $rehearsal -and $images.Count -eq $(if (Test-CapMatchedAdapter) {36} else {10})))) { 'COMPLETE' } else { 'FAILED' } try { diff --git a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 index b592c7c8..c7b131b6 100644 --- a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 +++ b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 @@ -20,6 +20,7 @@ param( [switch]$AdvisoryOlderContractOnly, [switch]$DAdapterContractOnly, [switch]$MatchedScenarioContractOnly, + [switch]$MatchedDpiContractOnly, [switch]$SeedPathContractOnly, [ValidateSet('All','ExitWait')][string]$SeedPathCase = 'All', [ValidateSet('All','SceneContract','ActualAdmission','FixtureDirectory')][string]$DAdapterCase = 'All', @@ -602,6 +603,9 @@ function Assert-CapRejects { 'CapNative|GetWindowRect|user32.dll|GetWindowRect', 'CapNative|GetClientRect|user32.dll|GetClientRect', 'CapNative|GetDpiForWindow|user32.dll|GetDpiForWindow', + 'CapNative|GetWindowLongPtr|user32.dll|GetWindowLongPtrW', + 'CapNative|GetMenu|user32.dll|GetMenu', + 'CapNative|AdjustWindowRectExForDpi|user32.dll|AdjustWindowRectExForDpi', 'CapNative|SetWindowPos|user32.dll|SetWindowPos', 'CapNative|PrintWindow|user32.dll|PrintWindow', 'CapProcessJob|CreateJobObjectW|kernel32.dll|CreateJobObjectW', @@ -616,8 +620,8 @@ function Assert-CapRejects { 'CapProcessJob|SetHandleInformation|kernel32.dll|SetHandleInformation' ) $actual = @(Test-CapNativeBindings | ForEach-Object { "$($_.Type)|$($_.Method)|$($_.Library)|$($_.EntryPoint)" }) - Assert-CapTest ($actual.Count -eq 30 -and @(Compare-Object $expected $actual).Count -eq 0) 'Exact static30 export contract differs.' - return @{ subcases = 30; evidence = 'API export resolution only, no native window or GUI action.' } + Assert-CapTest ($actual.Count -eq 33 -and @(Compare-Object $expected $actual).Count -eq 0) 'Exact static33 export contract differs.' + return @{ subcases = 33; evidence = 'API export resolution only, no native window or GUI action.' } } Invoke-CapTest 'legacy mutable seed journal proof is refused and exact numbered terminal proof is accepted' { $f = New-JournalTerminalFixture 'seed-reference' @@ -2499,6 +2503,33 @@ function New-CapMatchedTestJob { } | ConvertTo-Json -Depth 30 | ConvertFrom-Json -DateKind String } +function New-CapMatchedDpiTestJob { + $job=New-CapMatchedTestJob + $job.sceneAdapter='S_MATCHED_V12_DPI_V1' + $job.captureContract=[pscustomobject][ordered]@{ + geometrySchemaVersion=2L + phases=@('before','after') + sceneCountPerPhase=18L + placement='MEASURED_FIT_WORKAREA' + logicalClientWH=@(1440L,900L) + expectedWindowDpi=192L + dpiBase=96L + expectedPhysicalClientWH=@(2880L,1800L) + expectedAdjustedOuterWH=@(2906L,1910L) + minimumPhysicalWorkAreaWH=@(2906L,1910L) + windowStyleForAdjustment=47120384L + expectedRuntimeWindowStyle=315555840L + windowExStyle=0L + hasMenu=$true + geometryDerivation='AdjustWindowRectExForDpi' + canvasZoomPercentLogical=100L + rawCaptureResamplingAllowed=$false + cleanupReserveSeconds=90L + estimatedSeconds=1200L + } + return $job +} + function New-CapRestartTestEvidence { $project='C:\owned\Matched' $node='11111111-1111-4111-8111-000000000001' @@ -2631,6 +2662,102 @@ function Invoke-CapMatchedContracts { } } +function Invoke-CapMatchedDpiContracts { + Invoke-CapTest 'DPI matched geometry derives exact 96 and 192 physical frames' { + $job=New-CapMatchedDpiTestJob + Assert-CapSceneContract -Job $job + $style=47120384L + $at96=Get-CapDpiGeometry -LogicalClientWH @(1440,900) -ExpectedDpi 96 ` + -WindowStyle $style -WindowExStyle 0 -HasMenu $true -WorkArea @(0,0,3000,2000) ` + -ExpectedAdjustedOuterWH @(1456,959) + $at192=Get-CapDpiGeometry -LogicalClientWH @(1440,900) -ExpectedDpi 192 ` + -WindowStyle $style -WindowExStyle 0 -HasMenu $true -WorkArea @(0,0,3000,2000) ` + -ExpectedAdjustedOuterWH @(2906,1910) + Assert-CapTest (($at96.PhysicalClientWH -join ',') -ceq '1440,900' -and + ($at96.OuterWH -join ',') -ceq '1456,959' -and + ($at192.PhysicalClientWH -join ',') -ceq '2880,1800' -and + ($at192.OuterWH -join ',') -ceq '2906,1910' -and + ($at192.NonClientWH -join ',') -ceq '26,110' -and -not $at192.Resampled) ` + 'DPI-parametric exact frame derivation differs.' + return @{dpi96=$at96;dpi192=$at192;sceneContract='S_MATCHED_V12_DPI_V1'} + } + Invoke-CapTest 'DPI matched geometry refuses insufficient work area and invalid scaling' { + $arguments=@{LogicalClientWH=@(1440,900);ExpectedDpi=192;WindowStyle=47120384L + WindowExStyle=0L;HasMenu=$true;ExpectedAdjustedOuterWH=@(2906,1910)} + $small=Assert-CapRejects {Get-CapDpiGeometry @arguments -WorkArea @(0,0,1920,1032)} 'does not fit' + $shortWidth=Assert-CapRejects {Get-CapDpiGeometry @arguments -WorkArea @(0,0,2905,1910)} 'does not fit' + $shortHeight=Assert-CapRejects {Get-CapDpiGeometry @arguments -WorkArea @(0,0,2906,1909)} 'does not fit' + $fractional=Assert-CapRejects { + Get-CapDpiGeometry -LogicalClientWH @(1,1) -ExpectedDpi 144 -WindowStyle 47120384L ` + -WindowExStyle 0 -HasMenu $true -WorkArea @(0,0,100,100) + } 'not integral' + $outer=Assert-CapRejects { + Get-CapDpiGeometry @arguments -WorkArea @(0,0,3000,2000) -ExpectedAdjustedOuterWH @(2905,1910) + } 'adjusted outer size differs' + return @{currentRdpRefused=$small;onePixelWidth=$shortWidth;onePixelHeight=$shortHeight + fractionalScaling=$fractional;wrongOuter=$outer} + } + Invoke-CapTest 'DPI matched logical zoom normalizes physical UIA width' { + $dpi96=Get-CapLogicalZoomEvidence -PhysicalWidth 250 -NaturalLogicalWidth 250 -Dpi 96 + $dpi192=Get-CapLogicalZoomEvidence -PhysicalWidth 500 -NaturalLogicalWidth 250 -Dpi 192 + Assert-CapTest ($dpi96.ZoomPercent -eq 100 -and $dpi192.ZoomPercent -eq 100 -and + $dpi192.MeasuredPhysicalWidth -eq 500 -and $dpi192.MeasuredLogicalWidth -eq 250) ` + 'DPI-normalized actual-size evidence differs.' + $unnormalized=Assert-CapRejects { + Get-CapLogicalZoomEvidence -PhysicalWidth 250 -NaturalLogicalWidth 250 -Dpi 192 + } 'expected 100%' + return @{dpi96=$dpi96;dpi192=$dpi192;oldPhysicalWidthMutant=$unnormalized} + } + Invoke-CapTest 'DPI matched manifest preserves raw logical physical metadata without resampling' { + $job=New-CapMatchedDpiTestJob + $contract=$job.captureContract + $baseImage=[ordered]@{ + captureMode='PRINTWINDOW';observedState=@{readSucceeded=$true;evidenceMode='OWNED_UIA_READ_NOT_VISUAL_PARITY'} + evidenceClassification='C8R_RDP_DPI192_QUALIFIED';geometrySchemaVersion=2L + geometryDerivation='AdjustWindowRectExForDpi';logicalClientWH=@(1440L,900L) + physicalClientBounds=@(0L,0L,2880L,1800L);clientBounds=@(0L,0L,2880L,1800L) + physicalOuterBounds=@(0L,0L,2906L,1910L);outerBounds=@(0L,0L,2906L,1910L) + nonClientWH=@(26L,110L);rawPixelWH=@(2906L,1910L);width=2906L;height=1910L + actualWindowDpi=192L;dpi=192L;dpiBase=96L;dpiScaleNumerator=192L;dpiScaleDenominator=96L + windowStyle=315555840L;windowStyleForAdjustment=47120384L;windowExStyle=0L;hasMenu=$true + physicalWorkAreaBounds=@(0L,0L,3000L,2000L);placement='MEASURED_FIT_WORKAREA';resampled=$false + } + $images=@(foreach ($phase in @('before','after')) {foreach ($scene in Get-CapMatchedSceneNames) { + $image=$baseImage | ConvertTo-Json -Depth 20 | ConvertFrom-Json -DateKind String + $image | Add-Member -NotePropertyName phase -NotePropertyValue $phase + $image | Add-Member -NotePropertyName scene -NotePropertyValue $scene + $image + }}) + $manifest=@{sceneAdapter='S_MATCHED_V12_DPI_V1';lane='S';status='COMPLETE';executionMode='LIVE_CAPTURE' + backendTurns=0;g11Qualification='ASSESSABLE_SOURCE_CAPABILITY_NOT_RUNTIME_VALIDATED';images=$images + realRestarts=@(@{phase='before';evidence=(New-CapRestartTestEvidence)}, + @{phase='after';evidence=(New-CapRestartTestEvidence)})} | + ConvertTo-Json -Depth 40 | ConvertFrom-Json -DateKind String + Assert-CapMatchedManifest -Manifest $manifest -Job $job + foreach ($image in $manifest.images) {$null=Assert-CapMatchedDpiImage -Image $image -Contract $contract} + $mutations=@('resampled','dpi','logical','raw','outer','work') + foreach ($mutation in $mutations) { + $changed=$baseImage | ConvertTo-Json -Depth 20 | ConvertFrom-Json -DateKind String + switch ($mutation) { + 'resampled' {$changed.resampled=$true} + 'dpi' {$changed.actualWindowDpi=96L} + 'logical' {$changed.logicalClientWH=@(720L,450L)} + 'raw' {$changed.rawPixelWH=@(1440L,900L)} + 'outer' { + $changed.physicalOuterBounds=@(0L,0L,3000L,2000L) + $changed.outerBounds=@(0L,0L,3000L,2000L) + $changed.rawPixelWH=@(3000L,2000L) + $changed.width=3000L;$changed.height=2000L + } + 'work' {$changed.physicalWorkAreaBounds=@(0L,0L,1920L,1032L)} + } + $null=Assert-CapRejects {Assert-CapMatchedDpiImage -Image $changed -Contract $contract} 'DPI-parametric matched image' + } + return @{images=$images.Count;positive=36;negative=$mutations.Count;mutations=$mutations + evidenceClassification='C8R_RDP_DPI192_QUALIFIED';resampled=$false} + } +} + function Invoke-CapMatchedTabContracts { foreach ($vector in @( @('workspace-tab:0','workspace-tab-2015407614065646652'), @@ -3596,9 +3723,9 @@ function Invoke-CapDContracts { Assert-CapTest ($errors.Count -eq 0) 'Current worker did not parse.' $definitions=@($ast.FindAll({param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and - $node.Name -cin @('Assert-CapOwnedPath','Get-CapBaseEnvironment') + $node.Name -cin @('Assert-CapOwnedPath','Test-CapMatchedAdapter','Get-CapBaseEnvironment') },$true)) - Assert-CapTest ($definitions.Count -eq 2) 'Actual environment/path functions missing or duplicated.' + Assert-CapTest ($definitions.Count -eq 3) 'Actual environment/path functions missing or duplicated.' foreach ($definition in $definitions) {. ([scriptblock]::Create($definition.Extent.Text))} $expected=Join-Path $root 'fixtures\projects\D Offline Capture Fixture' Assert-CapTest (-not (Test-Path -LiteralPath $expected)) 'Owned D project must start absent.' @@ -6069,6 +6196,19 @@ if ($MatchedScenarioContractOnly) { exit 0 } +if ($MatchedDpiContractOnly) { + Invoke-CapMatchedDpiContracts + $passed=@($results | Where-Object passed -EQ $true).Count + $failed=@($results | Where-Object passed -EQ $false).Count + $report=[pscustomobject][ordered]@{suite='UiCaptureMatchedDpiContracts';executed=$results.Count + passed=$passed;failed=$failed;skipped=0;gui='NotExecuted';images=0;cases=@($results) + evidence='NATIVE_GEOMETRY_AND_SYNTHETIC_RECORDS_NO_WINDOW_LAUNCH'} + $report | ConvertTo-Json -Depth 35 | Set-Content (Join-Path $scratch 'report.json') -Encoding utf8NoBOM + $report | Select-Object suite,executed,passed,failed | ConvertTo-Json -Compress + if ($results.Count -ne 4 -or $passed -ne 4 -or $failed -ne 0) {exit 1} + exit 0 +} + if ($DAdapterContractOnly -and $DAdapterCase -cin @('SceneContract','FixtureDirectory')) { Invoke-CapDContracts Write-CapDContractReport @@ -6111,7 +6251,9 @@ if ($DescendantMetadataContractOnly) { } if ($PrepareFixturesOnly) { - if ($ReceiptContractOnly -or $FixtureContractOnly -or $JournalTimingContractOnly -or $JournalFirstOpenContractOnly -or $JournalObservationContractOnly -or $PrebuiltFixtureManifest) { + if ($ReceiptContractOnly -or $FixtureContractOnly -or $JournalTimingContractOnly -or + $JournalFirstOpenContractOnly -or $JournalObservationContractOnly -or + $MatchedDpiContractOnly -or $PrebuiltFixtureManifest) { throw 'Fixture preparation cannot also consume or run contract suites.' } $null = New-CapHarmlessTarget @@ -6173,6 +6315,7 @@ try { Invoke-CapAppChildContracts Invoke-CapDContracts Invoke-CapMatchedContracts + Invoke-CapMatchedDpiContracts Invoke-CapSeedPathContracts Invoke-CapIdentityContracts Invoke-CapSelectorRefusalContracts @@ -7228,6 +7371,6 @@ try { } [ordered]@{ suite = $suiteName; executed = $results.Count; passed = $passed; failed = $failed; skipped = 0; artifacts = $scratch } | ConvertTo-Json -Compress -$expectedCount = if ($JournalContractOnly) { 10 } elseif ($LaunchContractOnly) { 2 } elseif ($RequireRealSeed) { 180 } else { 177 } +$expectedCount = if ($JournalContractOnly) { 10 } elseif ($LaunchContractOnly) { 2 } elseif ($RequireRealSeed) { 184 } else { 181 } if ($results.Count -ne $expectedCount -or $failed -ne 0 -or $passed -ne $results.Count) { exit 1 } exit 0 diff --git a/Tools/windows/Tests/ValidationRunner.Tests.ps1 b/Tools/windows/Tests/ValidationRunner.Tests.ps1 index 37b7b87e..4ac439a8 100644 --- a/Tools/windows/Tests/ValidationRunner.Tests.ps1 +++ b/Tools/windows/Tests/ValidationRunner.Tests.ps1 @@ -4780,7 +4780,7 @@ if ($LASTEXITCODE -ne 0) { throw "RED: offline real-launch capture child failed" $uiCaptureSummaries = @($uiCaptureOutput | Where-Object { $_ -is [string] -and $_.StartsWith('{"suite":"UiCaptureDriver",') }) if ($uiCaptureSummaries.Count -ne 1) { throw "RED: offline real-launch capture structured count missing or duplicated" } $uiCaptureSummary = ConvertFrom-Json $uiCaptureSummaries[0] -if ($uiCaptureSummary.executed -isnot [long] -or $uiCaptureSummary.executed -ne 177 -or +if ($uiCaptureSummary.executed -isnot [long] -or $uiCaptureSummary.executed -ne 181 -or $uiCaptureSummary.passed -isnot [long] -or $uiCaptureSummary.passed -ne $uiCaptureSummary.executed -or $uiCaptureSummary.failed -isnot [long] -or $uiCaptureSummary.failed -ne 0) { throw "RED: offline real-launch capture child has no positive executed/passed contract count" @@ -4797,7 +4797,11 @@ $matchedGroups=@( 'real restart transcript requires daemon confirmation resume and new owned attach', 'real restart scene invokes existing UIA route and refuses offline peer authority', 'matched transparent proxy relays unchanged real pipe frames with connection identity', - 'matched manifest seals scene inventory and refuses premature restart success' + 'matched manifest seals scene inventory and refuses premature restart success', + 'DPI matched geometry derives exact 96 and 192 physical frames', + 'DPI matched geometry refuses insufficient work area and invalid scaling', + 'DPI matched logical zoom normalizes physical UIA width', + 'DPI matched manifest preserves raw logical physical metadata without resampling' ) foreach ($name in $matchedGroups) { $group=@($uiCaptureReport.cases | Where-Object name -CEQ $name) diff --git a/Tools/windows/UI-CAPTURE-SCENARIOS.md b/Tools/windows/UI-CAPTURE-SCENARIOS.md index bcb1b977..c5f30557 100644 --- a/Tools/windows/UI-CAPTURE-SCENARIOS.md +++ b/Tools/windows/UI-CAPTURE-SCENARIOS.md @@ -33,11 +33,74 @@ a separately granted capture window. No subset or skipped-scene support is added Lane S2 adds **two top-level groups**, taking the combined registered totals from **175 to 177 default**, or **178 to 180 with `-RequireRealSeed`**. -`Tests\ValidationRunner.Tests.ps1` requires exactly 177 passing groups and the -unique presence of the original eight S groups plus both S2 groups. The live -matched preflight requires exactly 180 passing groups, zero skips, and the same +At the S2 checkpoint, `Tests\ValidationRunner.Tests.ps1` required exactly 177 passing groups and the +unique presence of the original eight S groups plus both S2 groups. The then-current live matched preflight required exactly 180 passing groups, zero skips, and the same unique groups; a higher count is not accepted. +Those **177 / 180** totals are the pre-DPI historical boundary. The +`S_MATCHED_V12_DPI_V1` harness change adds four no-desktop top-level groups, +so current merged-source registration becomes **181 default / 184 with +`-RequireRealSeed`**. Current matched preflight requires exactly **184**, +zero skips, the ten existing matched/S2 identities once, and all four DPI +identities once. Historical reports retain their original denominators. + +## DPI-parametric matched adapter + +`S_MATCHED_V12_DPI_V1` preserves the V1 fixture, eighteen scene names, actions, +real Restart requirements, process ownership, privacy and cleanup gates. It +changes only the geometry/evidence contract. + +The job separates logical and physical geometry: + +- logical client 1440x900; +- actual window DPI 192, base DPI 96; +- physical client 2880x1800; +- `AdjustWindowRectExForDpi` outer 2906x1910 for + `WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN`, extended style 0 and an installed + menu; +- runtime shown-window style `0x12CF0000`, while adjustment uses the + source frame style `0x02CF0000`; +- measured fit inside a physical work area at least 2906x1910; +- no resampling. + +The supervisor performs the work-area calculation before worker launch. The +worker queries the actual window DPI/style/menu, derives the physical frame, +positions it without changing size, and remeasures exact physical client and +outer bounds before any scene action. A 1920x1032 RDP mode refuses before +product launch. + +UIA bounding rectangles remain physical pixels. Actual-size zoom normalizes +the physical width by `96 / actualDpi` before comparing with the card's 250 +logical-pixel natural width. A 500-physical-pixel card at DPI 192 is therefore +250 logical pixels and 100% zoom; the old unnormalized 200% interpretation is +a negative contract. + +Each image retains the raw PrintWindow bitmap and records logical client size, +physical client/outer bounds, nonclient extents, raw pixel dimensions, actual +DPI and scale, adjustment/runtime styles, menu state, physical work area, +placement and `resampled=false`. The evidence classification is +`C8R_RDP_DPI192_QUALIFIED`. + +The four focused groups cover: + +| Group | Boundary | +| --- | --- | +| DPI exact frames | 96-DPI 1456x959 and 192-DPI 2906x1910 results, plus the new scene contract | +| DPI refusals | insufficient/one-pixel-short work areas, non-integral scaling and wrong adjusted outer | +| DPI logical zoom | physical-to-logical normalization and the old unnormalized mutant | +| DPI manifest | 36 simulated raw records and DPI/resampling/logical/work-area mutations | + +These contracts launch no GraphCode window and capture no images. A future live +run still requires a source-reviewed job, exact 184-case preflight, warned +one-attempt lease and sufficiently large DPI-192 RDP work area. + +An exact macOS @2x 1440x900 logical reference has a 2880x1800 client raster and +may be reviewed against the raw Windows client at native 1:1 pixels without +resampling. Such review may support observed layout/state/action findings, but +not physical input, local-console compositor equivalence, native local glyph +or antialiasing fidelity, focus outside the observed RDP session, or automatic +row validation. Unmatched scenes remain logical-geometry-only or unassessable. + Matched S seed receipts use the initialized `Roots.Root\zmx-seed.json`, not a path rebuilt from the diagnostic phase label. B and D retain their existing normal and per-session receipt layouts (D seeds share an app root). That directory is @@ -92,7 +155,7 @@ nor an arbitrary `<=100` cap defines the native argument. A second control injects a fully spent 5000 ms window and requires an actual zero-millisecond wait, an unsignalled held root, null signal latency and the unchanged kernel-live refusal. These controls remain inside the existing first S2 group: **delta 0**, -with registered totals still **177 default / 180 real-seed**. +with the S2-checkpoint totals still **177 default / 180 real-seed**. Wall observation is separate: a deterministic 1500 ms delay after actual Close reproduces the old strict wall-time assertion, and a documented 5000 ms observational scheduling tolerance applies only to that wall measurement @@ -144,7 +207,7 @@ The standalone Lane S change adds **eight top-level groups exactly once**: The three existing actual-zmx rehearsal cases are unchanged; the real-seed top-level delta is **zero**. No old group or negative case was removed or relabelled. Its original registration required 174 and the unique -presence of all eight names; the current S2 totals above supersede that total. +presence of all eight names; the historical S2 totals above superseded that total. A different total is a failure, not an empty-filter pass. A later merged change with its own declared delta requires an explicit combined-tree count update and registered-runner rerun. diff --git a/Tools/windows/UiCaptureDriver.psm1 b/Tools/windows/UiCaptureDriver.psm1 index 04d2c996..36ab7efd 100644 --- a/Tools/windows/UiCaptureDriver.psm1 +++ b/Tools/windows/UiCaptureDriver.psm1 @@ -77,6 +77,9 @@ public sealed class CapWindowSnapshot { public int[] OuterBounds { get; set; } public int[] ClientBounds { get; set; } public uint Dpi { get; set; } + public long Style { get; set; } + public long ExStyle { get; set; } + public bool HasMenu { get; set; } } public sealed class CapCaptureResult { @@ -129,6 +132,12 @@ public static class CapNative { private static extern bool GetClientRect(IntPtr window, out NativeRect rect); [DllImport("user32.dll")] private static extern uint GetDpiForWindow(IntPtr window); + [DllImport("user32.dll", EntryPoint="GetWindowLongPtrW", SetLastError=true)] + private static extern IntPtr GetWindowLongPtr(IntPtr window, int index); + [DllImport("user32.dll")] + private static extern IntPtr GetMenu(IntPtr window); + [DllImport("user32.dll", SetLastError=true)] + private static extern bool AdjustWindowRectExForDpi(ref NativeRect rect, uint style, bool hasMenu, uint exStyle, uint dpi); [DllImport("user32.dll", SetLastError=true)] private static extern bool SetWindowPos(IntPtr window, IntPtr insertAfter, int x, int y, int width, int height, uint flags); [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] @@ -269,6 +278,15 @@ public static class CapNative { return SetWindowPos(window, IntPtr.Zero, x, y, width, height, 0x0014); } + public static int[] AdjustWindowRectForDpi(int clientWidth, int clientHeight, long style, bool hasMenu, long exStyle, uint dpi) { + if (clientWidth <= 0 || clientHeight <= 0 || dpi == 0) + throw new ArgumentOutOfRangeException("DPI window geometry requires positive dimensions and DPI."); + var rect = new NativeRect { Left = 0, Top = 0, Right = clientWidth, Bottom = clientHeight }; + if (!AdjustWindowRectExForDpi(ref rect, unchecked((uint)style), hasMenu, unchecked((uint)exStyle), dpi)) + throw new InvalidOperationException("AdjustWindowRectExForDpi failed: " + Marshal.GetLastWin32Error()); + return new[] { rect.Left, rect.Top, rect.Right, rect.Bottom, rect.Right - rect.Left, rect.Bottom - rect.Top }; + } + public static CapWindowSnapshot WindowSnapshot(IntPtr window, int expectedPid) { uint owner; if (window == IntPtr.Zero || GetWindowThreadProcessId(window, out owner) == 0 || owner != (uint)expectedPid) @@ -279,7 +297,10 @@ public static class CapNative { return new CapWindowSnapshot { OuterBounds = new[] { outer.Left, outer.Top, outer.Right, outer.Bottom }, ClientBounds = new[] { client.Left, client.Top, client.Right, client.Bottom }, - Dpi = GetDpiForWindow(window) + Dpi = GetDpiForWindow(window), + Style = GetWindowLongPtr(window, -16).ToInt64(), + ExStyle = GetWindowLongPtr(window, -20).ToInt64(), + HasMenu = GetMenu(window) != IntPtr.Zero }; } @@ -991,7 +1012,8 @@ function Assert-CapFixtureMapping { [Parameter(Mandatory)][ValidatePattern('^[A-Fa-f0-9]{64}$')][string]$ExpectedMappingSha256, [Parameter(Mandatory)][ValidatePattern('^[A-Fa-f0-9]{64}$')][string]$ExpectedSourceSha256, [Parameter(Mandatory)][string]$OriginalRoot, - [ValidateSet('B_FIVE_SCENES_V1','D_QUICK_CHAT_V1','S_MATCHED_V12_V1')][string]$SceneAdapter = 'B_FIVE_SCENES_V1', + [ValidateSet('B_FIVE_SCENES_V1','D_QUICK_CHAT_V1','S_MATCHED_V12_V1','S_MATCHED_V12_DPI_V1')] + [string]$SceneAdapter = 'B_FIVE_SCENES_V1', [string]$ExpectedPeerSha256 ) Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters @@ -1006,7 +1028,7 @@ function Assert-CapFixtureMapping { } $mapping = Get-Content -LiteralPath $MappingPath -Raw | ConvertFrom-Json -DateKind String $isD = $SceneAdapter -ceq 'D_QUICK_CHAT_V1' - $isS = $SceneAdapter -ceq 'S_MATCHED_V12_V1' + $isS = $SceneAdapter -cin @('S_MATCHED_V12_V1','S_MATCHED_V12_DPI_V1') $graphCount = if ($isD -or $isS) {1} else {2} $nodeCount = if ($isS) {8} elseif ($isD) {2} else {10} if ($mapping.sourcePath -cne (Join-Path $OriginalRoot 'fixture.json') -or @@ -1701,11 +1723,36 @@ function Invoke-CapAction { } } +function Get-CapLogicalZoomEvidence { + [CmdletBinding()] + param( + [Parameter(Mandatory)][ValidateRange(0.01, 100000)][double]$PhysicalWidth, + [Parameter(Mandatory)][ValidateRange(1, 1000)][int]$NaturalLogicalWidth, + [Parameter(Mandatory)][ValidateRange(1, 960)][uint32]$Dpi + ) + Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters + + $logicalWidth = $PhysicalWidth * 96.0 / $Dpi + $percent = [Math]::Round(($logicalWidth / $NaturalLogicalWidth) * 100, 2) + if ($percent -lt 99 -or $percent -gt 101) { + throw "Actual-size logical zoom measured $percent%, expected 100%." + } + return [pscustomobject]@{ + NaturalLogicalWidth = $NaturalLogicalWidth + MeasuredPhysicalWidth = $PhysicalWidth + MeasuredLogicalWidth = [Math]::Round($logicalWidth, 4) + Dpi = [long]$Dpi + DpiBase = 96L + ZoomPercent = $percent + } +} + function Measure-CapZoom { [CmdletBinding()] param( [Parameter(Mandatory)][System.Windows.Automation.AutomationElement]$Element, - [Parameter(Mandatory)][ValidateRange(1, 1000)][int]$NaturalWidth + [Parameter(Mandatory)][ValidateRange(1, 1000)][int]$NaturalWidth, + [ValidateRange(1, 960)][uint32]$Dpi = 96 ) Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters @@ -1715,15 +1762,147 @@ function Measure-CapZoom { if ($current.IsOffscreen -or $rect.Width -le 0 -or $rect.Height -le 0) { throw "Zoom probe '$($current.Name)' is not visibly bounded." } - $percent = [Math]::Round(($rect.Width / $NaturalWidth) * 100, 2) - if ($percent -lt 99 -or $percent -gt 101) { throw "Actual-size zoom measured $percent%, expected 100%." } + $logical = Get-CapLogicalZoomEvidence -PhysicalWidth $rect.Width ` + -NaturalLogicalWidth $NaturalWidth -Dpi $Dpi return [pscustomobject]@{ Name = $current.Name AutomationId = $current.AutomationId Bounds = [pscustomobject]@{ Left = $rect.Left; Top = $rect.Top; Width = $rect.Width; Height = $rect.Height } NaturalWidth = $NaturalWidth MeasuredWidth = $rect.Width - ZoomPercent = $percent + MeasuredLogicalWidth = $logical.MeasuredLogicalWidth + Dpi = $logical.Dpi + DpiBase = $logical.DpiBase + ZoomPercent = $logical.ZoomPercent + } +} + +function Get-CapDpiGeometry { + [CmdletBinding()] + param( + [Parameter(Mandatory)][int[]]$LogicalClientWH, + [Parameter(Mandatory)][ValidateRange(1, 960)][uint32]$ExpectedDpi, + [Parameter(Mandatory)][long]$WindowStyle, + [Parameter(Mandatory)][long]$WindowExStyle, + [Parameter(Mandatory)][bool]$HasMenu, + [Parameter(Mandatory)][int[]]$WorkArea, + [int[]]$ExpectedAdjustedOuterWH = @() + ) + Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters + + if ($LogicalClientWH.Count -ne 2 -or $LogicalClientWH[0] -le 0 -or $LogicalClientWH[1] -le 0) { + throw 'DPI-parametric geometry requires two positive logical client dimensions.' + } + if ($WorkArea.Count -ne 4 -or $WorkArea[2] -le $WorkArea[0] -or $WorkArea[3] -le $WorkArea[1]) { + throw 'DPI-parametric geometry requires positive physical work-area bounds.' + } + $physical = [Collections.Generic.List[int]]::new() + foreach ($logical in $LogicalClientWH) { + $scaled = [long]$logical * [long]$ExpectedDpi + if ($scaled % 96L -ne 0) { + throw "DPI-parametric logical dimension $logical is not integral at DPI $ExpectedDpi." + } + $value = $scaled / 96L + if ($value -le 0 -or $value -gt [int]::MaxValue) { + throw 'DPI-parametric physical client dimension overflowed.' + } + $physical.Add([int]$value) + } + $adjusted = [CapNative]::AdjustWindowRectForDpi( + $physical[0], $physical[1], $WindowStyle, $HasMenu, $WindowExStyle, $ExpectedDpi) + $outerWH = @([int]$adjusted[4], [int]$adjusted[5]) + if ($ExpectedAdjustedOuterWH.Count -gt 0 -and + ($ExpectedAdjustedOuterWH.Count -ne 2 -or + ($ExpectedAdjustedOuterWH -join ',') -cne ($outerWH -join ','))) { + throw "DPI-parametric adjusted outer size differs: $($outerWH -join 'x')." + } + $workWidth = $WorkArea[2] - $WorkArea[0] + $workHeight = $WorkArea[3] - $WorkArea[1] + if ($outerWH[0] -gt $workWidth -or $outerWH[1] -gt $workHeight) { + throw "DPI-parametric window does not fit physical work area: outer=$($outerWH -join 'x') work=${workWidth}x${workHeight}." + } + return [pscustomobject]@{ + GeometrySchemaVersion = 2L + GeometryDerivation = 'AdjustWindowRectExForDpi' + LogicalClientWH = @([long]$LogicalClientWH[0], [long]$LogicalClientWH[1]) + PhysicalClientWH = @([long]$physical[0], [long]$physical[1]) + AdjustedOffsets = @([long]$adjusted[0], [long]$adjusted[1], + [long]$adjusted[2], [long]$adjusted[3]) + NonClientWH = @([long]($outerWH[0] - $physical[0]), [long]($outerWH[1] - $physical[1])) + OuterWH = @([long]$outerWH[0], [long]$outerWH[1]) + OuterBounds = @([long]$WorkArea[0], [long]$WorkArea[1], + [long]($WorkArea[0] + $outerWH[0]), [long]($WorkArea[1] + $outerWH[1])) + PhysicalWorkAreaBounds = @([long]$WorkArea[0], [long]$WorkArea[1], + [long]$WorkArea[2], [long]$WorkArea[3]) + ActualDpi = [long]$ExpectedDpi + DpiBase = 96L + DpiScaleNumerator = [long]$ExpectedDpi + DpiScaleDenominator = 96L + WindowStyle = [long]$WindowStyle + WindowExStyle = [long]$WindowExStyle + HasMenu = $HasMenu + Placement = 'MEASURED_FIT_WORKAREA' + Resampled = $false + } +} + +function Assert-CapMatchedDpiImage { + [CmdletBinding()] + param( + [Parameter(Mandatory)]$Image, + [Parameter(Mandatory)]$Contract + ) + Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters + + Assert-CapDataRecord $Image @{ + evidenceClassification=[string];geometrySchemaVersion=[long];geometryDerivation=[string] + logicalClientWH=[array];physicalClientBounds=[array];physicalOuterBounds=[array] + nonClientWH=[array];rawPixelWH=[array];actualWindowDpi=[long];dpiBase=[long] + dpiScaleNumerator=[long];dpiScaleDenominator=[long];windowStyle=[long] + windowStyleForAdjustment=[long] + windowExStyle=[long];hasMenu=[bool];physicalWorkAreaBounds=[array] + placement=[string];resampled=[bool];clientBounds=[array];outerBounds=[array] + width=[long];height=[long];dpi=[long] + } 'DPI-parametric matched image' + if ($Image.evidenceClassification -cne 'C8R_RDP_DPI192_QUALIFIED' -or + $Image.geometrySchemaVersion -ne 2 -or $Image.geometryDerivation -cne 'AdjustWindowRectExForDpi' -or + ($Image.logicalClientWH -join ',') -cne ($Contract.logicalClientWH -join ',') -or + ($Image.physicalClientBounds -join ',') -cne + "0,0,$($Contract.expectedPhysicalClientWH[0]),$($Contract.expectedPhysicalClientWH[1])" -or + ($Image.clientBounds -join ',') -cne ($Image.physicalClientBounds -join ',') -or + ($Image.physicalOuterBounds -join ',') -cne ($Image.outerBounds -join ',') -or + ($Image.outerBounds[2]-$Image.outerBounds[0]) -ne $Contract.expectedAdjustedOuterWH[0] -or + ($Image.outerBounds[3]-$Image.outerBounds[1]) -ne $Contract.expectedAdjustedOuterWH[1] -or + ($Image.nonClientWH -join ',') -cne + "$($Contract.expectedAdjustedOuterWH[0]-$Contract.expectedPhysicalClientWH[0]),$($Contract.expectedAdjustedOuterWH[1]-$Contract.expectedPhysicalClientWH[1])" -or + ($Image.rawPixelWH -join ',') -cne ($Contract.expectedAdjustedOuterWH -join ',') -or + ($Image.rawPixelWH -join ',') -cne "$($Image.width),$($Image.height)" -or + $Image.width -ne $Contract.expectedAdjustedOuterWH[0] -or + $Image.height -ne $Contract.expectedAdjustedOuterWH[1] -or + $Image.width -ne ($Image.outerBounds[2]-$Image.outerBounds[0]) -or + $Image.height -ne ($Image.outerBounds[3]-$Image.outerBounds[1]) -or + $Image.actualWindowDpi -ne $Contract.expectedWindowDpi -or + $Image.dpi -ne $Contract.expectedWindowDpi -or $Image.dpiBase -ne $Contract.dpiBase -or + $Image.dpiScaleNumerator -ne $Contract.expectedWindowDpi -or + $Image.dpiScaleDenominator -ne $Contract.dpiBase -or + $Image.windowStyle -ne $Contract.expectedRuntimeWindowStyle -or + $Image.windowStyleForAdjustment -ne $Contract.windowStyleForAdjustment -or + $Image.windowExStyle -ne $Contract.windowExStyle -or $Image.hasMenu -ne $Contract.hasMenu -or + $Image.physicalWorkAreaBounds.Count -ne 4 -or + ($Image.physicalWorkAreaBounds[2]-$Image.physicalWorkAreaBounds[0]) -lt + $Contract.minimumPhysicalWorkAreaWH[0] -or + ($Image.physicalWorkAreaBounds[3]-$Image.physicalWorkAreaBounds[1]) -lt + $Contract.minimumPhysicalWorkAreaWH[1] -or + $Image.placement -cne $Contract.placement -or $Image.resampled -cne $false) { + throw 'DPI-parametric matched image logical, physical or raw-pixel metadata differs.' + } + return [pscustomobject]@{ + logicalClientWH=@($Image.logicalClientWH) + physicalClientWH=@($Contract.expectedPhysicalClientWH) + outerWH=@([long]$Image.width,[long]$Image.height) + dpi=[long]$Image.actualWindowDpi + resampled=$false + evidenceClassification=$Image.evidenceClassification } } @@ -3456,7 +3635,8 @@ function Assert-CapMatchedManifest { param([Parameter(Mandatory)]$Manifest,[Parameter(Mandatory)]$Job) $expected=@(foreach ($phase in @('before','after')) {foreach ($scene in Get-CapMatchedSceneNames) {"${phase}:$scene"}}) $actual=@($Manifest.images | ForEach-Object {"$($_.phase):$($_.scene)"}) - if ($Manifest.sceneAdapter -cne 'S_MATCHED_V12_V1' -or $Manifest.lane -cne 'S' -or + if ($Job.sceneAdapter -cnotin @('S_MATCHED_V12_V1','S_MATCHED_V12_DPI_V1') -or + $Manifest.sceneAdapter -cne $Job.sceneAdapter -or $Manifest.lane -cne 'S' -or $Manifest.status -cne 'COMPLETE' -or $Manifest.executionMode -cne 'LIVE_CAPTURE' -or $actual.Count -ne 36 -or @($actual | Sort-Object -Unique).Count -ne 36 -or (($actual | Sort-Object) -join ',') -cne (($expected | Sort-Object) -join ',') -or @@ -3477,6 +3657,66 @@ function Assert-CapMatchedManifest { function Assert-CapSceneContract { param([Parameter(Mandatory)]$Job) + if ($Job.lane -ceq 'S' -and $Job.sceneAdapter -ceq 'S_MATCHED_V12_DPI_V1') { + Assert-CapDataRecord $Job @{authority=[pscustomobject];captureContract=[pscustomobject];scenes=[array] + fixture=[pscustomobject];binaries=[pscustomobject]} 'DPI-parametric matched scene' + Assert-CapDataRecord $Job.authority @{centralCaptureLeaseGrantedByThisArtifact=[bool] + sWorkerMayLaunchGui=[bool]} 'DPI-parametric matched scene authority' + $contract=$Job.captureContract + Assert-CapDataRecord $contract @{geometrySchemaVersion=[long];phases=[array];sceneCountPerPhase=[long] + placement=[string];logicalClientWH=[array];expectedWindowDpi=[long];dpiBase=[long] + expectedPhysicalClientWH=[array];expectedAdjustedOuterWH=[array];minimumPhysicalWorkAreaWH=[array] + windowStyleForAdjustment=[long];expectedRuntimeWindowStyle=[long] + windowExStyle=[long];hasMenu=[bool];geometryDerivation=[string] + canvasZoomPercentLogical=[long];rawCaptureResamplingAllowed=[bool] + cleanupReserveSeconds=[long];estimatedSeconds=[long]} 'DPI-parametric matched geometry' + Assert-CapDataRecord $Job.binaries.backendFixture @{sha256=[string];sourceSha256=[string] + networkAllowed=[bool]} 'DPI-parametric matched backend fixture' + if ($Job.authority.centralCaptureLeaseGrantedByThisArtifact -cne $false -or + $Job.authority.sWorkerMayLaunchGui -cne $false -or $contract.geometrySchemaVersion -ne 2 -or + ($contract.phases -join ',') -cne 'before,after' -or $contract.sceneCountPerPhase -ne 18 -or + $contract.placement -cne 'MEASURED_FIT_WORKAREA' -or + ($contract.logicalClientWH -join ',') -cne '1440,900' -or + $contract.expectedWindowDpi -ne 192 -or $contract.dpiBase -ne 96 -or + ($contract.expectedPhysicalClientWH -join ',') -cne '2880,1800' -or + ($contract.expectedAdjustedOuterWH -join ',') -cne '2906,1910' -or + ($contract.minimumPhysicalWorkAreaWH -join ',') -cne '2906,1910' -or + $contract.windowStyleForAdjustment -ne 47120384 -or + $contract.expectedRuntimeWindowStyle -ne 315555840 -or $contract.windowExStyle -ne 0 -or + $contract.hasMenu -cne $true -or $contract.geometryDerivation -cne 'AdjustWindowRectExForDpi' -or + $contract.canvasZoomPercentLogical -ne 100 -or $contract.rawCaptureResamplingAllowed -cne $false -or + $contract.cleanupReserveSeconds -ne 90 -or $contract.estimatedSeconds -lt 1 -or + $contract.estimatedSeconds -gt 1710 -or + ($Job.scenes.name -join ',') -cne ((Get-CapMatchedSceneNames) -join ',')) { + throw 'DPI-parametric matched scene inventory, geometry or non-authority differs.' + } + foreach ($scene in $Job.scenes) { + Assert-CapDataRecord $scene @{name=[string];naturalWidthBeforeAfter=[array]} 'DPI matched scene width' + if ($scene.naturalWidthBeforeAfter.Count -ne 2 -or + @($scene.naturalWidthBeforeAfter | Where-Object {$_ -isnot [long] -or $_ -ne 250}).Count) { + throw 'DPI-parametric matched scene requires exact 250 logical-pixel source widths.' + } + } + foreach ($binary in @('daemon','backendFixture')) { + if ($Job.binaries.$binary.sha256 -cnotmatch '^[A-Fa-f0-9]{64}$') { + throw 'DPI-parametric matched scene requires hash-bound daemon and backend fixture.' + } + } + if ($Job.binaries.backendFixture.networkAllowed -cne $false) { + throw 'DPI-parametric matched backend fixture must prohibit network.' + } + $seed=@('run','graphcode-11111111-1111-4111-8111-000000000001','-d', + 'cmd.exe','/Q','/K','echo Offline card fixture - no backend') + if (($Job.binaries.offlineZmx.seedArgumentOrder -join [char]0) -cne ($seed -join [char]0)) { + throw 'DPI-parametric matched scene requires the exact cmd-only seed vector.' + } + $sourceHash=[Convert]::ToHexString([Security.Cryptography.SHA256]::HashData( + [Text.UTF8Encoding]::new($false).GetBytes((Get-CapMatchedBackendSource)))) + if ($Job.binaries.backendFixture.sourceSha256 -ne $sourceHash) { + throw 'DPI-parametric matched scene requires its canonical harmless backend source hash.' + } + return + } if ($Job.lane -ceq 'S' -and $Job.sceneAdapter -ceq 'S_MATCHED_V12_V1') { Assert-CapDataRecord $Job @{authority=[pscustomobject];captureContract=[pscustomobject];scenes=[array] fixture=[pscustomobject];binaries=[pscustomobject]} 'Matched scene' @@ -5138,7 +5378,10 @@ Export-ModuleMember -Function @( 'Assert-CapRootProcess', 'Assert-CapUiaProcessIds', 'Invoke-CapAction', + 'Get-CapLogicalZoomEvidence', 'Measure-CapZoom', + 'Get-CapDpiGeometry', + 'Assert-CapMatchedDpiImage', 'Get-CapWindowMetrics', 'Set-CapWindowPosition', 'Find-CapTopLevelWindow',