After finally installing Vista Home Premium at home, I found that a C# utility I had created for capturing a region of the screen suddenly stopped working reliably for windowed Direct3D applications.
Update: 2010-06-21: Applied fix as pointed out within the comments by digitalutopia1 (lines 71-74)
Update: 2010-03-29: An example of hooking the Direct3D API directly in C#:http://spazzarama.wordpress.com/2010/03/29/screen-capture-with-direct3d-api-hooks/
Note: If you are on Vista you may like to check this instead.
My code was previously using the GDI32 BitBlt method (see http://www.pinvoke.net/default.aspx/gdi32/BitBlt.html) which worked perfectly on Windows XP but was having intermittent issues with capturing windowed Direct3D applications on Windows Vista.
I took a quick look around, and decided that the most reliable approach would be to try and capture the image using the Direct3D API instead.
I am a DirectX novice, so this took a fair amount of cruising around forums and articles (the GameDev.net forums prooved to be a treasure trove of information). The gist of it was that I would need to capture the image by using the Direct3D devices front buffer.
Finding C# examples was a little difficult, and the Managed DirectX libraries were not an option as they are not being maintained past .NET 1.1. I then stumbled across “SlimDX” which provides a managed wrapper around DirectX 9 and 10. The original idea was posted here SlimDX — A Prototype MDX Replacement Library and now has an official website here SlimDX homepage.
I could now easily implement my screen capture code, and after only a few minor false starts around getting the initialisation properties correct, it was smooth sailing and required only a small amount of code to implement.
Here is a complete example extracted from my application (it requires a reference to the “SlimDX assembly”).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | using System;using System.Collections.Generic;using System.Text;using System.Drawing;using System.Runtime.InteropServices;namespace Spazzarama.ScreenCapture{ public static class Direct3DCapture { private static SlimDX.Direct3D9.Direct3D _direct3D9 = new SlimDX.Direct3D9.Direct3D(); private static Dictionary<IntPtr, SlimDX.Direct3D9.Device> _direct3DDeviceCache = new Dictionary<IntPtr, SlimDX.Direct3D9.Device>(); /// <summary> /// Capture the entire client area of a window /// </summary> /// <param name="hWnd"></param> /// <returns></returns> public static Bitmap CaptureWindow(IntPtr hWnd) { return CaptureRegionDirect3D(hWnd, NativeMethods.GetAbsoluteClientRect(hWnd)); } /// <summary> /// Capture a region of the screen using Direct3D /// </summary> /// <param name="handle">The handle of a window</param> /// <param name="region">The region to capture (in screen coordinates)</param> /// <returns>A bitmap containing the captured region, this should be disposed of appropriately when finished with it</returns> public static Bitmap CaptureRegionDirect3D(IntPtr handle, Rectangle region) { IntPtr hWnd = handle; Bitmap bitmap = null; // We are only supporting the primary display adapter for Direct3D mode SlimDX.Direct3D9.AdapterInformation adapterInfo = _direct3D9.Adapters.DefaultAdapter; SlimDX.Direct3D9.Device device; #region Get Direct3D Device // Retrieve the existing Direct3D device if we already created one for the given handle if (_direct3DDeviceCache.ContainsKey(hWnd)) { device = _direct3DDeviceCache[hWnd]; } // We need to create a new device else { // Setup the device creation parameters SlimDX.Direct3D9.PresentParameters parameters = new SlimDX.Direct3D9.PresentParameters(); parameters.BackBufferFormat = adapterInfo.CurrentDisplayMode.Format; Rectangle clientRect = NativeMethods.GetAbsoluteClientRect(hWnd); parameters.BackBufferHeight = clientRect.Height; parameters.BackBufferWidth = clientRect.Width; parameters.Multisample = SlimDX.Direct3D9.MultisampleType.None; parameters.SwapEffect = SlimDX.Direct3D9.SwapEffect.Discard; parameters.DeviceWindowHandle = hWnd; parameters.PresentationInterval = SlimDX.Direct3D9.PresentInterval.Default; parameters.FullScreenRefreshRateInHertz = 0; // Create the Direct3D device device = new SlimDX.Direct3D9.Device(_direct3D9, adapterInfo.Adapter, SlimDX.Direct3D9.DeviceType.Hardware, hWnd, SlimDX.Direct3D9.CreateFlags.SoftwareVertexProcessing, parameters); _direct3DDeviceCache.Add(hWnd, device); } #endregion // Capture the screen and copy the region into a Bitmap using (SlimDX.Direct3D9.Surface surface = SlimDX.Direct3D9.Surface.CreateOffscreenPlain(device, adapterInfo.CurrentDisplayMode.Width, adapterInfo.CurrentDisplayMode.Height, SlimDX.Direct3D9.Format.A8R8G8B8, SlimDX.Direct3D9.Pool.SystemMemory)) { device.GetFrontBufferData(0, surface); // Update: thanks digitalutopia1 for pointing out that SlimDX have fixed a bug // where they previously expected a RECT type structure for their Rectangle bitmap = new Bitmap(SlimDX.Direct3D9.Surface.ToStream(surface, SlimDX.Direct3D9.ImageFileFormat.Bmp, new Rectangle(region.Left, region.Top, region.Width, region.Height))); // Previous SlimDX bug workaround: new Rectangle(region.Left, region.Top, region.Right, region.Bottom))); } return bitmap; } } #region Native Win32 Interop /// <summary> /// The RECT structure defines the coordinates of the upper-left and lower-right corners of a rectangle. /// </summary> [Serializable, StructLayout(LayoutKind.Sequential)] internal struct RECT { public int Left; public int Top; public int Right; public int Bottom; public RECT(int left, int top, int right, int bottom) { this.Left = left; this.Top = top; this.Right = right; this.Bottom = bottom; } public Rectangle AsRectangle { get { return new Rectangle(this.Left, this.Top, this.Right - this.Left, this.Bottom - this.Top); } } public static RECT FromXYWH(int x, int y, int width, int height) { return new RECT(x, y, x + width, y + height); } public static RECT FromRectangle(Rectangle rect) { return new RECT(rect.Left, rect.Top, rect.Right, rect.Bottom); } } [System.Security.SuppressUnmanagedCodeSecurity()] internal sealed class NativeMethods { [DllImport("user32.dll")] internal static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); /// <summary> /// Get a windows client rectangle in a .NET structure /// </summary> /// <param name="hwnd">The window handle to look up</param> /// <returns>The rectangle</returns> internal static Rectangle GetClientRect(IntPtr hwnd) { RECT rect = new RECT(); GetClientRect(hwnd, out rect); return rect.AsRectangle; } /// <summary> /// Get a windows rectangle in a .NET structure /// </summary> /// <param name="hwnd">The window handle to look up</param> /// <returns>The rectangle</returns> internal static Rectangle GetWindowRect(IntPtr hwnd) { RECT rect = new RECT(); GetWindowRect(hwnd, out rect); return rect.AsRectangle; } internal static Rectangle GetAbsoluteClientRect(IntPtr hWnd) { Rectangle windowRect = NativeMethods.GetWindowRect(hWnd); Rectangle clientRect = NativeMethods.GetClientRect(hWnd); // This gives us the width of the left, right and bottom chrome - we can then determine the top height int chromeWidth = (int)((windowRect.Width - clientRect.Width) / 2); return new Rectangle(new Point(windowRect.X + chromeWidth, windowRect.Y + (windowRect.Height - clientRect.Height - chromeWidth)), clientRect.Size); } } #endregion} |
Note: before working with DirectX I strongly suggest having the DirectX SDKinstalled, know how to enable DirectX debug dll’s and logging, and have DebugViewhandy (see the debugging tutorial linked in resources below). WARNING! remember to turn off the debug dll’s and logging when finished, otherwise the next time you try to play your favourite Direct3D game it will crawl along at a snail pace (took me two days before I realised what was wrong – doh!).
Note 2: to capture images in full screen Direct3D applications requires the Direct3D device to be in cooperative mode. The only way to achieve this if the application doesn’t have cooperative mode set by default, is to implement a Direct3D hook which intercepts the API calls and changes the setting on the way through. As I was happy to continue in windowed mode I had no need to continue down that road – but it was an interesting read. I’m pretty sure that this is how applications like FRAPS would do it. These two pages should help you along if this is what you are after: GameDev.net: C++ Direct3D hooking sample, and Direct3d 9 hook v1.1
Resources:
- SlimDX – managed wrapper for DirectX 9/10
- http://www.gamedev.net – lots of info here
- http://directxcapture.wikidot.com/ – another managed wrapper for DirectX 9
- DirectX 9 Tutorials – Debug Info


0 comments:
Post a Comment
Note: only a member of this blog may post a comment.