1using System; 2using System.Collections.Generic; 3using System.Drawing; 4using System.Drawing.Imaging; 5using System.Runtime.InteropServices; 6using WindowCapture.Helpers; 7using WindowCapture.Native; 8using WindowCapture.App; 9 10namespace WindowCapture.Effects 11{ 12 /// <summary> 13 /// Hardware-accelerated rendering engine using Direct2D. 14 /// Uses off-screen composition to eliminate flickering: 15 /// all drawing happens on an invisible bitmap target, then the 16 /// completed frame is blitted to the HWND target in one atomic present. 17 /// </summary> 18 public static class D2DRenderer 19 { 20 // HWND render target (presents to screen) 21 private static ID2D1Factory factory; 22 private static ID2D1HwndRenderTarget renderTarget; 23 private static ID2D1GdiInteropRenderTarget gdiInterop; 24 25 // Off-screen composition target (invisible, flicker-free) 26 private static ID2D1BitmapRenderTarget offscreenRT; 27 private static ID2D1GdiInteropRenderTarget offscreenGdiInterop; 28 private static int offscreenW, offscreenH; 29 30 // Shared resources 31 private static ID2D1Bitmap cachedBitmap; 32 private static ID2D1SolidColorBrush dimBrush; 33 34 // State 35 private static bool initialized; 36 private static bool available; 37 private static int targetWidth, targetHeight; 38 private static int bitmapWidth, bitmapHeight; 39 private static bool inDraw; 40 private static bool useOffscreen; // true if offscreen composition is active 41 42 public static int TargetWidth { get { return targetWidth; } } 43 public static int TargetHeight { get { return targetHeight; } } 44 45 /// <summary> 46 /// Whether D2D is initialized and ready for rendering 47 /// </summary> 48 public static bool IsAvailable { get { return available && renderTarget != null; } } 49 50 /// <summary> 51 /// Initialize Direct2D for a given HWND control 52 /// </summary> 53 public static bool Initialize(IntPtr hwnd, int width, int height) 54 { 55 if (initialized && available) return true; 56 // Previously initialized but the device was lost (available==false) — tear down cleanly and 57 // rebuild from scratch so GPU rendering can recover instead of being stuck on GDI forever. 58 if (initialized && !available) Dispose(); 59 initialized = true; 60 61 try 62 { 63 // Create D2D factory 64 int hr = D2DApi.D2D1CreateFactory( 65 D2D1_FACTORY_TYPE.SINGLE_THREADED, 66 D2DApi.IID_ID2D1Factory, 67 IntPtr.Zero, 68 out factory); 69 70 if (hr != 0 || factory == null) 71 { 72 Log("D2D1CreateFactory failed: 0x" + hr.ToString("X8")); 73 return false; 74 } 75 76 Log("D2D factory created"); 77 78 // Create HWND render target with GDI compatibility 79 if (!CreateRenderTarget(hwnd, width, height)) 80 return false; 81 82 available = true; 83 Log("D2D initialized successfully (" + width + "x" + height + ")"); 84 return true; 85 } 86 catch (Exception ex) 87 { 88 Log("D2D init exception: " + ex.Message); 89 available = false; 90 return false; 91 } 92 } 93 94 private static bool CreateRenderTarget(IntPtr hwnd, int width, int height) 95 { 96 if (width < 1) width = 1; 97 if (height < 1) height = 1; 98 targetWidth = width; 99 targetHeight = height; 100 101 var pixelFormat = new D2D1_PIXEL_FORMAT 102 { 103 format = DXGI_FORMAT.B8G8R8A8_UNORM, 104 alphaMode = D2D1_ALPHA_MODE.PREMULTIPLIED 105 }; 106 107 var rtProps = new D2D1_RENDER_TARGET_PROPERTIES 108 { 109 type = D2D1_RENDER_TARGET_TYPE.DEFAULT, 110 pixelFormat = pixelFormat, 111 dpiX = 96f, 112 dpiY = 96f, 113 usage = D2D1_RENDER_TARGET_USAGE.GDI_COMPATIBLE, 114 minLevel = D2D1_FEATURE_LEVEL.DEFAULT 115 }; 116 117 var hwndProps = new D2D1_HWND_RENDER_TARGET_PROPERTIES 118 { 119 hwnd = hwnd, 120 pixelSize = new D2D1_SIZE_U((uint)width, (uint)height), 121 presentOptions = D2D1_PRESENT_OPTIONS.RETAIN_CONTENTS 122 }; 123 124 int hr = factory.CreateHwndRenderTarget(ref rtProps, ref hwndProps, out renderTarget); 125 if (hr != 0 || renderTarget == null) 126 { 127 Log("CreateHwndRenderTarget failed: 0x" + hr.ToString("X8")); 128 available = false; 129 return false; 130 } 131 132 // Get GDI interop for fallback (direct HWND rendering if offscreen fails) 133 try 134 { 135 gdiInterop = (ID2D1GdiInteropRenderTarget)renderTarget; 136 } 137 catch 138 { 139 Log("GDI interop not available, D2D-only mode"); 140 gdiInterop = null; 141 } 142 143 return true; 144 } 145 146 /// <summary> 147 /// Ensure the off-screen composition target exists and matches viewport size. 148 /// Created with GDI_COMPATIBLE for annotation overlay via GetDC/ReleaseDC. 149 /// </summary> 150 private static bool EnsureOffscreenTarget() 151 { 152 if (offscreenRT != null && offscreenW == targetWidth && offscreenH == targetHeight) 153 return true; 154 155 ReleaseOffscreen(); 156 157 var size = new D2D1_SIZE_F(targetWidth, targetHeight); 158 IntPtr pSize = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(D2D1_SIZE_F))); 159 try 160 { 161 Marshal.StructureToPtr(size, pSize, false); 162 int hr = renderTarget.CreateCompatibleRenderTarget( 163 pSize, IntPtr.Zero, IntPtr.Zero, 164 D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS.GDI_COMPATIBLE, 165 out offscreenRT); 166 167 if (hr != 0 || offscreenRT == null) 168 { 169 Log("CreateCompatibleRenderTarget (offscreen) failed: 0x" + hr.ToString("X8")); 170 offscreenRT = null; 171 return false; 172 } 173 174 // Get GDI interop for off-screen target (for annotation drawing) 175 try 176 { 177 offscreenGdiInterop = (ID2D1GdiInteropRenderTarget)offscreenRT; 178 } 179 catch 180 { 181 Log("Offscreen GDI interop not available"); 182 offscreenGdiInterop = null; 183 } 184 185 offscreenW = targetWidth; 186 offscreenH = targetHeight; 187 Log("Offscreen target created (" + targetWidth + "x" + targetHeight + ")"); 188 return true; 189 } 190 finally 191 { 192 Marshal.FreeHGlobal(pSize); 193 } 194 } 195 196 private static void ReleaseOffscreen() 197 { 198 // Release dim brush (bound to offscreen or renderTarget - recreate on next use) 199 if (dimBrush != null) 200 { 201 try { Marshal.ReleaseComObject(dimBrush); } catch { } 202 dimBrush = null; 203 } 204 205 // offscreenGdiInterop is same COM object as offscreenRT, don't release separately 206 offscreenGdiInterop = null; 207 208 if (offscreenRT != null) 209 { 210 try { Marshal.ReleaseComObject(offscreenRT); } catch { } 211 offscreenRT = null; 212 } 213 offscreenW = 0; 214 offscreenH = 0; 215 } 216 217 /// <summary> 218 /// Resize render target when control size changes 219 /// </summary> 220 public static void Resize(int width, int height) 221 { 222 if (!available || renderTarget == null) return; 223 if (width < 1 || height < 1) return; 224 if (width == targetWidth && height == targetHeight) return; 225 226 targetWidth = width; 227 targetHeight = height; 228 229 // Release offscreen (will be recreated at new size in next BeginDraw) 230 ReleaseOffscreen(); 231 232 var size = new D2D1_SIZE_U((uint)width, (uint)height); 233 int hr = renderTarget.Resize(ref size); 234 if (hr != 0) 235 { 236 Log("Resize failed: 0x" + hr.ToString("X8")); 237 } 238 } 239 240 /// <summary> 241 /// Upload a GDI+ Bitmap to GPU as D2D bitmap. Optimized for speed. 242 /// </summary> 243 public static void UploadBitmap(Bitmap source) 244 { 245 if (!available || renderTarget == null || source == null) return; 246 247 using (PerformanceLogger.Measure("D2D.UploadBitmap")) 248 { 249 // If same size, we could potentially use CopyFromMemory instead of Recreating 250 bool canReuse = cachedBitmap != null && 251 bitmapWidth == source.Width && 252 bitmapHeight == source.Height; 253 254 try 255 { 256 bitmapWidth = source.Width; 257 bitmapHeight = source.Height; 258 259 var rect = new Rectangle(0, 0, source.Width, source.Height); 260 BitmapData data = source.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppPArgb); 261 262 try 263 { 264 if (canReuse) 265 { 266 var size = new D2D1_SIZE_U((uint)source.Width, (uint)source.Height); 267 int hr = cachedBitmap.CopyFromMemory(IntPtr.Zero, data.Scan0, (uint)data.Stride); 268 if (hr == 0) return; 269 } 270 271 // Recreate if reuse failed or not possible 272 ReleaseBitmap(); 273 var bitmapProps = new D2D1_BITMAP_PROPERTIES 274 { 275 pixelFormat = new D2D1_PIXEL_FORMAT 276 { 277 format = DXGI_FORMAT.B8G8R8A8_UNORM, 278 alphaMode = D2D1_ALPHA_MODE.PREMULTIPLIED 279 }, 280 dpiX = 96f, 281 dpiY = 96f 282 }; 283 284 var d2dSize = new D2D1_SIZE_U((uint)source.Width, (uint)source.Height); 285 int hrCreate = renderTarget.CreateBitmap( 286 d2dSize, 287 data.Scan0, 288 (uint)data.Stride, 289 ref bitmapProps, 290 out cachedBitmap); 291 292 if (hrCreate != 0) 293 { 294 Log("CreateBitmap failed: 0x" + hrCreate.ToString("X8")); 295 cachedBitmap = null; 296 } 297 } 298 finally 299 { 300 source.UnlockBits(data); 301 } 302 } 303 catch (Exception ex) 304 { 305 Log("UploadBitmap exception: " + ex.Message); 306 cachedBitmap = null; 307 } 308 } 309 } 310 311 /// <summary> 312 /// Begin a D2D rendering frame. Uses off-screen composition for flicker-free rendering. 313 /// </summary> 314 public static bool BeginDraw() 315 { 316 if (!available || renderTarget == null) return false; 317 try 318 { 319 renderTarget.BeginDraw(); 320 321 // Try to use off-screen composition 322 useOffscreen = EnsureOffscreenTarget(); 323 if (useOffscreen) 324 { 325 offscreenRT.BeginDraw(); 326 } 327 328 inDraw = true; 329 return true; 330 } 331 catch (Exception ex) 332 { 333 Log("BeginDraw exception: " + ex.Message); 334 inDraw = false; 335 return false; 336 } 337 } 338 339 /// <summary> 340 /// End D2D rendering frame. Blits off-screen buffer to HWND target atomically. 341 /// Returns false if device lost. 342 /// </summary> 343 public static bool EndDraw() 344 { 345 if (!inDraw || renderTarget == null) return false; 346 inDraw = false; 347 348 // Off-screen → HWND blit. This is wrapped in its OWN try/catch so that an exception 349 // here can never skip the HWND renderTarget.EndDraw() below — otherwise the HWND target 350 // stays stuck in draw state and every subsequent frame fails (D2DERR_WRONG_STATE), 351 // permanently wedging GPU rendering for the rest of the session. 352 try 353 { 354 if (useOffscreen && offscreenRT != null) 355 { 356 ulong ot1, ot2; 357 offscreenRT.EndDraw(out ot1, out ot2); 358 359 ID2D1Bitmap frameBitmap; 360 offscreenRT.GetBitmap(out frameBitmap); 361 362 if (frameBitmap != null) 363 { 364 try 365 { 366 // Clear HWND target before blit to prevent ghosting 367 var clearColor = new D2D1_COLOR_F(0, 0, 0, 0); 368 renderTarget.Clear(ref clearColor); 369 370 // Blit completed frame to HWND target in one operation 371 var destRect = new D2D1_RECT_F(0, 0, targetWidth, targetHeight); 372 IntPtr pDest = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(D2D1_RECT_F))); 373 try 374 { 375 Marshal.StructureToPtr(destRect, pDest, false); 376 renderTarget.DrawBitmap(frameBitmap, pDest, 1f, 377 D2D1_BITMAP_INTERPOLATION_MODE.LINEAR, IntPtr.Zero); 378 } 379 finally { Marshal.FreeHGlobal(pDest); } 380 } 381 finally { Marshal.ReleaseComObject(frameBitmap); } 382 } 383 } 384 } 385 catch (Exception ex) 386 { 387 Log("EndDraw offscreen blit exception: " + ex.Message); 388 } 389 390 // ALWAYS present the HWND target so it leaves draw state. 391 try 392 { 393 ulong tag1, tag2; 394 int hr = renderTarget.EndDraw(out tag1, out tag2); 395 396 // D2DERR_RECREATE_TARGET 397 if (hr == unchecked((int)0x8899000C)) 398 { 399 Log("Device lost - need to recreate"); 400 HandleDeviceLost(); 401 return false; 402 } 403 404 return hr == 0; 405 } 406 catch (Exception ex) 407 { 408 Log("EndDraw HWND exception: " + ex.Message); 409 return false; 410 } 411 } 412 413 /// <summary> 414 /// Clear the render target 415 /// </summary> 416 public static void Clear(float r, float g, float b, float a) 417 { 418 if (!inDraw) return; 419 var color = new D2D1_COLOR_F(r, g, b, a); 420 if (useOffscreen && offscreenRT != null) 421 offscreenRT.Clear(ref color); 422 else 423 renderTarget.Clear(ref color); 424 } 425 426 /// <summary> 427 /// Draw the cached bitmap scaled to destination rectangle 428 /// </summary> 429 public static void DrawImage(RectangleF dest, float opacity) 430 { 431 if (!inDraw || cachedBitmap == null) return; 432 433 var destRect = new D2D1_RECT_F(dest.Left, dest.Top, dest.Right, dest.Bottom); 434 IntPtr pDest = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(D2D1_RECT_F))); 435 try 436 { 437 Marshal.StructureToPtr(destRect, pDest, false); 438 if (useOffscreen && offscreenRT != null) 439 offscreenRT.DrawBitmap(cachedBitmap, pDest, opacity, 440 D2D1_BITMAP_INTERPOLATION_MODE.LINEAR, IntPtr.Zero); 441 else 442 renderTarget.DrawBitmap(cachedBitmap, pDest, opacity, 443 D2D1_BITMAP_INTERPOLATION_MODE.LINEAR, IntPtr.Zero); 444 } 445 finally 446 { 447 Marshal.FreeHGlobal(pDest); 448 } 449 } 450 451 /// <summary> 452 /// Draw image with GPU-accelerated blur using downscale/upscale technique. 453 /// Strength 1-10: higher = more blur passes / smaller intermediate size. 454 /// </summary> 455 public static void DrawImageBlurred(RectangleF dest, int strength) 456 { 457 if (!inDraw || cachedBitmap == null) return; 458 459 // Clamp strength 460 if (strength < 1) strength = 1; 461 if (strength > 10) strength = 10; 462 463 // Calculate downscale factor based on strength 464 float scaleFactor = 1f / (1 + strength); 465 466 int smallW = Math.Max(1, (int)(bitmapWidth * scaleFactor)); 467 int smallH = Math.Max(1, (int)(bitmapHeight * scaleFactor)); 468 469 // Create small intermediate render target 470 ID2D1BitmapRenderTarget smallRT = null; 471 ID2D1BitmapRenderTarget medRT = null; 472 bool smallDrawing = false, medDrawing = false; 473 474 // Use the active draw target for creating compatible targets 475 var parentRT = (useOffscreen && offscreenRT != null) ? (object)offscreenRT : (object)renderTarget; 476 477 try 478 { 479 // Allocate small render target 480 var smallSize = new D2D1_SIZE_F(smallW, smallH); 481 IntPtr pSmallSize = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(D2D1_SIZE_F))); 482 Marshal.StructureToPtr(smallSize, pSmallSize, false); 483 484 int hr; 485 if (useOffscreen && offscreenRT != null) 486 hr = offscreenRT.CreateCompatibleRenderTarget( 487 pSmallSize, IntPtr.Zero, IntPtr.Zero, 488 D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS.NONE, 489 out smallRT); 490 else 491 hr = renderTarget.CreateCompatibleRenderTarget( 492 pSmallSize, IntPtr.Zero, IntPtr.Zero, 493 D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS.NONE, 494 out smallRT); 495 Marshal.FreeHGlobal(pSmallSize); 496 497 if (hr != 0 || smallRT == null) 498 { 499 // Fallback: draw without blur 500 DrawImage(dest, 1f); 501 return; 502 } 503 504 // Pass 1: Draw full image to small target (GPU bilinear downscale = blur) 505 smallDrawing = true; 506 smallRT.BeginDraw(); 507 var smallRect = new D2D1_RECT_F(0, 0, smallW, smallH); 508 IntPtr pSmallRect = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(D2D1_RECT_F))); 509 Marshal.StructureToPtr(smallRect, pSmallRect, false); 510 smallRT.DrawBitmap(cachedBitmap, pSmallRect, 1f, 511 D2D1_BITMAP_INTERPOLATION_MODE.LINEAR, IntPtr.Zero); 512 Marshal.FreeHGlobal(pSmallRect); 513 ulong t1, t2; 514 smallRT.EndDraw(out t1, out t2); 515 smallDrawing = false; 516 517 // Get the small bitmap 518 ID2D1Bitmap smallBmp; 519 smallRT.GetBitmap(out smallBmp); 520 521 if (smallBmp == null) 522 { 523 DrawImage(dest, 1f); 524 return; 525 } 526 527 // For extra blur, do a second pass through an even smaller target 528 if (strength > 3) 529 { 530 int tinyW = Math.Max(1, smallW / 2); 531 int tinyH = Math.Max(1, smallH / 2); 532 533 var medSize = new D2D1_SIZE_F(tinyW, tinyH); 534 IntPtr pMedSize = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(D2D1_SIZE_F))); 535 Marshal.StructureToPtr(medSize, pMedSize, false); 536 if (useOffscreen && offscreenRT != null) 537 hr = offscreenRT.CreateCompatibleRenderTarget( 538 pMedSize, IntPtr.Zero, IntPtr.Zero, 539 D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS.NONE, 540 out medRT); 541 else 542 hr = renderTarget.CreateCompatibleRenderTarget( 543 pMedSize, IntPtr.Zero, IntPtr.Zero, 544 D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS.NONE, 545 out medRT); 546 Marshal.FreeHGlobal(pMedSize); 547 548 if (hr == 0 && medRT != null) 549 { 550 medDrawing = true; 551 medRT.BeginDraw(); 552 var tinyRect = new D2D1_RECT_F(0, 0, tinyW, tinyH); 553 IntPtr pTinyRect = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(D2D1_RECT_F))); 554 Marshal.StructureToPtr(tinyRect, pTinyRect, false); 555 medRT.DrawBitmap(smallBmp, pTinyRect, 1f, 556 D2D1_BITMAP_INTERPOLATION_MODE.LINEAR, IntPtr.Zero); 557 Marshal.FreeHGlobal(pTinyRect); 558 medRT.EndDraw(out t1, out t2); 559 medDrawing = false; 560 561 // Get tiny bitmap and use it instead 562 Marshal.ReleaseComObject(smallBmp); 563 medRT.GetBitmap(out smallBmp); 564 } 565 } 566 567 // Pass 2: Draw small bitmap back to active target at full destination size 568 if (smallBmp != null) 569 { 570 var destR = new D2D1_RECT_F(dest.Left, dest.Top, dest.Right, dest.Bottom); 571 IntPtr pDest = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(D2D1_RECT_F))); 572 Marshal.StructureToPtr(destR, pDest, false); 573 if (useOffscreen && offscreenRT != null) 574 offscreenRT.DrawBitmap(smallBmp, pDest, 1f, 575 D2D1_BITMAP_INTERPOLATION_MODE.LINEAR, IntPtr.Zero); 576 else 577 renderTarget.DrawBitmap(smallBmp, pDest, 1f, 578 D2D1_BITMAP_INTERPOLATION_MODE.LINEAR, IntPtr.Zero); 579 Marshal.FreeHGlobal(pDest); 580 Marshal.ReleaseComObject(smallBmp); 581 } 582 } 583 catch (Exception ex) 584 { 585 Log("DrawImageBlurred exception: " + ex.Message); 586 // Fallback 587 DrawImage(dest, 1f); 588 } 589 finally 590 { 591 // Never release an intermediate target that is still mid-BeginDraw (D2D state violation). 592 if (medRT != null && medDrawing) { try { ulong a, b; medRT.EndDraw(out a, out b); } catch { } } 593 if (smallRT != null && smallDrawing) { try { ulong a, b; smallRT.EndDraw(out a, out b); } catch { } } 594 if (medRT != null) Marshal.ReleaseComObject(medRT); 595 if (smallRT != null) Marshal.ReleaseComObject(smallRT); 596 } 597 } 598 599 /// <summary> 600 /// Draw a semi-transparent dim overlay 601 /// </summary> 602 public static void DrawDimOverlay(RectangleF area, float alpha) 603 { 604 if (!inDraw) return; 605 606 EnsureDimBrush(); 607 if (dimBrush == null) return; 608 609 var c = new D2D1_COLOR_F(0, 0, 0, alpha); 610 dimBrush.SetColor(ref c); 611 612 var rect = new D2D1_RECT_F(area.Left, area.Top, area.Right, area.Bottom); 613 if (useOffscreen && offscreenRT != null) 614 offscreenRT.FillRectangle(ref rect, (ID2D1Brush)dimBrush); 615 else 616 renderTarget.FillRectangle(ref rect, (ID2D1Brush)dimBrush); 617 } 618 619 /// <summary> 620 /// Push an axis-aligned clip rectangle 621 /// </summary> 622 public static void PushClip(RectangleF clipRect) 623 { 624 if (!inDraw) return; 625 var rect = new D2D1_RECT_F(clipRect.Left, clipRect.Top, clipRect.Right, clipRect.Bottom); 626 if (useOffscreen && offscreenRT != null) 627 offscreenRT.PushAxisAlignedClip(ref rect, D2D1_ANTIALIAS_MODE.PER_PRIMITIVE); 628 else 629 renderTarget.PushAxisAlignedClip(ref rect, D2D1_ANTIALIAS_MODE.PER_PRIMITIVE); 630 } 631 632 /// <summary> 633 /// Pop the last clip rectangle 634 /// </summary> 635 public static void PopClip() 636 { 637 if (!inDraw) return; 638 if (useOffscreen && offscreenRT != null) 639 offscreenRT.PopAxisAlignedClip(); 640 else 641 renderTarget.PopAxisAlignedClip(); 642 } 643 644 /// <summary> 645 /// Set transform matrix (for zoom) 646 /// </summary> 647 public static void SetTransform(float scaleX, float scaleY, float translateX, float translateY) 648 { 649 if (!inDraw) return; 650 var m = new D2D1_MATRIX_3X2_F 651 { 652 _11 = scaleX, _12 = 0, 653 _21 = 0, _22 = scaleY, 654 _31 = translateX, _32 = translateY 655 }; 656 if (useOffscreen && offscreenRT != null) 657 offscreenRT.SetTransform(ref m); 658 else 659 renderTarget.SetTransform(ref m); 660 } 661 662 /// <summary> 663 /// Reset transform to identity 664 /// </summary> 665 public static void ResetTransform() 666 { 667 if (!inDraw) return; 668 var identity = D2D1_MATRIX_3X2_F.Identity; 669 if (useOffscreen && offscreenRT != null) 670 offscreenRT.SetTransform(ref identity); 671 else 672 renderTarget.SetTransform(ref identity); 673 } 674 675 /// <summary> 676 /// Get a GDI+ Graphics object for drawing annotations on top of D2D content. 677 /// Uses off-screen target for flicker-free composition. 678 /// Must call ReleaseGdiGraphics() when done! 679 /// </summary> 680 public static Graphics GetGdiGraphics() 681 { 682 if (!inDraw) return null; 683 684 // Prefer off-screen GDI interop (flicker-free) 685 var interop = (useOffscreen && offscreenGdiInterop != null) ? offscreenGdiInterop : gdiInterop; 686 if (interop == null) return null; 687 688 try 689 { 690 IntPtr hdc; 691 int hr = interop.GetDC(D2D1_DC_INITIALIZE_MODE.COPY, out hdc); 692 if (hr != 0 || hdc == IntPtr.Zero) 693 { 694 Log("GetDC failed: 0x" + hr.ToString("X8")); 695 return null; 696 } 697 698 return Graphics.FromHdc(hdc); 699 } 700 catch (Exception ex) 701 { 702 Log("GetGdiGraphics exception: " + ex.Message); 703 return null; 704 } 705 } 706 707 /// <summary> 708 /// Release the GDI+ Graphics obtained from GetGdiGraphics 709 /// </summary> 710 public static void ReleaseGdiGraphics(Graphics g) 711 { 712 if (g == null) return; 713 714 var interop = (useOffscreen && offscreenGdiInterop != null) ? offscreenGdiInterop : gdiInterop; 715 716 try 717 { 718 // Dispose flushes pending GDI+ commands to the HDC 719 g.Dispose(); 720 721 // Release the HDC back to D2D 722 if (interop != null) 723 interop.ReleaseDC(IntPtr.Zero); 724 } 725 catch (Exception ex) 726 { 727 Log("ReleaseGdiGraphics exception: " + ex.Message); 728 try { g.Dispose(); } catch { } 729 } 730 } 731 732 /// <summary> 733 /// Dispose all D2D resources 734 /// </summary> 735 public static void Dispose() 736 { 737 inDraw = false; 738 available = false; 739 740 ReleaseBitmap(); 741 ReleaseOffscreen(); 742 ReleaseBrushCache(); 743 ReleaseTextFormatCache(); 744 745 if (dwFactory != null) 746 { 747 try { Marshal.ReleaseComObject(dwFactory); } catch { } 748 dwFactory = null; 749 } 750 751 // gdiInterop is same COM object as renderTarget, don't release separately 752 gdiInterop = null; 753 754 if (renderTarget != null) 755 { 756 try { Marshal.ReleaseComObject(renderTarget); } catch { } 757 renderTarget = null; 758 } 759 760 if (factory != null) 761 { 762 try { Marshal.ReleaseComObject(factory); } catch { } 763 factory = null; 764 } 765 766 initialized = false; 767 Log("D2D disposed"); 768 } 769 770 /// <summary> 771 /// Reinitialize after device loss 772 /// </summary> 773 public static void Reinitialize(IntPtr hwnd, int width, int height) 774 { 775 Dispose(); 776 Initialize(hwnd, width, height); 777 } 778 779 #region Private Helpers 780 781 private static void ReleaseBitmap() 782 { 783 if (cachedBitmap != null) 784 { 785 try { Marshal.ReleaseComObject(cachedBitmap); } catch { } 786 cachedBitmap = null; 787 } 788 } 789 790 // Solid-color brushes are bound to renderTarget, so they must be released whenever the 791 // target is released (device loss / dispose) — otherwise they leak and, after a device 792 // loss, become dangling references to a destroyed target. 793 private static void ReleaseBrushCache() 794 { 795 foreach (var br in brushCache.Values) 796 { 797 if (br != null) { try { Marshal.ReleaseComObject(br); } catch { } } 798 } 799 brushCache.Clear(); 800 } 801 802 // DirectWrite text formats are device-independent; only released on full Dispose. 803 private static void ReleaseTextFormatCache() 804 { 805 foreach (var fmt in textFormatCache.Values) 806 { 807 if (fmt != null) { try { Marshal.ReleaseComObject(fmt); } catch { } } 808 } 809 textFormatCache.Clear(); 810 } 811 812 private static void EnsureDimBrush() 813 { 814 if (dimBrush != null) return; 815 if (renderTarget == null) return; 816 817 var color = new D2D1_COLOR_F(0, 0, 0, 0.5f); 818 int hr = renderTarget.CreateSolidColorBrush(ref color, IntPtr.Zero, out dimBrush); 819 if (hr != 0) 820 { 821 Log("CreateSolidColorBrush failed: 0x" + hr.ToString("X8")); 822 dimBrush = null; 823 } 824 } 825 826 private static void HandleDeviceLost() 827 { 828 Log("Handling device loss..."); 829 ReleaseBitmap(); 830 ReleaseOffscreen(); 831 ReleaseBrushCache(); // brushes are bound to the (now dead) render target 832 gdiInterop = null; 833 if (renderTarget != null) 834 { 835 try { Marshal.ReleaseComObject(renderTarget); } catch { } 836 renderTarget = null; 837 } 838 available = false; 839 } 840 841 private static void Log(string message) 842 { 843 Logger.Log("d2d", message); 844 } 845 846 #endregion 847 848 #region SearchForm D2D Helpers (text, rects, brushes) 849 850 private static IDWriteFactory dwFactory; 851 private static readonly Dictionary<string, IDWriteTextFormat> textFormatCache = new Dictionary<string, IDWriteTextFormat>(); 852 private static readonly Dictionary<int, ID2D1SolidColorBrush> brushCache = new Dictionary<int, ID2D1SolidColorBrush>(); 853 854 private static void EnsureDWrite() 855 { 856 if (dwFactory != null) return; 857 try 858 { 859 object factoryObj; 860 int hr = D2DApi.DWriteCreateFactory(0, D2DApi.IID_IDWriteFactory, out factoryObj); 861 if (hr == 0 && factoryObj != null) 862 dwFactory = (IDWriteFactory)factoryObj; 863 else 864 Log("DWriteCreateFactory failed: 0x" + hr.ToString("X8")); 865 } 866 catch (Exception ex) { Log("DWrite init error: " + ex.Message); } 867 } 868 869 public static IDWriteTextFormat GetTextFormat(string fontFamily, float size, bool bold) 870 { 871 EnsureDWrite(); 872 if (dwFactory == null) return null; 873 string key = fontFamily + "|" + size + "|" + (bold ? "B" : "N"); 874 IDWriteTextFormat fmt; 875 if (textFormatCache.TryGetValue(key, out fmt)) return fmt; 876 int hr = dwFactory.CreateTextFormat(fontFamily, IntPtr.Zero, 877 bold ? 700 : 400, 0, 5, size, "en-us", out fmt); 878 if (hr == 0 && fmt != null) 879 { 880 fmt.SetWordWrapping(1); // NoWrap 881 fmt.SetParagraphAlignment(0); // Near (top) 882 textFormatCache[key] = fmt; 883 } 884 else { Log("CreateTextFormat failed: 0x" + hr.ToString("X8")); return null; } 885 return fmt; 886 } 887 888 /// <summary>Get or create a cached solid color brush. Color packed as ARGB int.</summary> 889 public static ID2D1SolidColorBrush GetBrush(uint argb) { return GetBrushInt(unchecked((int)argb)); } 890 891 public static ID2D1SolidColorBrush GetBrushInt(int argb) 892 { 893 ID2D1SolidColorBrush br; 894 if (brushCache.TryGetValue(argb, out br)) return br; 895 float a = ((argb >> 24) & 0xFF) / 255f; 896 float r = ((argb >> 16) & 0xFF) / 255f; 897 float g = ((argb >> 8) & 0xFF) / 255f; 898 float b = (argb & 0xFF) / 255f; 899 var color = new D2D1_COLOR_F(r, g, b, a); 900 ID2D1SolidColorBrush newBr; 901 if (renderTarget == null) return null; 902 int hr = renderTarget.CreateSolidColorBrush(ref color, IntPtr.Zero, out newBr); 903 if (hr == 0 && newBr != null) { brushCache[argb] = newBr; return newBr; } 904 return null; 905 } 906 907 /// <summary>Get the active render target for external use.</summary> 908 public static ID2D1HwndRenderTarget ActiveTarget { get { return renderTarget; } } 909 910 /// <summary>Draw text using DirectWrite (GPU-accelerated).</summary> 911 public static void DrawTextD2D(string text, float x, float y, float w, float h, ID2D1SolidColorBrush brush, IDWriteTextFormat format) 912 { 913 if (renderTarget == null || brush == null || format == null || string.IsNullOrEmpty(text)) return; 914 var rect = new D2D1_RECT_F { left = x, top = y, right = x + w, bottom = y + h }; 915 IntPtr pFormat = Marshal.GetIUnknownForObject(format); 916 try { renderTarget.DrawText(text, (uint)text.Length, pFormat, ref rect, (ID2D1Brush)brush, D2D1_DRAW_TEXT_OPTIONS.CLIP, 0); } 917 finally { Marshal.Release(pFormat); } 918 } 919 920 /// <summary>Fill rectangle using D2D (GPU).</summary> 921 public static void FillRectD2D(float x, float y, float w, float h, ID2D1SolidColorBrush brush) 922 { 923 if (renderTarget == null || brush == null) return; 924 var rect = new D2D1_RECT_F { left = x, top = y, right = x + w, bottom = y + h }; 925 renderTarget.FillRectangle(ref rect, (ID2D1Brush)brush); 926 } 927 928 /// <summary>Draw line using D2D (GPU).</summary> 929 public static void DrawLineD2D(float x1, float y1, float x2, float y2, ID2D1SolidColorBrush brush, float width) 930 { 931 if (renderTarget == null || brush == null) return; 932 var p0 = new D2D1_POINT_2F { x = x1, y = y1 }; 933 var p1 = new D2D1_POINT_2F { x = x2, y = y2 }; 934 renderTarget.DrawLine(p0, p1, (ID2D1Brush)brush, width, null); 935 } 936 937 public static void ClearBrushCache() 938 { 939 foreach (var br in brushCache.Values) try { Marshal.ReleaseComObject(br); } catch { } 940 brushCache.Clear(); 941 } 942 943 #endregion 944 } 945}