fix(tools): preserve opaque framebuffer RGB in captures
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s

This commit is contained in:
Valentin Popov committed 2026-11-01 09:45:59 +04:00
1 parent 6c603976d4
commit f771a62298
4 files changed
+40 -12

No files matched your search

+6
View File
@@ -348,3 +348,9 @@ D3D7 в `fparkan-render`, а затем один раз меняет знак в
каналов не определяет. Последовательность не содержит структурированных ID каналов не определяет. Последовательность не содержит структурированных ID
видимых объектов, списка элементов отрисовки, ключей конвейера, матриц или видимых объектов, списка элементов отрисовки, ключей конвейера, матриц или
хэшей промежуточных буферов. хэшей промежуточных буферов.
При преобразовании кадра в PNG сохраняется исходный RGB, а прозрачность
выключается: окно показывает готовое непрозрачное изображение. Альфа-канал
GPU может содержать результат смешивания поверхностей; повторное применение
его к готовому цвету затемнило бы кадр ещё раз. Поэтому конвертер и сборщик
сравнительных пар игнорируют эту альфу, а raw-файл сохраняют без изменений.
+6
View File
@@ -117,6 +117,12 @@ the verified value for the run above, not a universal surface format. The
converter uses the installed `System.Drawing` PNG encoder and adds no package converter uses the installed `System.Drawing` PNG encoder and adds no package
dependency. dependency.
PNG exports treat the captured frame as opaque and preserve its stored RGB.
Framebuffer alpha is a rendering result, not image transparency; applying it
again when displaying the PNG would darken surfaces a second time. Raw files
retain all four original channels. The comparison script also ignores alpha
in older PNG exports, without changing the measured RGB values.
## Сравнение областей кадра ## Сравнение областей кадра
`compare-frames.ps1` собирает пару «оригинал / FParkan» без масштабирования и `compare-frames.ps1` собирает пару «оригинал / FParkan» без масштабирования и
+21 -5
View File
@@ -41,7 +41,21 @@ if ($SelfTest) {
try { Get-MeanRgb $test ([System.Drawing.Rectangle]::new(1, 0, 2, 1)) | Out-Null } try { Get-MeanRgb $test ([System.Drawing.Rectangle]::new(1, 0, 2, 1)) | Out-Null }
catch { $rejected = $true } catch { $rejected = $true }
if (-not $rejected) { throw 'Out-of-bounds region was accepted.' } if (-not $rejected) { throw 'Out-of-bounds region was accepted.' }
Write-Output 'self-test: exact region RGB means and bounds rejection OK' $test.SetPixel(0, 0, [System.Drawing.Color]::FromArgb(0, 31, 16, 24))
$test.SetPixel(1, 0, [System.Drawing.Color]::FromArgb(64, 31, 16, 24))
$opaque = $test.Clone([System.Drawing.Rectangle]::new(0, 0, 2, 1), [System.Drawing.Imaging.PixelFormat]::Format32bppRgb)
$pair = [System.Drawing.Bitmap]::new(2, 1)
$graphics = [System.Drawing.Graphics]::FromImage($pair)
try {
$graphics.Clear([System.Drawing.Color]::Black)
$graphics.DrawImageUnscaled($opaque, 0, 0)
foreach ($x in @(0, 1)) {
if ($pair.GetPixel($x, 0).ToArgb() -ne [System.Drawing.Color]::FromArgb(31, 16, 24).ToArgb()) {
throw 'Comparison applied framebuffer alpha to stored RGB.'
}
}
} finally { $graphics.Dispose(); $pair.Dispose(); $opaque.Dispose() }
Write-Output 'self-test: exact region RGB means, opaque presentation and bounds rejection OK'
} finally { $test.Dispose() } } finally { $test.Dispose() }
return return
} }
@@ -63,19 +77,21 @@ foreach ($value in $viewport) {
} }
$crop = [System.Drawing.Rectangle]::new($viewport[0], $viewport[1], $viewport[2] - $viewport[0], $viewport[3] - $viewport[1]) $crop = [System.Drawing.Rectangle]::new($viewport[0], $viewport[1], $viewport[2] - $viewport[0], $viewport[3] - $viewport[1])
$native = [System.Drawing.Bitmap]::new((Resolve-Path -LiteralPath $NativePng).Path) $native = [System.Drawing.Bitmap]::new((Resolve-Path -LiteralPath $NativePng).Path)
$viewer = $nativeCrop = $pair = $graphics = $null $viewer = $viewerOpaque = $nativeCrop = $pair = $graphics = $null
try { try {
$viewer = [System.Drawing.Bitmap]::new((Resolve-Path -LiteralPath $ViewerPng).Path) $viewer = [System.Drawing.Bitmap]::new((Resolve-Path -LiteralPath $ViewerPng).Path)
if ($crop.Width -le 0 -or $crop.Height -le 0 -or $crop.Right -gt $native.Width -or $crop.Bottom -gt $native.Height -or if ($crop.Width -le 0 -or $crop.Height -le 0 -or $crop.Right -gt $native.Width -or $crop.Bottom -gt $native.Height -or
$viewer.Width -ne $crop.Width -or $viewer.Height -ne $crop.Height) { $viewer.Width -ne $crop.Width -or $viewer.Height -ne $crop.Height) {
throw 'Native crop and viewer dimensions must match without scaling.' throw 'Native crop and viewer dimensions must match without scaling.'
} }
$nativeCrop = $native.Clone($crop, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb) # Captured frames are opaque presentations; framebuffer alpha is not PNG opacity.
$nativeCrop = $native.Clone($crop, [System.Drawing.Imaging.PixelFormat]::Format32bppRgb)
$viewerOpaque = $viewer.Clone([System.Drawing.Rectangle]::new(0, 0, $viewer.Width, $viewer.Height), [System.Drawing.Imaging.PixelFormat]::Format32bppRgb)
$pair = [System.Drawing.Bitmap]::new($viewer.Width * 2, $viewer.Height + 24) $pair = [System.Drawing.Bitmap]::new($viewer.Width * 2, $viewer.Height + 24)
$graphics = [System.Drawing.Graphics]::FromImage($pair) $graphics = [System.Drawing.Graphics]::FromImage($pair)
$graphics.Clear([System.Drawing.Color]::Black) $graphics.Clear([System.Drawing.Color]::Black)
$graphics.DrawImageUnscaled($nativeCrop, 0, 24) $graphics.DrawImageUnscaled($nativeCrop, 0, 24)
$graphics.DrawImageUnscaled($viewer, $viewer.Width, 24) $graphics.DrawImageUnscaled($viewerOpaque, $viewer.Width, 24)
$font = [System.Drawing.SystemFonts]::DefaultFont $font = [System.Drawing.SystemFonts]::DefaultFont
$graphics.DrawString('Original', $font, [System.Drawing.Brushes]::White, 4, 4) $graphics.DrawString('Original', $font, [System.Drawing.Brushes]::White, 4, 4)
$graphics.DrawString('FParkan', $font, [System.Drawing.Brushes]::White, $viewer.Width + 4, 4) $graphics.DrawString('FParkan', $font, [System.Drawing.Brushes]::White, $viewer.Width + 4, 4)
@@ -100,7 +116,7 @@ try {
ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $jsonPath -Encoding UTF8 ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $jsonPath -Encoding UTF8
Write-Output "Wrote $pngPath and $jsonPath" Write-Output "Wrote $pngPath and $jsonPath"
} finally { } finally {
foreach ($resource in @($graphics, $pair, $nativeCrop, $viewer, $native)) { foreach ($resource in @($graphics, $pair, $nativeCrop, $viewerOpaque, $viewer, $native)) {
if ($null -ne $resource) { $resource.Dispose() } if ($null -ne $resource) { $resource.Dispose() }
} }
} }
@@ -118,7 +118,7 @@ function Save-BgraPngToStream {
$bitmap = New-Object System.Drawing.Bitmap( $bitmap = New-Object System.Drawing.Bitmap(
$Width, $Width,
$Height, $Height,
[System.Drawing.Imaging.PixelFormat]::Format32bppArgb [System.Drawing.Imaging.PixelFormat]::Format32bppRgb
) )
$rectangle = New-Object System.Drawing.Rectangle(0, 0, $Width, $Height) $rectangle = New-Object System.Drawing.Rectangle(0, 0, $Width, $Height)
$bitmapData = $null $bitmapData = $null
@@ -126,7 +126,7 @@ function Save-BgraPngToStream {
$bitmapData = $bitmap.LockBits( $bitmapData = $bitmap.LockBits(
$rectangle, $rectangle,
[System.Drawing.Imaging.ImageLockMode]::WriteOnly, [System.Drawing.Imaging.ImageLockMode]::WriteOnly,
[System.Drawing.Imaging.PixelFormat]::Format32bppArgb [System.Drawing.Imaging.PixelFormat]::Format32bppRgb
) )
$rowBytes = $Width * 4 $rowBytes = $Width * 4
@@ -201,12 +201,12 @@ function Get-CameraExtent {
function Invoke-SelfTest { function Invoke-SelfTest {
$rgba = [byte[]]@( $rgba = [byte[]]@(
255, 0, 0, 255, 0, 255, 0, 255, 255, 0, 0, 0, 0, 255, 0, 64,
0, 0, 255, 255, 255, 255, 255, 255 0, 0, 255, 128, 255, 255, 255, 255
) )
$expectedBgra = [byte[]]@( $expectedBgra = [byte[]]@(
0, 0, 255, 255, 0, 255, 0, 255, 0, 0, 255, 0, 0, 255, 0, 64,
255, 0, 0, 255, 255, 255, 255, 255 255, 0, 0, 128, 255, 255, 255, 255
) )
$rgbaResult = Convert-ReadbackToBgra -Source $rgba -Format 37 -Width 2 -Height 2 $rgbaResult = Convert-ReadbackToBgra -Source $rgba -Format 37 -Width 2 -Height 2
@@ -303,7 +303,7 @@ function Invoke-SelfTest {
$stream.Dispose() $stream.Dispose()
} }
'Self-check passed (2x2 RGBA/BGRA PNG, byte-length/64 MiB limits, viewport-coordinate bounds, and cropped pixel_capture bounds).' 'Self-check passed (opaque 2x2 RGBA/BGRA PNG, byte-length/64 MiB limits, viewport-coordinate bounds, and cropped pixel_capture bounds).'
} }
if ($SelfTest) { if ($SelfTest) {