Skip to content

Commit 9c5d647

Browse files
gameknifeclaude
andcommitted
RemotePlay P2: Vulkan Video infrastructure (extensions, encode queue, caps probe)
Per docs/WebRTC-RemotePlay-HWEncode-Plan.md P2: - Remote/VulkanVideoCaps: physical-device probe of VK_KHR_video_encode_h264 (extensions, encode queue family, H.264 profile CBP->Main->High, coded extent/granularity/DPB limits, NV12 ENCODE_SRC and ENCODE_SRC+STORAGE) - Device extensions + synchronization2 feature enabled only in --remote with encoder auto/vulkan and a usable probe; otherwise behavior is unchanged - Device gains a video encode queue (VideoEncodeQueue/VideoEncodeFamilyIndex, UINT32_MAX/null when absent); DeviceProcedures gains the video session, coding and encode entry points - New --remote-encoder {auto,vulkan,openh264} option Probed on RTX (queueFamily=4, ConstrainedBaseline, up to 4096x4096, granularity 16x16, NV12 ENCODE_SRC ok, ENCODE_SRC+STORAGE not supported -> P4 will use the plane-copy fallback). openh264 forcing and the P1 software path verified unchanged (browser 30fps), unit tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 21c6fd6 commit 9c5d647

10 files changed

Lines changed: 372 additions & 2 deletions

File tree

src/Engine/Options.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Options::Options(const int argc, const char* argv[])
4444
("remote-bitrate", "Remote Play starting video bitrate in kbps.", cxxopts::value<uint32_t>(RemoteBitrateKbps)->default_value("4000"))
4545
("remote-fps", "Remote Play target stream frame rate.", cxxopts::value<uint32_t>(RemoteFps)->default_value("30"))
4646
("remote-res", "Remote Play encode resolution, e.g. 1280x720. Empty means source resolution.", cxxopts::value<std::string>(remoteResolution)->default_value(""))
47+
("remote-encoder", "Remote Play video encoder: auto, vulkan or openh264.", cxxopts::value<std::string>(RemoteEncoder)->default_value("auto"))
4748
("keep-cpu-mesh-data", "Keep CPU mesh data for editor mode.", cxxopts::value<bool>(KeepCPUMeshData)->default_value("false"))
4849
("update-baseline", "Update visual test baseline images from the current run.", cxxopts::value<bool>(UpdateVisualTestBaseline)->default_value("false")->implicit_value("true"))
4950
("flappy-replay", "Run Flappy deterministic replay and write trace output.", cxxopts::value<bool>(FlappyReplay)->default_value("false")->implicit_value("true"))
@@ -112,6 +113,10 @@ Options::Options(const int argc, const char* argv[])
112113
{
113114
Throw(std::out_of_range("Remote Play ports must be in range 0..65535."));
114115
}
116+
if (RemoteEncoder != "auto" && RemoteEncoder != "vulkan" && RemoteEncoder != "openh264")
117+
{
118+
Throw(std::out_of_range("Invalid --remote-encoder. Expected auto, vulkan or openh264."));
119+
}
115120
}
116121

117122
if (PresentMode > 3)

src/Engine/Options.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class Options final
4646
uint32_t RemoteFps{30};
4747
uint32_t RemoteWidth{};
4848
uint32_t RemoteHeight{};
49+
std::string RemoteEncoder{"auto"};
4950
bool KeepCPUMeshData{}; // 保留CPU网格数据(编辑器模式需要)
5051
bool UpdateVisualTestBaseline{};
5152
bool FlappyReplay{};

src/Engine/Rendering/VulkanBaseRenderer.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,38 @@ namespace Vulkan
546546
nextDeviceFeatures = &rayQueryFeatures;
547547
}
548548

549+
// Vulkan Video H.264 encode (remote play hardware path). Probe before device creation;
550+
// the feature struct must outlive ctx_.device.reset().
551+
VkPhysicalDeviceSynchronization2FeaturesKHR synchronization2Features = {};
552+
if (GOption->RemoteMode && GOption->RemoteEncoder != "openh264")
553+
{
554+
videoCaps_ = Runtime::Remote::FVulkanVideoCaps::Probe(ctx_.instance->Handle(), physicalDevice);
555+
videoCaps_.LogSummary();
556+
if (videoCaps_.Usable())
557+
{
558+
requiredExtensions.insert(requiredExtensions.end(),
559+
{
560+
VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME,
561+
VK_KHR_VIDEO_QUEUE_EXTENSION_NAME,
562+
VK_KHR_VIDEO_ENCODE_QUEUE_EXTENSION_NAME,
563+
VK_KHR_VIDEO_ENCODE_H264_EXTENSION_NAME,
564+
});
565+
if (videoCaps_.maintenance1Present)
566+
{
567+
requiredExtensions.push_back(VK_KHR_VIDEO_MAINTENANCE_1_EXTENSION_NAME);
568+
}
569+
synchronization2Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES_KHR;
570+
synchronization2Features.pNext = nextDeviceFeatures;
571+
synchronization2Features.synchronization2 = true;
572+
nextDeviceFeatures = &synchronization2Features;
573+
}
574+
else if (GOption->RemoteEncoder == "vulkan")
575+
{
576+
SPDLOG_WARN("RemotePlay: --remote-encoder vulkan requested but Vulkan Video H.264 encode is not "
577+
"usable on this device; falling back to openh264");
578+
}
579+
}
580+
549581
VkPhysicalDeviceFeatures supportedFeatures = {};
550582
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
551583

src/Engine/Rendering/VulkanBaseRenderer.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include "Engine/Assets/AssetsFwd.hpp"
4+
#include "Engine/Runtime/Remote/VulkanVideoCaps.hpp"
45
#include "Engine/Vulkan/RenderingPipeline.hpp"
56
#include "Engine/Vulkan/SyncAndTiming.hpp"
67
#include "Engine/Vulkan/GpuResources.hpp"
@@ -102,6 +103,7 @@ namespace Vulkan
102103
bool HasFullAmbientCubeBudget() const { return caps_.fullAmbientCubeBudget; }
103104
void SetDenoiserEnabled(bool enabled) { caps_.supportDenoiser = enabled; }
104105
void SetVisualDebugEnabled(bool enabled) { visualDebug_ = enabled; }
106+
const Runtime::Remote::FVulkanVideoCaps& VideoCaps() const { return videoCaps_; }
105107

106108
// Engine callbacks
107109
struct Delegates
@@ -247,6 +249,7 @@ namespace Vulkan
247249
};
248250

249251
DeviceCaps caps_;
252+
Runtime::Remote::FVulkanVideoCaps videoCaps_;
250253
DeviceContext ctx_;
251254
FrameResources frame_;
252255
SkinnedMeshResources skin_;
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
#include "Engine/Common/CoreMinimal.hpp"
2+
#include "Engine/Runtime/Remote/VulkanVideoCaps.hpp"
3+
4+
#include <array>
5+
#include <cstring>
6+
#include <vector>
7+
8+
#include <spdlog/spdlog.h>
9+
10+
namespace Runtime::Remote
11+
{
12+
namespace
13+
{
14+
bool HasExtension(const std::vector<VkExtensionProperties>& available, const char* name)
15+
{
16+
for (const auto& extension : available)
17+
{
18+
if (std::strcmp(extension.extensionName, name) == 0)
19+
{
20+
return true;
21+
}
22+
}
23+
return false;
24+
}
25+
26+
const char* ProfileIdcName(int32_t idc)
27+
{
28+
switch (idc)
29+
{
30+
case STD_VIDEO_H264_PROFILE_IDC_BASELINE: return "ConstrainedBaseline";
31+
case STD_VIDEO_H264_PROFILE_IDC_MAIN: return "Main";
32+
case STD_VIDEO_H264_PROFILE_IDC_HIGH: return "High";
33+
default: return "none";
34+
}
35+
}
36+
37+
VkVideoProfileInfoKHR MakeProfile(VkVideoEncodeH264ProfileInfoKHR& h264Profile,
38+
StdVideoH264ProfileIdc profileIdc)
39+
{
40+
h264Profile = {};
41+
h264Profile.sType = VK_STRUCTURE_TYPE_VIDEO_ENCODE_H264_PROFILE_INFO_KHR;
42+
h264Profile.stdProfileIdc = profileIdc;
43+
44+
VkVideoProfileInfoKHR profile{};
45+
profile.sType = VK_STRUCTURE_TYPE_VIDEO_PROFILE_INFO_KHR;
46+
profile.pNext = &h264Profile;
47+
profile.videoCodecOperation = VK_VIDEO_CODEC_OPERATION_ENCODE_H264_BIT_KHR;
48+
profile.chromaSubsampling = VK_VIDEO_CHROMA_SUBSAMPLING_420_BIT_KHR;
49+
profile.lumaBitDepth = VK_VIDEO_COMPONENT_BIT_DEPTH_8_BIT_KHR;
50+
profile.chromaBitDepth = VK_VIDEO_COMPONENT_BIT_DEPTH_8_BIT_KHR;
51+
return profile;
52+
}
53+
}
54+
55+
uint32_t FVulkanVideoCaps::FindEncodeH264QueueFamily(VkPhysicalDevice physicalDevice)
56+
{
57+
uint32_t count = 0;
58+
vkGetPhysicalDeviceQueueFamilyProperties2(physicalDevice, &count, nullptr);
59+
if (count == 0)
60+
{
61+
return UINT32_MAX;
62+
}
63+
64+
std::vector<VkQueueFamilyVideoPropertiesKHR> videoProps(count);
65+
std::vector<VkQueueFamilyProperties2> props(count);
66+
for (uint32_t i = 0; i < count; ++i)
67+
{
68+
videoProps[i] = {};
69+
videoProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_VIDEO_PROPERTIES_KHR;
70+
props[i] = {};
71+
props[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2;
72+
props[i].pNext = &videoProps[i];
73+
}
74+
vkGetPhysicalDeviceQueueFamilyProperties2(physicalDevice, &count, props.data());
75+
76+
for (uint32_t i = 0; i < count; ++i)
77+
{
78+
if ((props[i].queueFamilyProperties.queueFlags & VK_QUEUE_VIDEO_ENCODE_BIT_KHR) != 0 &&
79+
(videoProps[i].videoCodecOperations & VK_VIDEO_CODEC_OPERATION_ENCODE_H264_BIT_KHR) != 0)
80+
{
81+
return i;
82+
}
83+
}
84+
return UINT32_MAX;
85+
}
86+
87+
FVulkanVideoCaps FVulkanVideoCaps::Probe(VkInstance instance, VkPhysicalDevice physicalDevice)
88+
{
89+
FVulkanVideoCaps caps;
90+
91+
uint32_t extensionCount = 0;
92+
vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, nullptr);
93+
std::vector<VkExtensionProperties> extensions(extensionCount);
94+
vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, extensions.data());
95+
96+
caps.extensionsPresent = HasExtension(extensions, VK_KHR_VIDEO_QUEUE_EXTENSION_NAME) &&
97+
HasExtension(extensions, VK_KHR_VIDEO_ENCODE_QUEUE_EXTENSION_NAME) &&
98+
HasExtension(extensions, VK_KHR_VIDEO_ENCODE_H264_EXTENSION_NAME) &&
99+
HasExtension(extensions, VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME);
100+
caps.maintenance1Present = HasExtension(extensions, VK_KHR_VIDEO_MAINTENANCE_1_EXTENSION_NAME);
101+
if (!caps.extensionsPresent)
102+
{
103+
return caps;
104+
}
105+
106+
caps.encodeQueueFamily = FindEncodeH264QueueFamily(physicalDevice);
107+
if (caps.encodeQueueFamily == UINT32_MAX)
108+
{
109+
return caps;
110+
}
111+
112+
const auto pfnGetVideoCapabilities = reinterpret_cast<PFN_vkGetPhysicalDeviceVideoCapabilitiesKHR>(
113+
vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceVideoCapabilitiesKHR"));
114+
const auto pfnGetVideoFormatProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceVideoFormatPropertiesKHR>(
115+
vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceVideoFormatPropertiesKHR"));
116+
if (!pfnGetVideoCapabilities || !pfnGetVideoFormatProperties)
117+
{
118+
return caps;
119+
}
120+
121+
// Prefer ConstrainedBaseline (matches the default 42e01f SDP fmtp), then Main, then High.
122+
constexpr std::array<StdVideoH264ProfileIdc, 3> profileCandidates = {
123+
STD_VIDEO_H264_PROFILE_IDC_BASELINE,
124+
STD_VIDEO_H264_PROFILE_IDC_MAIN,
125+
STD_VIDEO_H264_PROFILE_IDC_HIGH,
126+
};
127+
128+
VkVideoEncodeH264ProfileInfoKHR h264Profile{};
129+
VkVideoProfileInfoKHR profile{};
130+
for (const StdVideoH264ProfileIdc candidate : profileCandidates)
131+
{
132+
profile = MakeProfile(h264Profile, candidate);
133+
134+
VkVideoEncodeH264CapabilitiesKHR h264Caps{};
135+
h264Caps.sType = VK_STRUCTURE_TYPE_VIDEO_ENCODE_H264_CAPABILITIES_KHR;
136+
VkVideoEncodeCapabilitiesKHR encodeCaps{};
137+
encodeCaps.sType = VK_STRUCTURE_TYPE_VIDEO_ENCODE_CAPABILITIES_KHR;
138+
encodeCaps.pNext = &h264Caps;
139+
VkVideoCapabilitiesKHR videoCaps{};
140+
videoCaps.sType = VK_STRUCTURE_TYPE_VIDEO_CAPABILITIES_KHR;
141+
videoCaps.pNext = &encodeCaps;
142+
143+
if (pfnGetVideoCapabilities(physicalDevice, &profile, &videoCaps) != VK_SUCCESS)
144+
{
145+
continue;
146+
}
147+
148+
caps.h264Supported = true;
149+
caps.profileIdc = candidate;
150+
caps.minExtent = videoCaps.minCodedExtent;
151+
caps.maxExtent = videoCaps.maxCodedExtent;
152+
caps.pictureAccessGranularity = videoCaps.pictureAccessGranularity;
153+
caps.maxDpbSlots = videoCaps.maxDpbSlots;
154+
caps.maxActiveReferencePictures = videoCaps.maxActiveReferencePictures;
155+
caps.minBitstreamBufferOffsetAlignment = videoCaps.minBitstreamBufferOffsetAlignment;
156+
caps.minBitstreamBufferSizeAlignment = videoCaps.minBitstreamBufferSizeAlignment;
157+
break;
158+
}
159+
if (!caps.h264Supported)
160+
{
161+
return caps;
162+
}
163+
164+
// ENCODE_SRC format support for NV12, with and without STORAGE for the zero-copy path.
165+
const auto queryNv12 = [&](VkImageUsageFlags usage) -> bool
166+
{
167+
VkVideoProfileListInfoKHR profileList{};
168+
profileList.sType = VK_STRUCTURE_TYPE_VIDEO_PROFILE_LIST_INFO_KHR;
169+
profileList.profileCount = 1;
170+
profileList.pProfiles = &profile;
171+
172+
VkPhysicalDeviceVideoFormatInfoKHR formatInfo{};
173+
formatInfo.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VIDEO_FORMAT_INFO_KHR;
174+
formatInfo.pNext = &profileList;
175+
formatInfo.imageUsage = usage;
176+
177+
uint32_t formatCount = 0;
178+
if (pfnGetVideoFormatProperties(physicalDevice, &formatInfo, &formatCount, nullptr) != VK_SUCCESS ||
179+
formatCount == 0)
180+
{
181+
return false;
182+
}
183+
std::vector<VkVideoFormatPropertiesKHR> formats(formatCount);
184+
for (auto& format : formats)
185+
{
186+
format = {};
187+
format.sType = VK_STRUCTURE_TYPE_VIDEO_FORMAT_PROPERTIES_KHR;
188+
}
189+
if (pfnGetVideoFormatProperties(physicalDevice, &formatInfo, &formatCount, formats.data()) != VK_SUCCESS)
190+
{
191+
return false;
192+
}
193+
for (const auto& format : formats)
194+
{
195+
if (format.format == VK_FORMAT_G8_B8R8_2PLANE_420_UNORM)
196+
{
197+
return true;
198+
}
199+
}
200+
return false;
201+
};
202+
203+
caps.nv12EncodeSrc = queryNv12(VK_IMAGE_USAGE_VIDEO_ENCODE_SRC_BIT_KHR);
204+
caps.nv12EncodeSrcStorage =
205+
queryNv12(VK_IMAGE_USAGE_VIDEO_ENCODE_SRC_BIT_KHR | VK_IMAGE_USAGE_STORAGE_BIT);
206+
207+
return caps;
208+
}
209+
210+
void FVulkanVideoCaps::LogSummary() const
211+
{
212+
if (!extensionsPresent)
213+
{
214+
SPDLOG_INFO("RemotePlay: Vulkan Video encode unavailable (missing device extensions)");
215+
return;
216+
}
217+
if (encodeQueueFamily == UINT32_MAX)
218+
{
219+
SPDLOG_INFO("RemotePlay: Vulkan Video encode unavailable (no H.264 encode queue family)");
220+
return;
221+
}
222+
if (!h264Supported)
223+
{
224+
SPDLOG_INFO("RemotePlay: Vulkan Video encode unavailable (H.264 profile not supported)");
225+
return;
226+
}
227+
SPDLOG_INFO(
228+
"RemotePlay: Vulkan Video H.264 encode available: queueFamily={} profile={} coded={}x{}..{}x{} "
229+
"granularity={}x{} dpb={} refs={} nv12EncodeSrc={} nv12+storage={} maintenance1={}",
230+
encodeQueueFamily, ProfileIdcName(profileIdc), minExtent.width, minExtent.height, maxExtent.width,
231+
maxExtent.height, pictureAccessGranularity.width, pictureAccessGranularity.height, maxDpbSlots,
232+
maxActiveReferencePictures, nv12EncodeSrc, nv12EncodeSrcStorage, maintenance1Present);
233+
}
234+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#pragma once
2+
3+
#include <vulkan/vulkan.h>
4+
5+
#include <cstdint>
6+
7+
namespace Runtime::Remote
8+
{
9+
// Physical-device probe result for VK_KHR_video_encode_h264. Probed before logical device
10+
// creation; drives the --remote-encoder auto path selection and which device extensions get
11+
// enabled.
12+
struct FVulkanVideoCaps
13+
{
14+
bool extensionsPresent = false; // video_queue + video_encode_queue + video_encode_h264 + synchronization2
15+
bool maintenance1Present = false; // VK_KHR_video_maintenance1 (optional)
16+
uint32_t encodeQueueFamily = UINT32_MAX;
17+
18+
bool h264Supported = false;
19+
int32_t profileIdc = -1; // chosen StdVideoH264ProfileIdc
20+
VkExtent2D minExtent{0, 0};
21+
VkExtent2D maxExtent{0, 0};
22+
VkExtent2D pictureAccessGranularity{1, 1};
23+
uint32_t maxDpbSlots = 0;
24+
uint32_t maxActiveReferencePictures = 0;
25+
VkDeviceSize minBitstreamBufferOffsetAlignment = 1;
26+
VkDeviceSize minBitstreamBufferSizeAlignment = 1;
27+
28+
bool nv12EncodeSrc = false; // G8_B8R8_2PLANE_420 usable as ENCODE_SRC
29+
bool nv12EncodeSrcStorage = false; // ... with STORAGE usage as well (zero-copy compute write)
30+
31+
bool Usable() const
32+
{
33+
return extensionsPresent && encodeQueueFamily != UINT32_MAX && h264Supported && nv12EncodeSrc;
34+
}
35+
36+
static FVulkanVideoCaps Probe(VkInstance instance, VkPhysicalDevice physicalDevice);
37+
static uint32_t FindEncodeH264QueueFamily(VkPhysicalDevice physicalDevice);
38+
39+
void LogSummary() const;
40+
};
41+
}

0 commit comments

Comments
 (0)