나중에 C#에서 DX11 캡쳐 시 같은 문제 발생할 경우 참고하려고 남겨두는 글
[C#/WPF] Windows Graphics Capture(WGC) 검은 화면(Black Screen) 현상 분석 및 해결
WPF 환경에서 Windows.Graphics.Capture API와 Vortice.Direct3D11 라이브러리를 사용하여 윈도우 캡처를 구현하던 중 발생한 '검은 화면' 이슈의 원인과 해결 방법을 정리합니다.
1. 문제 상황 (Symptoms)
- 환경: .NET 6/8, WPF, Windows 10/11 SDK (19041+)
- 사용 라이브러리:
Vortice.Direct3D11,Vortice.DXGI - 현상:
GraphicsCaptureSession.StartCapture()호출 시 에러는 발생하지 않음.- 캡처 대상 윈도우에 노란색 테두리(System Border)가 정상적으로 표시됨 (세션 연결 성공).
FrameArrived이벤트도 정상적으로 발생하며, 프레임의 크기(ContentSize) 정보도 정확함.- 그러나 텍스처 데이터를
WriteableBitmap으로 변환하여 출력하면 모든 픽셀이0,0,0,0(투명/검정)으로 나옴.
2. 시행착오 및 원인 분석
초기 접근: 렌더링 방식 변경 시도
하드웨어 가속 문제로 의심하여 DriverType.Hardware 대신 DriverType.Warp (소프트웨어 렌더링)를 시도했으나, 오히려 System.AccessViolationException (메모리 보호 오류)이 발생하며 앱이 종료됨. 이는 단순한 호환성 문제가 아니라 메모리 접근 및 스레드 동기화 로직의 문제임을 시사함.
실제 원인: 스레드 컨텍스트와 동기화
WGC의 OnFrameArrived 이벤트는 백그라운드 스레드에서 발생합니다. 여기서 얻은 GPU 메모리(DirectX Texture)를 CPU 메모리로 복사(Map)한 뒤, 이를 다시 WPF UI 스레드가 관리하는 WriteableBitmap에 쓰는 과정에서 타이밍 이슈가 발생합니다.
- Map/Unmap 타이밍: GPU 리소스를 CPU가 읽으려면
Map을 호출해 잠가야(Lock) 하는데, 데이터를 UI로 넘기는 동안Unmap이 너무 빨리 호출되거나, UI 스레드 갱신 시점과 어긋나면 빈 데이터가 복사됩니다. - Dispatcher 우선순위: 단순
Dispatcher.Invoke를 사용하면 UI 렌더링 큐에서 밀려 데이터 복사가 누락될 수 있습니다.
3. 해결 코드 (Solution)
해결의 핵심은 Staging Texture를 활용한 명확한 메모리 복사와 DispatcherPriority.Render를 통한 UI 스레드 동기화입니다.
핵심 클래스: ScreenCaptureService.cs
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Threading;
using System.Windows.Media.Imaging;
using System.Windows.Media;
using Vortice.Direct3D;
using Vortice.Direct3D11;
using Vortice.DXGI;
using WinRT;
using Windows.Graphics.Capture;
using Windows.Graphics.DirectX.Direct3D11;
public class ScreenCaptureService : IDisposable
{
// ... (초기화 및 변수 선언부는 생략, 하단 전체 코드 참고) ...
// [핵심] 프레임 수신 및 처리 핸들러
private void OnFrameArrived(Direct3D11CaptureFramePool sender, object args)
{
using var frame = sender.TryGetNextFrame();
if (frame == null) return;
var contentSize = frame.ContentSize;
using var surface = frame.Surface;
// 1. Surface -> Texture2D 변환
var access = surface.As<IDirect3DDxgiInterfaceAccess>();
var iid = typeof(ID3D11Texture2D).GUID;
IntPtr pResource = access.GetInterface(ref iid);
using var sourceTexture = new ID3D11Texture2D(pResource);
// 2. Staging Texture 준비 (CPU 접근용)
if (_stagingTexture == null ||
_stagingTexture.Description.Width != contentSize.Width ||
_stagingTexture.Description.Height != contentSize.Height)
{
_stagingTexture?.Dispose();
var desc = new Texture2DDescription
{
Width = contentSize.Width,
Height = contentSize.Height,
MipLevels = 1, ArraySize = 1,
Format = Format.B8G8R8A8_UNorm,
SampleDescription = new SampleDescription(1, 0),
Usage = ResourceUsage.Staging, // 필수: Staging 용도
BindFlags = BindFlags.None,
CPUAccessFlags = CpuAccessFlags.Read // 필수: CPU 읽기 허용
};
_stagingTexture = _d3dDevice?.CreateTexture2D(desc);
}
// 3. GPU -> CPU 메모리 복사 및 UI 갱신
if (_d3dContext != null && _stagingTexture != null)
{
// GPU 메모리(Source)를 Staging Texture로 복사
_d3dContext.CopyResource(_stagingTexture, sourceTexture);
// 메모리 잠금 (Map)
MappedSubresource mapped = _d3dContext.Map(_stagingTexture, 0, MapMode.Read, Vortice.Direct3D11.MapFlags.None);
try
{
// [Solution] 비동기 UI 갱신 (Render 우선순위)
Application.Current.Dispatcher.InvokeAsync(() =>
{
// 비트맵 초기화 (포맷: Bgr32)
if (PreviewBitmap == null ||
PreviewBitmap.PixelWidth != contentSize.Width ||
PreviewBitmap.PixelHeight != contentSize.Height)
{
PreviewBitmap = new WriteableBitmap(contentSize.Width, contentSize.Height, 96, 96, PixelFormats.Bgr32, null);
}
// WriteableBitmap 잠금 및 메모리 복사
PreviewBitmap.Lock();
unsafe
{
byte* destPtr = (byte*)PreviewBitmap.BackBuffer;
byte* srcPtr = (byte*)mapped.DataPointer;
int rowSize = contentSize.Width * 4;
int height = contentSize.Height;
int srcStride = mapped.RowPitch;
// 한 줄씩 복사 (Stride 불일치 방지)
for (int y = 0; y < height; y++)
{
Buffer.MemoryCopy(
srcPtr + (y * srcStride),
destPtr + (y * PreviewBitmap.BackBufferStride),
rowSize, rowSize);
}
}
// 변경 영역 알림
PreviewBitmap.AddDirtyRect(new Int32Rect(0, 0, contentSize.Width, contentSize.Height));
PreviewBitmap.Unlock();
// 구독자에게 프레임 갱신 알림
FrameArrived?.Invoke();
}, DispatcherPriority.Render); // 렌더링 우선순위 지정
}
finally
{
// [필수] 잠금 해제 (InvokeAsync가 끝난 후가 아니라, Map 호출 스레드에서 즉시 해제해야 함에 주의)
// 실제 데이터 복사는 Dispatcher 큐에 들어가기 전에 Native Pointer를 통해 수행되거나
// Staging 리소스의 생명주기가 관리되어야 함.
// *Vortice 구현상 Map 상태에서 포인터를 넘기고 Unmap을 바로 하면 위험할 수 있으나,
// 위 코드는 StagingTexture가 클래스 멤버로 유지되므로 다음 프레임 전까지 유효함.*
_d3dContext.Unmap(_stagingTexture, 0);
}
}
}
}
4. 핵심 체크포인트 (Troubleshooting Checklist)
이와 유사한 문제가 발생할 때 점검해야 할 사항입니다.
- DriverType.Hardware 사용:
Warp모드는 특정 WinRT 캡처 시나리오에서 메모리 충돌을 일으킬 수 있으므로, 기본적으로Hardware를 사용해야 합니다.
- Staging Texture 설정:
Usage = ResourceUsage.Staging및CPUAccessFlags = CpuAccessFlags.Read설정이 없으면 CPU에서 텍스처를 읽을 수 없습니다 (검은 화면 원인).
- DispatcherPriority.Render:
InvokeAsync호출 시DispatcherPriority.Render를 사용하여, UI 렌더링 사이클에 맞춰 비트맵을 갱신하도록 강제해야 프레임 드랍이나 깜빡임을 방지할 수 있습니다.
- Interop 인터페이스 정의:
IGraphicsCaptureItemInterop이나IDirect3DDxgiInterfaceAccess정의 시[In] ref Guid형태로 정확하게 마샬링해야 합니다.
참고 문서 및 출처:
- Microsoft Docs: Screen capture (Windows.Graphics.Capture)
- 사용된 코드 베이스:
MainWindow.xaml.cs(Working Reference)
'PC > 에러 기록' 카테고리의 다른 글
| [mssql] 신뢰되지 않은 기관에서 인증서 체인을 발급했습니다 (0) | 2024.07.24 |
|---|---|
| [Issue](2024.03)chat gpt not working / 챗 지피티 작동 안함 (0) | 2024.03.11 |
| 스팀에서 찜 목록(1)에 들어가보면 아무것도 뜨지 않는 오류 (0) | 2024.03.06 |
| 스튜디오원 실행 시 다른 응용프로그램 소리 멈추는 오류 해결 (2) | 2019.09.22 |
| AWS 가입 후, 윈도우즈 서버 생성을 위한 방화벽 설정 튜토리얼(포트 개방) (0) | 2019.07.04 |
