GifRecordingSession.cpp
1 //============================================================================== 2 // 3 // Zoomit 4 // Sysinternals - www.sysinternals.com 5 // 6 // GIF recording support using Windows Imaging Component (WIC) 7 // 8 //============================================================================== 9 #include "pch.h" 10 #include "GifRecordingSession.h" 11 #include "CaptureFrameWait.h" 12 #include <shcore.h> 13 14 extern DWORD g_RecordScaling; 15 16 namespace winrt 17 { 18 using namespace Windows::Foundation; 19 using namespace Windows::Graphics; 20 using namespace Windows::Graphics::Capture; 21 using namespace Windows::Graphics::DirectX; 22 using namespace Windows::Graphics::DirectX::Direct3D11; 23 using namespace Windows::Storage; 24 using namespace Windows::UI::Composition; 25 } 26 27 namespace util 28 { 29 using namespace robmikh::common::uwp; 30 } 31 32 const float CLEAR_COLOR[] = { 0.0f, 0.0f, 0.0f, 1.0f }; 33 34 int32_t EnsureEvenGif(int32_t value) 35 { 36 if (value % 2 == 0) 37 { 38 return value; 39 } 40 else 41 { 42 return value + 1; 43 } 44 } 45 46 //---------------------------------------------------------------------------- 47 // 48 // GifRecordingSession::GifRecordingSession 49 // 50 //---------------------------------------------------------------------------- 51 GifRecordingSession::GifRecordingSession( 52 winrt::IDirect3DDevice const& device, 53 winrt::GraphicsCaptureItem const& item, 54 RECT const cropRect, 55 uint32_t frameRate, 56 winrt::Streams::IRandomAccessStream const& stream) 57 { 58 m_device = device; 59 m_d3dDevice = GetDXGIInterfaceFromObject<ID3D11Device>(m_device); 60 m_d3dDevice->GetImmediateContext(m_d3dContext.put()); 61 m_item = item; 62 m_frameRate = frameRate; 63 m_stream = stream; 64 65 auto itemSize = item.Size(); 66 auto inputWidth = EnsureEvenGif(itemSize.Width); 67 auto inputHeight = EnsureEvenGif(itemSize.Height); 68 m_frameWait = std::make_shared<CaptureFrameWait>(m_device, m_item, winrt::SizeInt32{ inputWidth, inputHeight }); 69 auto weakPointer{ std::weak_ptr{ m_frameWait } }; 70 m_itemClosed = item.Closed(winrt::auto_revoke, [weakPointer](auto&, auto&) 71 { 72 auto sharedPointer{ weakPointer.lock() }; 73 if (sharedPointer) 74 { 75 sharedPointer->StopCapture(); 76 } 77 }); 78 79 // Get crop dimension 80 if ((cropRect.right - cropRect.left) != 0) 81 { 82 m_rcCrop = cropRect; 83 m_frameWait->ShowCaptureBorder(false); 84 } 85 else 86 { 87 m_rcCrop.left = 0; 88 m_rcCrop.top = 0; 89 m_rcCrop.right = inputWidth; 90 m_rcCrop.bottom = inputHeight; 91 } 92 93 // Apply scaling 94 constexpr int c_minimumSize = 34; 95 auto scaledWidth = MulDiv(m_rcCrop.right - m_rcCrop.left, g_RecordScaling, 100); 96 auto scaledHeight = MulDiv(m_rcCrop.bottom - m_rcCrop.top, g_RecordScaling, 100); 97 m_width = scaledWidth; 98 m_height = scaledHeight; 99 if (m_width < c_minimumSize) 100 { 101 m_width = c_minimumSize; 102 m_height = MulDiv(m_height, m_width, scaledWidth); 103 } 104 if (m_height < c_minimumSize) 105 { 106 m_height = c_minimumSize; 107 m_width = MulDiv(m_width, m_height, scaledHeight); 108 } 109 if (m_width > inputWidth) 110 { 111 m_width = inputWidth; 112 m_height = c_minimumSize, MulDiv(m_height, scaledWidth, m_width); 113 } 114 if (m_height > inputHeight) 115 { 116 m_height = inputHeight; 117 m_width = c_minimumSize, MulDiv(m_width, scaledHeight, m_height); 118 } 119 m_width = EnsureEvenGif(m_width); 120 m_height = EnsureEvenGif(m_height); 121 122 m_frameDelay = (frameRate > 0) ? (100 / frameRate) : 15; 123 124 // Initialize WIC 125 winrt::check_hresult(CoCreateInstance( 126 CLSID_WICImagingFactory, 127 nullptr, 128 CLSCTX_INPROC_SERVER, 129 IID_PPV_ARGS(m_wicFactory.put()))); 130 131 // Create WIC stream from IRandomAccessStream 132 winrt::check_hresult(m_wicFactory->CreateStream(m_wicStream.put())); 133 134 // Get the IStream from the IRandomAccessStream 135 winrt::com_ptr<IStream> streamInterop; 136 winrt::check_hresult(CreateStreamOverRandomAccessStream( 137 winrt::get_unknown(stream), 138 IID_PPV_ARGS(streamInterop.put()))); 139 winrt::check_hresult(m_wicStream->InitializeFromIStream(streamInterop.get())); 140 141 // Create GIF encoder 142 winrt::check_hresult(m_wicFactory->CreateEncoder( 143 GUID_ContainerFormatGif, 144 nullptr, 145 m_gifEncoder.put())); 146 147 winrt::check_hresult(m_gifEncoder->Initialize(m_wicStream.get(), WICBitmapEncoderNoCache)); 148 149 // Set global GIF metadata for looping (NETSCAPE2.0 application extension) 150 try 151 { 152 winrt::com_ptr<IWICMetadataQueryWriter> encoderMetadataWriter; 153 if (SUCCEEDED(m_gifEncoder->GetMetadataQueryWriter(encoderMetadataWriter.put())) && encoderMetadataWriter) 154 { 155 OutputDebugStringW(L"Setting NETSCAPE2.0 looping extension on encoder...\n"); 156 157 // Set application extension 158 PROPVARIANT propValue; 159 PropVariantInit(&propValue); 160 propValue.vt = VT_UI1 | VT_VECTOR; 161 propValue.caub.cElems = 11; 162 propValue.caub.pElems = static_cast<UCHAR*>(CoTaskMemAlloc(11)); 163 if (propValue.caub.pElems != nullptr) 164 { 165 memcpy(propValue.caub.pElems, "NETSCAPE2.0", 11); 166 HRESULT hr = encoderMetadataWriter->SetMetadataByName(L"/appext/application", &propValue); 167 if (SUCCEEDED(hr)) 168 { 169 OutputDebugStringW(L"Encoder application extension set successfully\n"); 170 } 171 else 172 { 173 OutputDebugStringW(L"Failed to set encoder application extension\n"); 174 } 175 PropVariantClear(&propValue); 176 177 // Set loop count (0 = infinite) 178 PropVariantInit(&propValue); 179 propValue.vt = VT_UI1 | VT_VECTOR; 180 propValue.caub.cElems = 5; 181 propValue.caub.pElems = static_cast<UCHAR*>(CoTaskMemAlloc(5)); 182 if (propValue.caub.pElems != nullptr) 183 { 184 propValue.caub.pElems[0] = 3; 185 propValue.caub.pElems[1] = 1; 186 propValue.caub.pElems[2] = 0; 187 propValue.caub.pElems[3] = 0; 188 propValue.caub.pElems[4] = 0; 189 hr = encoderMetadataWriter->SetMetadataByName(L"/appext/data", &propValue); 190 if (SUCCEEDED(hr)) 191 { 192 OutputDebugStringW(L"Encoder loop count set successfully\n"); 193 } 194 else 195 { 196 OutputDebugStringW(L"Failed to set encoder loop count\n"); 197 } 198 PropVariantClear(&propValue); 199 } 200 } 201 } 202 else 203 { 204 OutputDebugStringW(L"Failed to get encoder metadata writer\n"); 205 } 206 } 207 catch (...) 208 { 209 OutputDebugStringW(L"Warning: Failed to set GIF encoder looping metadata\n"); 210 } 211 } 212 213 //---------------------------------------------------------------------------- 214 // 215 // GifRecordingSession::~GifRecordingSession 216 // 217 //---------------------------------------------------------------------------- 218 GifRecordingSession::~GifRecordingSession() 219 { 220 Close(); 221 } 222 223 //---------------------------------------------------------------------------- 224 // 225 // GifRecordingSession::Create 226 // 227 //---------------------------------------------------------------------------- 228 std::shared_ptr<GifRecordingSession> GifRecordingSession::Create( 229 winrt::IDirect3DDevice const& device, 230 winrt::GraphicsCaptureItem const& item, 231 RECT const& crop, 232 uint32_t frameRate, 233 winrt::Streams::IRandomAccessStream const& stream) 234 { 235 return std::shared_ptr<GifRecordingSession>(new GifRecordingSession(device, item, crop, frameRate, stream)); 236 } 237 238 //---------------------------------------------------------------------------- 239 // 240 // GifRecordingSession::EncodeFrame 241 // 242 //---------------------------------------------------------------------------- 243 HRESULT GifRecordingSession::EncodeFrame(ID3D11Texture2D* frameTexture) 244 { 245 std::lock_guard<std::mutex> lock(m_encoderMutex); 246 if (m_encoderReleased) 247 { 248 OutputDebugStringW(L"EncodeFrame called after encoder released.\n"); 249 return E_FAIL; 250 } 251 252 try 253 { 254 // Create a staging texture for CPU access 255 D3D11_TEXTURE2D_DESC frameDesc; 256 frameTexture->GetDesc(&frameDesc); 257 258 // GIF encoding with palette generation is VERY slow at high resolutions (4K takes 1 second per frame!) 259 260 UINT targetWidth = frameDesc.Width; 261 UINT targetHeight = frameDesc.Height; 262 263 if (frameDesc.Width > static_cast<uint32_t>(m_width) || frameDesc.Height > static_cast<uint32_t>(m_height)) 264 { 265 float scaleX = static_cast<float>(m_width) / frameDesc.Width; 266 float scaleY = static_cast<float>(m_height) / frameDesc.Height; 267 float scale = min(scaleX, scaleY); 268 269 targetWidth = static_cast<UINT>(frameDesc.Width * scale); 270 targetHeight = static_cast<UINT>(frameDesc.Height * scale); 271 272 // Ensure even dimensions for GIF 273 targetWidth = (targetWidth / 2) * 2; 274 targetHeight = (targetHeight / 2) * 2; 275 } 276 277 D3D11_TEXTURE2D_DESC stagingDesc = frameDesc; 278 stagingDesc.Usage = D3D11_USAGE_STAGING; 279 stagingDesc.BindFlags = 0; 280 stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; 281 stagingDesc.MiscFlags = 0; 282 283 winrt::com_ptr<ID3D11Texture2D> stagingTexture; 284 winrt::check_hresult(m_d3dDevice->CreateTexture2D(&stagingDesc, nullptr, stagingTexture.put())); 285 286 // Copy the frame to staging texture 287 m_d3dContext->CopyResource(stagingTexture.get(), frameTexture); 288 289 // Map the staging texture 290 D3D11_MAPPED_SUBRESOURCE mappedResource; 291 winrt::check_hresult(m_d3dContext->Map(stagingTexture.get(), 0, D3D11_MAP_READ, 0, &mappedResource)); 292 293 // Create a new frame in the GIF 294 winrt::com_ptr<IWICBitmapFrameEncode> frameEncode; 295 winrt::com_ptr<IPropertyBag2> propertyBag; 296 winrt::check_hresult(m_gifEncoder->CreateNewFrame(frameEncode.put(), propertyBag.put())); 297 298 // Initialize the frame encoder with property bag 299 winrt::check_hresult(frameEncode->Initialize(propertyBag.get())); 300 301 // CRITICAL: For GIF, we MUST set size and pixel format BEFORE WriteSource 302 // Use target dimensions (may be downsampled) 303 winrt::check_hresult(frameEncode->SetSize(targetWidth, targetHeight)); 304 305 // Set the pixel format to 8-bit indexed (required for GIF) 306 WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat8bppIndexed; 307 winrt::check_hresult(frameEncode->SetPixelFormat(&pixelFormat)); 308 309 // Create a WIC bitmap from the BGRA texture data 310 winrt::com_ptr<IWICBitmap> sourceBitmap; 311 winrt::check_hresult(m_wicFactory->CreateBitmapFromMemory( 312 frameDesc.Width, 313 frameDesc.Height, 314 GUID_WICPixelFormat32bppBGRA, 315 mappedResource.RowPitch, 316 frameDesc.Height * mappedResource.RowPitch, 317 static_cast<BYTE*>(mappedResource.pData), 318 sourceBitmap.put())); 319 320 // If we need downsampling, use WIC scaler 321 winrt::com_ptr<IWICBitmapSource> finalSource = sourceBitmap; 322 if (targetWidth != frameDesc.Width || targetHeight != frameDesc.Height) 323 { 324 winrt::com_ptr<IWICBitmapScaler> scaler; 325 winrt::check_hresult(m_wicFactory->CreateBitmapScaler(scaler.put())); 326 winrt::check_hresult(scaler->Initialize( 327 sourceBitmap.get(), 328 targetWidth, 329 targetHeight, 330 WICBitmapInterpolationModeHighQualityCubic)); 331 finalSource = scaler; 332 333 OutputDebugStringW((L"Downsampled from " + std::to_wstring(frameDesc.Width) + L"x" + std::to_wstring(frameDesc.Height) + 334 L" to " + std::to_wstring(targetWidth) + L"x" + std::to_wstring(targetHeight) + L"\n").c_str()); 335 } 336 337 // Use WriteSource - WIC will handle the BGRA to 8bpp indexed conversion 338 winrt::check_hresult(frameEncode->WriteSource(finalSource.get(), nullptr)); 339 340 try 341 { 342 winrt::com_ptr<IWICMetadataQueryWriter> frameMetadataWriter; 343 if (SUCCEEDED(frameEncode->GetMetadataQueryWriter(frameMetadataWriter.put())) && frameMetadataWriter) 344 { 345 // Set the frame delay in the metadata (in hundredths of a second) 346 PROPVARIANT propValue; 347 PropVariantInit(&propValue); 348 propValue.vt = VT_UI2; 349 propValue.uiVal = static_cast<USHORT>(m_frameDelay); 350 frameMetadataWriter->SetMetadataByName(L"/grctlext/Delay", &propValue); 351 PropVariantClear(&propValue); 352 353 // Set disposal method (2 = restore to background, needed for animation) 354 PropVariantInit(&propValue); 355 propValue.vt = VT_UI1; 356 propValue.bVal = 2; // Disposal method: restore to background color 357 frameMetadataWriter->SetMetadataByName(L"/grctlext/Disposal", &propValue); 358 PropVariantClear(&propValue); 359 } 360 } 361 catch (...) 362 { 363 // Metadata setting failed, continue anyway 364 OutputDebugStringW(L"Warning: Failed to set GIF frame metadata\n"); 365 } 366 367 // Commit the frame 368 OutputDebugStringW(L"About to commit frame to encoder...\n"); 369 winrt::check_hresult(frameEncode->Commit()); 370 OutputDebugStringW(L"Frame committed successfully\n"); 371 372 // Unmap the staging texture 373 m_d3dContext->Unmap(stagingTexture.get(), 0); 374 375 // Increment and log frame count 376 m_frameCount++; 377 m_hasAnyFrame.store(true); 378 OutputDebugStringW((L"GIF Frame #" + std::to_wstring(m_frameCount) + L" fully encoded and committed\n").c_str()); 379 380 return S_OK; 381 } 382 catch (const winrt::hresult_error& error) 383 { 384 OutputDebugStringW(error.message().c_str()); 385 return error.code(); 386 } 387 } 388 389 //---------------------------------------------------------------------------- 390 // 391 // GifRecordingSession::StartAsync 392 // 393 //---------------------------------------------------------------------------- 394 winrt::IAsyncAction GifRecordingSession::StartAsync() 395 { 396 auto expected = false; 397 if (m_isRecording.compare_exchange_strong(expected, true)) 398 { 399 auto self = shared_from_this(); 400 401 try 402 { 403 // Start capturing frames 404 auto frameStartTime = std::chrono::high_resolution_clock::now(); 405 int captureAttempts = 0; 406 int successfulCaptures = 0; 407 int duplicatedFrames = 0; 408 409 // Keep track of the last frame to duplicate when needed 410 winrt::com_ptr<ID3D11Texture2D> lastCroppedTexture; 411 412 while (m_isRecording && !m_closed) 413 { 414 captureAttempts++; 415 auto frame = m_frameWait->TryGetNextFrame(); 416 if (!frame && !m_isRecording) 417 { 418 // Recording was stopped while waiting for frame 419 OutputDebugStringW(L"[GIF] Recording stopped during frame wait\n"); 420 break; 421 } 422 423 winrt::com_ptr<ID3D11Texture2D> croppedTexture; 424 425 if (frame) 426 { 427 successfulCaptures++; 428 auto contentSize = frame->ContentSize; 429 auto frameTexture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame->FrameTexture); 430 D3D11_TEXTURE2D_DESC desc = {}; 431 frameTexture->GetDesc(&desc); 432 433 // Use the smaller of the crop size or content size 434 auto width = min(m_rcCrop.right - m_rcCrop.left, contentSize.Width); 435 auto height = min(m_rcCrop.bottom - m_rcCrop.top, contentSize.Height); 436 437 D3D11_TEXTURE2D_DESC croppedDesc = {}; 438 croppedDesc.Width = width; 439 croppedDesc.Height = height; 440 croppedDesc.MipLevels = 1; 441 croppedDesc.ArraySize = 1; 442 croppedDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; 443 croppedDesc.SampleDesc.Count = 1; 444 croppedDesc.Usage = D3D11_USAGE_DEFAULT; 445 croppedDesc.BindFlags = D3D11_BIND_RENDER_TARGET; 446 447 winrt::check_hresult(m_d3dDevice->CreateTexture2D(&croppedDesc, nullptr, croppedTexture.put())); 448 449 // Set the content region to copy and clamp the coordinates 450 D3D11_BOX region = {}; 451 region.left = std::clamp(m_rcCrop.left, static_cast<LONG>(0), static_cast<LONG>(desc.Width)); 452 region.right = std::clamp(m_rcCrop.left + width, static_cast<LONG>(0), static_cast<LONG>(desc.Width)); 453 region.top = std::clamp(m_rcCrop.top, static_cast<LONG>(0), static_cast<LONG>(desc.Height)); 454 region.bottom = std::clamp(m_rcCrop.top + height, static_cast<LONG>(0), static_cast<LONG>(desc.Height)); 455 region.back = 1; 456 457 // Copy the cropped region 458 m_d3dContext->CopySubresourceRegion( 459 croppedTexture.get(), 460 0, 461 0, 0, 0, 462 frameTexture.get(), 463 0, 464 ®ion); 465 466 // Save this as the last frame for duplication 467 lastCroppedTexture = croppedTexture; 468 } 469 else if (lastCroppedTexture) 470 { 471 // No new frame, duplicate the last one 472 duplicatedFrames++; 473 croppedTexture = lastCroppedTexture; 474 } 475 476 // Encode the frame (either new or duplicated) 477 if (croppedTexture) 478 { 479 HRESULT hr = EncodeFrame(croppedTexture.get()); 480 if (FAILED(hr)) 481 { 482 CloseInternal(); 483 break; 484 } 485 } 486 487 // Wait for the next frame interval 488 co_await winrt::resume_after(std::chrono::milliseconds(1000 / m_frameRate)); 489 490 // Check again after resuming from sleep 491 if (!m_isRecording || m_closed) 492 { 493 OutputDebugStringW(L"[GIF] Loop exiting after resume_after\n"); 494 break; 495 } 496 } 497 498 OutputDebugStringW(L"[GIF] Capture loop exited\n"); 499 500 // Commit the GIF encoder 501 if (m_gifEncoder) 502 { 503 auto frameEndTime = std::chrono::high_resolution_clock::now(); 504 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(frameEndTime - frameStartTime).count(); 505 506 OutputDebugStringW(L"Recording stopped. Committing GIF encoder...\n"); 507 OutputDebugStringW((L"Total frames captured: " + std::to_wstring(m_frameCount) + L"\n").c_str()); 508 OutputDebugStringW((L"Capture attempts: " + std::to_wstring(captureAttempts) + L"\n").c_str()); 509 OutputDebugStringW((L"Successful captures: " + std::to_wstring(successfulCaptures) + L"\n").c_str()); 510 OutputDebugStringW((L"Duplicated frames: " + std::to_wstring(duplicatedFrames) + L"\n").c_str()); 511 OutputDebugStringW((L"Recording duration: " + std::to_wstring(duration) + L"ms\n").c_str()); 512 OutputDebugStringW((L"Actual FPS: " + std::to_wstring(m_frameCount * 1000.0 / duration) + L"\n").c_str()); 513 514 winrt::check_hresult(m_gifEncoder->Commit()); 515 OutputDebugStringW(L"GIF encoder committed successfully\n"); 516 } 517 } 518 catch (const winrt::hresult_error& error) 519 { 520 OutputDebugStringW(L"Error in GIF recording: "); 521 OutputDebugStringW(error.message().c_str()); 522 OutputDebugStringW(L"\n"); 523 524 // Try to commit the encoder even on error 525 if (m_gifEncoder) 526 { 527 try 528 { 529 m_gifEncoder->Commit(); 530 } 531 catch (...) {} 532 } 533 534 CloseInternal(); 535 } 536 } 537 538 // Ensure encoder resources are released in case caller forgets to Close explicitly. 539 ReleaseEncoderResources(); 540 OutputDebugStringW(L"[GIF] StartAsync completing, about to co_return\n"); 541 co_return; 542 } 543 544 //---------------------------------------------------------------------------- 545 // 546 // GifRecordingSession::Close 547 // 548 //---------------------------------------------------------------------------- 549 void GifRecordingSession::Close() 550 { 551 OutputDebugStringW(L"[GIF] Close() called\n"); 552 auto expected = false; 553 if (m_closed.compare_exchange_strong(expected, true)) 554 { 555 OutputDebugStringW(L"[GIF] Setting m_closed = true\n"); 556 // Signal the capture loop to stop 557 m_isRecording = false; 558 OutputDebugStringW(L"[GIF] Setting m_isRecording = false\n"); 559 560 // Stop the frame wait to unblock any pending frame acquisition 561 m_frameWait->StopCapture(); 562 OutputDebugStringW(L"[GIF] StopCapture called\n"); 563 } 564 } 565 566 //---------------------------------------------------------------------------- 567 // 568 // GifRecordingSession::CloseInternal 569 // 570 //---------------------------------------------------------------------------- 571 void GifRecordingSession::CloseInternal() 572 { 573 ReleaseEncoderResources(); 574 575 m_frameWait->StopCapture(); 576 m_itemClosed.revoke(); 577 } 578 579 //---------------------------------------------------------------------------- 580 // 581 // GifRecordingSession::ReleaseEncoderResources 582 // Ensures encoder/stream COM objects release the temp file handle so trim can reopen it. 583 // 584 //---------------------------------------------------------------------------- 585 void GifRecordingSession::ReleaseEncoderResources() 586 { 587 std::lock_guard<std::mutex> lock(m_encoderMutex); 588 if (m_encoderReleased) 589 { 590 return; 591 } 592 593 // Commit only if we still own the encoder and it has not been committed; swallow failures. 594 if (m_gifEncoder) 595 { 596 try 597 { 598 m_gifEncoder->Commit(); 599 } 600 catch (...) 601 { 602 } 603 } 604 605 m_encoderMetadataWriter = nullptr; 606 m_gifEncoder = nullptr; 607 m_wicStream = nullptr; 608 m_wicFactory = nullptr; 609 m_stream = nullptr; 610 m_encoderReleased = true; 611 }