Optional Vulkan Backend¶
Status: implemented as an optional offscreen presenter, a validation probe, a native Win32 swapchain probe, and an embedded Win32 child surface for the desktop shell. OpenGL/GLES remains the required desktop presenter; Vulkan is not a v1 release gate. The backend now implements xBR-lv2, FSR 1.0 (EASU + RCAS), and HQx 2x/3x/4x in addition to accurate, nearest, sharp-bilinear, and CRT.
What it is¶
FiveDo.Video.Vulkan is a pure C# Vulkan 1.0 backend that mirrors the OpenGL two-stage pipeline:
VdlFrame
|
+--> indexed pixels R16_UINT texture (max 1024x480)
+--> per-line CLUT R8G8B8A8_UNORM texture (32 x height)
+--> line parameters R32G32_UINT texture (background, out-control)
|
v
palette pass at native resolution (SPIR-V fragment shader)
|
+--> xBR-lv2 4x pass (optional)
+--> FSR EASU pass at viewport resolution (optional)
|
v
scale/filter pass at output resolution (or FSR RCAS)
|
v
B8G8R8A8_UNORM (or R8G8B8A8_UNORM) target
|
+--> host readback buffer (offscreen probe and captures)
|
+--> Win32 swapchain (desktop child surface and window probe)
The palette shader is a direct port of FrameAssembler/GpuPalette: pixel zero uses the per-line background, the fixed CLUT is selected by output-control bit 25 plus pixel bit 15, and the per-line CLUT uses independent red, green, and blue indices. A 320x240 Accurate render matches the CPU oracle exactly.
The renderer is offscreen by default. It does not create an instance surface, swapchain, or presentation queue unless InitializeSurface is called. --vk-window-probe creates a Win32 window, enables VK_KHR_surface/VK_KHR_win32_surface/VK_KHR_swapchain, and blits the rendered output image into the current swapchain image before presenting. The desktop shell hosts a native Win32 child window inside the Avalonia window through VulkanVideoSurface (--gl-backend vulkan), so the steady-state desktop path has no GPU-to-CPU readback and no WriteableBitmap upload. The offscreen readback path remains for the headless probe, screenshots, non-Windows platforms, and fallback if the child surface cannot be created. The headless probe remains the primary validation tool.
Filters¶
| Filter | Vulkan behavior |
|---|---|
Accurate | Nearest sampling, integer scaling; pixel-identical to FrameAssembler at 1:1. |
Nearest | Nearest sampling with arbitrary scale/letterbox. |
SharpBilinear | The same integer-prescale/sub-texel clamp as the OpenGL shader. |
Crt | Barrel distortion, scanlines, aperture mask, vignette, and 3x3 bloom. |
Xbr | Hyllian's xBR-lv2 4x GPU pass over the GPU-expanded native frame, finished with sharp-bilinear. |
Fsr | FSR 1.0 EASU at viewport resolution followed by the RCAS sharpening pass. |
Hqx, Hqx3x, Hqx4x | CPU FrameAssembler expansion plus HqxFilter, uploaded as a native-resolution source and finished with sharp-bilinear. CPU-bound and opt-in. |
Commands¶
Headless parity probe (320x240, 1:1 Accurate):
dotnet run -c Release --project src/FiveDo.Frontend.Avalonia -- `
--vk-probe artifacts/vk-probe/parity.json `
--vk-probe-size 320x240 --vk-probe-frames 20 --vk-probe-warmup 5 `
--vk-probe-filter accurate --vk-probe-capture artifacts/vk-probe/parity.png
4K sharp-bilinear timing:
dotnet run -c Release --project src/FiveDo.Frontend.Avalonia -- `
--vk-probe artifacts/vk-probe/4k-sharp.json `
--vk-probe-size 3840x2160 --vk-probe-frames 30 --vk-probe-warmup 10 `
--vk-probe-filter sharp-bilinear
4K xBR and FSR timing:
dotnet run -c Release --project src/FiveDo.Frontend.Avalonia -- `
--vk-probe artifacts/vk-probe/4k-xbr.json `
--vk-probe-size 3840x2160 --vk-probe-frames 60 --vk-probe-warmup 15 `
--vk-probe-filter xbr
dotnet run -c Release --project src/FiveDo.Frontend.Avalonia -- `
--vk-probe artifacts/vk-probe/4k-fsr.json `
--vk-probe-size 3840x2160 --vk-probe-frames 60 --vk-probe-warmup 15 `
--vk-probe-filter fsr
Desktop shell with the optional Vulkan path:
dotnet run -c Release --project src/FiveDo.Frontend.Avalonia -- `
--gl-backend vulkan --disc "game.chd"
Native Win32 swapchain probe (opens a real window, presents through a Vulkan swapchain, and writes the same JSON report):
dotnet run -c Release --project src/FiveDo.Frontend.Avalonia -- `
--vk-window-probe artifacts/vk-probe/window-4k.json `
--vk-window-probe-size 3840x2160 --vk-window-probe-frames 60 `
--vk-window-probe-warmup 15 --vk-window-probe-filter sharp-bilinear
Options: --vk-probe (headless output JSON), --vk-window-probe (native window output JSON), --vk-probe-size/--vk-window-probe-size (default 1920x1080), --vk-probe-frames/--vk-window-probe-frames (60), --vk-probe-warmup/--vk-window-probe-warmup (10), --vk-probe-filter/--vk-window-probe-filter (accurate, nearest, sharp-bilinear/sharp, crt, xbr, fsr, hqx, hqx3x, hqx4x, plus the OpenGL aliases), --vk-probe-capture/--vk-window-probe-capture <png>, and --vk-probe-timeout/--vk-window-probe-timeout (30 seconds). The window probe requests FIFO (vsync) presentation; the headless probe is uncapped.
Measured results¶
Development machine: Windows 11, Ryzen 9 7900, RTX 4070, Vulkan 1.4. All rows use 60 measured frames after 15 warm-up frames. The probe waits for each frame. The per-frame GPU readback is recorded only when CaptureToFrame is enabled; the probe leaves it disabled and performs a one-shot readback after the timing loop. GPU time covers the upload, palette, and filter passes, not the readback.
| Case | Present mean | Present p99 | GPU mean | GPU p99 | Managed bytes/frame |
|---|---|---|---|---|---|
| 4K sharp-bilinear | 0.240 ms | 1.060 ms | 0.075 ms | 0.091 ms | 0 |
| 4K accurate | 0.221 ms | 0.687 ms | 0.071 ms | 0.080 ms | 0 |
| 4K CRT | 0.364 ms | 1.015 ms | 0.175 ms | 0.187 ms | 0 |
| 4K xBR | 0.316 ms | 0.796 ms | 0.132 ms | 0.143 ms | 0 |
| 4K FSR (EASU + RCAS) | 0.604 ms | 1.303 ms | 0.404 ms | 0.415 ms | 0 |
| 1080p sharp-bilinear | 0.169 ms | 0.790 ms | 0.030 ms | 0.040 ms | 0 |
| 1080p xBR | 0.214 ms | 0.375 ms | 0.085 ms | 0.088 ms | 0 |
| 1080p FSR (EASU + RCAS) | 0.269 ms | 0.781 ms | 0.116 ms | 0.122 ms | 0 |
| 4K HQx 2x | 6.816 ms | 10.934 ms | 0.130 ms | 0.179 ms | 0 |
Present time includes the per-frame vkQueueWaitIdle and is therefore a synchronous end-to-end number; the OpenGL probe measures asynchronous command submission. Compare the GPU column for renderer cost.
Native Win32 swapchain probe, same machine, 60 measured frames after 15 warm-up frames, FIFO presentation on a 165 Hz display:
| Case | Present mean | Present p99 | GPU mean | GPU p99 | Managed bytes/frame |
|---|---|---|---|---|---|
| 4K sharp-bilinear, FIFO | 5.963 ms | 6.534 ms | 0.077 ms | 0.088 ms | 0 |
| 640x480 sharp-bilinear, FIFO | 5.935 ms | 6.811 ms | 0.019 ms | 0.031 ms | 0 |
Present time includes the swapchain acquire/present wait and the display refresh interval; GPU time is the comparable rendering cost. At 165 Hz the FIFO floor is ~6.06 ms, so both rows are display-paced. The window probe reports vsyncActive: true and vsyncBackend: "fifo"; a 320x240 Accurate capture still matches the CPU oracle with 0 different pixels.
320x240 Accurate parity: 0 different pixels, CPU/GPU FNV-1a hash 3602745797 (0xD6BD89C5), no vertical flip. The same hash is produced by the OpenGL WGL/ANGLE probes. A 640x480 source/output case and a source-size change (320x240 -> 640x480) also match the CPU oracle; this covers expanded-target recreation and descriptor rebinding. The xBR, FSR, and HQx probes run at 640x480 and 4K with filterFallback: false and zero managed bytes per frame. xBR samples the GPU-expanded native frame directly; the offscreen backend does not perform a CPU palette expansion or readback for it. A top-red/bottom-blue split-frame test verifies that xBR, FSR, and HQx preserve top-down orientation at 320x240 and 640x480. HQx uses the same CPU expansion as the OpenGL presenter and is CPU-bound; the 4K row above is present time, not a 60 fps claim.
These are development-machine numbers, not a hardware guarantee.
Build and packaging¶
Silk.NET.Vulkan2.23.0 provides the Vulkan bindings.XenoAtom.ShaderCompiler.Build1.2.2 compilesFrame.vert,Palette.frag,Scale.frag,Xbr.frag,FsrEasu.frag, andFsrRcas.fragto SPIR-V at build time and embeds the byte arrays in the assembly. No shader compiler or Vulkan SDK is required at runtime.- The Vulkan loader (
vulkan-1.dllon Windows,libvulkan.so.1on Linux, the MoltenVK loader on macOS) is provided by the operating system/driver; the project does not bundle it. - The desktop frontend still publishes trimmed; the Vulkan probe path has no reflection-based JSON.
Tests¶
VulkanShaderTestschecks that all six generated shader modules (frame, palette, scale, xBR, FSR EASU, FSR RCAS) are SPIR-V and thatRendererApi.Vulkanis reported correctly.VulkanRendererTests(FIVEDO_VULKAN_TESTS=1) renders 320x240 and 640x480 frames and compares them withFrameAssembler, including background, fixed-CLUT, and per-line CLUT paths. It also renders a top-red/bottom-blue split frame through xBR, FSR, and HQx at 320x240 and 640x480, asserts that all three preserve top-down orientation, and checks that HQx does not report a fallback.VulkanProbeIntegrationTests(FIVEDO_VULKAN_TESTS=1) runs the built frontend probe for 320x240 parity, a 4K sharp-bilinear pass, and 640x480 xBR/FSR/HQx passes withfilterFallback: false.VkProbeOptionsTestscovers headless/window defaults, size/frames/filter/capture parsing, shared option aliases, and invalid values.VulkanProbeIntegrationTests.WindowProbe_PresentsThroughASwapchain(FIVEDO_VULKAN_TESTS=1, Windows) opens a window and checksoffscreen: false, FIFO vsync, CPU parity, and zero managed bytes/frame. A CI-run window probe would require a desktop session and is not part of the default suite.
$env:FIVEDO_VULKAN_TESTS = "1"
dotnet test tests/FiveDo.Tests/FiveDo.Tests.csproj -c Release `
--filter "FullyQualifiedName~Vulkan"
The opt-in tests are skipped by default because CI runners do not expose a Vulkan device. The SPIR-V and option-parsing tests run everywhere.
Limitations and next steps¶
- The native swapchain is a Win32 path. The desktop shell embeds it as a native child window inside the Avalonia window; X11/Wayland/Metal surfaces are future work. The child window is outside Avalonia's compositor, so Avalonia-drawn overlays over the video rectangle (for example the diagnostic overlay) are obscured; the status bar and header remain visible. Non-Windows platforms fall back to the offscreen/readback path.
- The per-frame readback is only recorded when
CaptureToFrameis enabled; probes and screenshots use a one-shot copy. The embedded Win32 surface does not read back at all. The offscreen fallback still needs the readback every frame and remains slower than the direct OpenGL presenter. - xBR and FSR 1.0 are GPU passes. HQx 2x/3x/4x are CPU expansions (shared with the OpenGL presenter) and are CPU-bound; they are opt-in and not recommended at 4K on slow CPUs.
- The offscreen path has no vsync control. The window probe requests FIFO and reports the selected present mode; it does not yet expose mailbox/immediate as user settings.
- Vulkan 1.1+ features (storage buffers in fragment shaders, timeline semaphores) are not required; the backend requests Vulkan 1.0 and uses sampled images and push constants.
- Device/driver validation on Linux, macOS/MoltenVK, arm64, and integrated GPUs is pending.