/ src / modules / ZoomIt / ZoomIt / GifRecordingSession.cpp
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                          &region);
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  }