我正在开发一个应用程序,使用 DXGI 输出复制和 C# 中的 Direct3D 从桌面捕获屏幕截图。我的应用程序初始化 DXGI、创建 Direct3D 设备并复制输出。它捕获帧,但捕获的图像数据全为零。下面是我的应用程序的完整代码,旨在将屏幕截图保存到 PNG 文件:
using Silk.NET.Core.Native;
using Silk.NET.Direct3D11;
using Silk.NET.DXGI;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace TestApp;
public class Program
{
public const int DXGI_ERROR_NOT_FOUND = unchecked((int)0x887A0002);
public unsafe static void Main()
{
Console.WriteLine("Initializing DXGI...");
using var dxgi = DXGI.GetApi();
using var factory = dxgi.CreateDXGIFactory1<IDXGIFactory1>();
Console.WriteLine("DXGI Factory created.");
Console.WriteLine("Enumerating adapters and finding active outputs...");
uint adapterIndex = 0;
IDXGIAdapter1* adapter = null;
IDXGIOutput* activeOutput = null;
var foundActiveOutput = false;
while (!foundActiveOutput && factory.EnumAdapters1(adapterIndex, &adapter) != DXGI_ERROR_NOT_FOUND)
{
AdapterDesc1 desc;
adapter->GetDesc1(&desc);
var adapterDescription = Marshal.PtrToStringUni((nint)desc.Description);
Console.WriteLine($"Adapter {adapterIndex}: {adapterDescription}, VendorId: {desc.VendorId}, DeviceId: {desc.DeviceId}, SubSysId: {desc.SubSysId}, Revision: {desc.Revision}");
using var adapterCom = new ComPtr<IDXGIAdapter1>(adapter);
uint outputIndex = 0;
IDXGIOutput* output = null;
while (!foundActiveOutput && adapter->EnumOutputs(outputIndex, &output) != DXGI_ERROR_NOT_FOUND)
{
using var outputCom = new ComPtr<IDXGIOutput>(output);
OutputDesc outputDesc;
output->GetDesc(&outputDesc);
var outputName = Marshal.PtrToStringUni((nint)outputDesc.DeviceName);
Console.WriteLine($"Output {outputIndex} on Adapter {adapterIndex}: {outputName}, Attached to Desktop: {(bool)outputDesc.AttachedToDesktop}, Rotation: {outputDesc.Rotation}");
if (outputDesc.AttachedToDesktop)
{
Console.WriteLine($"Active output found on adapter {adapterIndex}, resolution: {outputDesc.DesktopCoordinates.Max.X - outputDesc.DesktopCoordinates.Min.X} x {outputDesc.DesktopCoordinates.Max.Y - outputDesc.DesktopCoordinates.Min.Y}");
activeOutput = output;
foundActiveOutput = true;
}
outputIndex++;
}
adapterIndex++;
}
if (!foundActiveOutput)
{
Console.WriteLine("No active output found.");
return;
}
using var activeOutputCom = new ComPtr<IDXGIOutput>(activeOutput);
OutputDesc activeDesc;
activeOutput->GetDesc(&activeDesc);
Console.WriteLine($"Selected Output: {Marshal.PtrToStringUni((nint)activeDesc.DeviceName)}, Total Size: {activeDesc.DesktopCoordinates.Max.X - activeDesc.DesktopCoordinates.Min.X} x {activeDesc.DesktopCoordinates.Max.Y - activeDesc.DesktopCoordinates.Min.Y}");
Console.WriteLine("Creating Direct3D device...");
D3DFeatureLevel[] featureLevels =
[
D3DFeatureLevel.Level121,
D3DFeatureLevel.Level120,
D3DFeatureLevel.Level111,
D3DFeatureLevel.Level110,
D3DFeatureLevel.Level101,
D3DFeatureLevel.Level100,
D3DFeatureLevel.Level93,
D3DFeatureLevel.Level92,
D3DFeatureLevel.Level91
];
ID3D11Device* pDevice = null;
ID3D11DeviceContext* pImmediateContext = null;
D3DFeatureLevel chosenFeatureLevel;
var hr = -1;
foreach (var level in featureLevels)
{
hr = D3D11.GetApi().CreateDevice((IDXGIAdapter*)adapter, D3DDriverType.Unknown, IntPtr.Zero, (uint)CreateDeviceFlag.BgraSupport, &level, 1, D3D11.SdkVersion, &pDevice, &chosenFeatureLevel, &pImmediateContext);
if (hr == 0)
{
Console.WriteLine($"Successfully created device with feature level {chosenFeatureLevel}");
break;
}
}
if (hr != 0)
{
Console.WriteLine($"Failed to create device: HR = {hr:X}");
return;
}
var textureDesc = new Texture2DDesc
{
Width = (uint)(activeDesc.DesktopCoordinates.Max.X - activeDesc.DesktopCoordinates.Min.X),
Height = (uint)(activeDesc.DesktopCoordinates.Max.Y - activeDesc.DesktopCoordinates.Min.Y),
MipLevels = 1,
ArraySize = 1,
Format = Format.FormatR8G8B8A8Unorm,
SampleDesc = new SampleDesc
{
Count = 1,
Quality = 0
},
Usage = Usage.Staging,
BindFlags = 0,
CPUAccessFlags = (uint)CpuAccessFlag.Read,
MiscFlags = 0
};
ID3D11Texture2D* stagingTexture = null;
pDevice->CreateTexture2D(&textureDesc, null, &stagingTexture);
Console.WriteLine("Staging texture created with dimensions: " + textureDesc.Width + "x" + textureDesc.Height + ", Format: " + textureDesc.Format);
IDXGIOutput1* output1 = (IDXGIOutput1*)activeOutput;
IDXGIOutputDuplication* duplicatedOutput;
hr = output1->DuplicateOutput((IUnknown*)pDevice, &duplicatedOutput);
if (hr < 0)
{
Console.WriteLine($"Failed to duplicate output, HR = {hr:X}");
return;
}
Console.WriteLine("Output duplicated successfully.");
OutduplFrameInfo frameInfo;
IDXGIResource* desktopResource = null;
hr = duplicatedOutput->AcquireNextFrame(1000, &frameInfo, &desktopResource);
if (hr < 0)
{
Console.WriteLine($"Failed to acquire next frame, HR = {hr:X}, Status: {frameInfo.LastPresentTime}, TotalFrames: {frameInfo.TotalMetadataBufferSize}");
return;
}
Console.WriteLine($"Next frame acquired, Last Present Time: {frameInfo.LastPresentTime}, Total Metadata Buffer Size: {frameInfo.TotalMetadataBufferSize}");
ID3D11Texture2D* desktopTexture;
hr = desktopResource->QueryInterface(SilkMarshal.GuidPtrOf<ID3D11Texture2D>(), (void**)&desktopTexture);
if (hr < 0)
{
Console.WriteLine($"Failed to query interface for ID3D11Texture2D, HR = {hr:X}");
return;
}
Console.WriteLine("ID3D11Texture2D interface obtained.");
pImmediateContext->CopyResource((ID3D11Resource*)stagingTexture, (ID3D11Resource*)desktopTexture);
Console.WriteLine("Resources copied.");
MappedSubresource mappedResource;
hr = pImmediateContext->Map((ID3D11Resource*)stagingTexture, 0, Map.Read, 0, &mappedResource);
if (hr >= 0)
{
Console.WriteLine("Staging texture mapped successfully.");
var dataPtr = (byte*)mappedResource.PData;
var stride = mappedResource.RowPitch;
Console.WriteLine($"Data pointer: {(long)dataPtr}, Stride: {stride}");
var allZero = true;
for (var i = 0; i < 100; i++)
{
if (dataPtr[i] != 0)
{
allZero = false;
break;
}
}
Console.WriteLine(allZero ? "Data is all zeros." : "Data contains non-zero values.");
using (var bitmap = new Bitmap((int)textureDesc.Width, (int)textureDesc.Height, (int)stride, PixelFormat.Format32bppArgb, (nint)dataPtr))
{
var filePath = @"C:\Users\vitaly\Desktop\screenshot.png";
bitmap.Save(filePath, ImageFormat.Png);
Console.WriteLine($"Screenshot saved to {filePath}.");
}
pImmediateContext->Unmap((ID3D11Resource*)stagingTexture, 0);
}
else
{
Console.WriteLine($"Failed to map staging texture, HR = {hr:X}");
}
Console.WriteLine("Releasing resources.");
if (pDevice != null)
{
pDevice->Release();
}
if (pImmediateContext != null)
{
pImmediateContext->Release();
}
Console.WriteLine("Resources released.");
}
}
输出显示一切都正确初始化,并且该帧据称是使用 DXGI 输出复制 API 捕获的。但是,当我映射暂存纹理并检查数据时,所有字节均为零。尽管纹理描述和复制似乎设置正确,但还是会发生这种情况。
控制台输出表示找到设备并输出,创建和执行过程中没有报错:
Initializing DXGI...
DXGI Factory created.
Enumerating adapters and finding active outputs...
Adapter 0: Intel(R) HD Graphics 5500, VendorId: 32902, DeviceId: 5654, SubSysId: 443355203, Revision: 9
Output 0 on Adapter 0: \\.\DISPLAY1, Attached to Desktop: True, Rotation: ModeRotationIdentity
Active output found on adapter 0, resolution: 1366 x 768
Selected Output: \\.\DISPLAY1, Total Size: 1366 x 768
Creating Direct3D device...
Successfully created device with feature level D3DFeatureLevel111
Staging texture created with dimensions: 1366x768, Format: FormatR8G8B8A8Unorm
Output duplicated successfully.
Next frame acquired, Last Present Time: 0, Total Metadata Buffer Size: 0
ID3D11Texture2D interface obtained.
Resources copied.
Staging texture mapped successfully.
Data pointer: 2005097365504, Stride: 5504
Data is all zeros.
Screenshot saved to C:\Users\vitaly\Desktop\screenshot.png.
Releasing resources.
Resources released.
在编写 DirectX(D3D、DXGI、Direct2D 等)时必须首先执行的操作是启用 DirectX 调试层。完成后,当在调试模式下运行代码时,您将在调试输出中看到这一点:
D3D11 错误:ID3D11DeviceContext::CopyResource:无法调用 当每个资源的格式不相同或位于时复制资源 彼此之间的可转换性最低,除非压缩了一种格式 (DXGI_FORMAT_R9G9B9E5_SHAREDEXP,或DXGI_FORMAT_BC[1,2,3,4,5,6,7]_*) 源格式与目标格式类似,根据: BC[1|4] ~= R16G16B16A16|R32G32,BC[2|3|5|6|7] ~= R32G32B32A32,R9G9B9E5_SHAREDEXP 〜= R32。 [资源操作错误#284: 复制资源_无效源]
这是宝贵的信息。在不确定纹理 (
desktopTexture
) 是否具有相同格式的情况下,您永远不应该将它们复制到另一个 (stagingTexture
),而事实上它们并不具有相同的格式。
所以,改变这个:
var textureDesc = new Texture2DDesc
{
...
Format = Format.FormatR8G8B8A8Unorm,
...
};
进入这个:
var textureDesc = new Texture2DDesc
{
...
Format = Format.FormatB8G8R8A8Unorm,
...
};
现在错误消失了,但资源中仍然全为零。原因是DXGI输出复制不是一个屏幕截图API,它是一个API,当一个新的桌面框架可用时,获取(又名复制)一个新的桌面框架,所以它在“一段时间内”确实是有意义的。例如,当屏幕上没有任何移动时,不会获取任何帧(并且您将收到超时,这是预期的)。
因此,在代码中查看其工作的最简单方法是在循环中运行获取代码,或者只是在
AcquireNextFrame
调用之前添加一个计时器。
Thread.Sleep(200); // wait a bit here
hr = duplicatedOutput->AcquireNextFrame(1000, &frameInfo, &desktopResource);