windowcapture
исходный код / Effects/D2DRenderer.cs

D2DRenderer.cs

945 строк · 36,506 байт · модуль Effects
  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}