using System; using System.Collections.Generic; //using SlimDX.Direct3D9; using System.Runtime.InteropServices; using RenderHookAPI.Interface; using SharpDX.Direct3D9; using RenderHookAPI.Hook.Common; namespace RenderHookAPI.Hook { internal class DXHookD3D9: BaseDXHook { public DXHookD3D9(CaptureInterface ssInterface) : base(ssInterface) { } Hook Direct3DDevice_EndSceneHook = null; Hook Direct3DDevice_ResetHook = null; Hook Direct3DDevice_PresentHook = null; Hook Direct3DDeviceEx_PresentExHook = null; object _lockRenderTarget = new object(); bool _resourcesInitialised; Query _query; SharpDX.Direct3D9.Font _font; bool _queryIssued; ScreenshotRequest _requestCopy; bool _renderTargetCopyLocked = false; Surface _renderTargetCopy; Surface _resolvedTarget; protected override string HookName { get { return "DXHookD3D9"; } } List id3dDeviceFunctionAddresses = new List(); //List id3dDeviceExFunctionAddresses = new List(); const int D3D9_DEVICE_METHOD_COUNT = 119; const int D3D9Ex_DEVICE_METHOD_COUNT = 15; bool _supportsDirect3D9Ex = false; public override void Hook() { this.DebugMessage("Hook: Begin"); // First we need to determine the function address for IDirect3DDevice9 Device device; id3dDeviceFunctionAddresses = new List(); //id3dDeviceExFunctionAddresses = new List(); this.DebugMessage("Hook: Before device creation"); using (Direct3D d3d = new Direct3D()) { using (var renderForm = new System.Windows.Forms.Form()) { using (device = new Device(d3d, 0, DeviceType.NullReference, IntPtr.Zero, CreateFlags.HardwareVertexProcessing, new PresentParameters() { BackBufferWidth = 1, BackBufferHeight = 1, DeviceWindowHandle = renderForm.Handle })) { this.DebugMessage("Hook: Device created"); id3dDeviceFunctionAddresses.AddRange(GetVTblAddresses(device.NativePointer, D3D9_DEVICE_METHOD_COUNT)); } } } try { using (Direct3DEx d3dEx = new Direct3DEx()) { this.DebugMessage("Hook: Direct3DEx..."); using (var renderForm = new System.Windows.Forms.Form()) { using (var deviceEx = new DeviceEx(d3dEx, 0, DeviceType.NullReference, IntPtr.Zero, CreateFlags.HardwareVertexProcessing, new PresentParameters() { BackBufferWidth = 1, BackBufferHeight = 1, DeviceWindowHandle = renderForm.Handle }, new DisplayModeEx() { Width = 800, Height = 600 })) { this.DebugMessage("Hook: DeviceEx created - PresentEx supported"); id3dDeviceFunctionAddresses.AddRange(GetVTblAddresses(deviceEx.NativePointer, D3D9_DEVICE_METHOD_COUNT, D3D9Ex_DEVICE_METHOD_COUNT)); _supportsDirect3D9Ex = true; } } } } catch (Exception) { _supportsDirect3D9Ex = false; } // We want to hook each method of the IDirect3DDevice9 interface that we are interested in // 42 - EndScene (we will retrieve the back buffer here) Direct3DDevice_EndSceneHook = new Hook( id3dDeviceFunctionAddresses[(int)Direct3DDevice9FunctionOrdinals.EndScene], // On Windows 7 64-bit w/ 32-bit app and d3d9 dll version 6.1.7600.16385, the address is equiv to: // (IntPtr)(GetModuleHandle("d3d9").ToInt32() + 0x1ce09), // A 64-bit app would use 0xff18 // Note: GetD3D9DeviceFunctionAddress will output these addresses to a log file new Direct3D9Device_EndSceneDelegate(EndSceneHook), this); unsafe { // If Direct3D9Ex is available - hook the PresentEx if (_supportsDirect3D9Ex) { Direct3DDeviceEx_PresentExHook = new Hook( id3dDeviceFunctionAddresses[(int)Direct3DDevice9ExFunctionOrdinals.PresentEx], new Direct3D9DeviceEx_PresentExDelegate(PresentExHook), this); } // Always hook Present also (device will only call Present or PresentEx not both) Direct3DDevice_PresentHook = new Hook( id3dDeviceFunctionAddresses[(int)Direct3DDevice9FunctionOrdinals.Present], new Direct3D9Device_PresentDelegate(PresentHook), this); } // 16 - Reset (called on resolution change or windowed/fullscreen change - we will reset some things as well) Direct3DDevice_ResetHook = new Hook( id3dDeviceFunctionAddresses[(int)Direct3DDevice9FunctionOrdinals.Reset], // On Windows 7 64-bit w/ 32-bit app and d3d9 dll version 6.1.7600.16385, the address is equiv to: //(IntPtr)(GetModuleHandle("d3d9").ToInt32() + 0x58dda), // A 64-bit app would use 0x3b3a0 // Note: GetD3D9DeviceFunctionAddress will output these addresses to a log file new Direct3D9Device_ResetDelegate(ResetHook), this); /* * Don't forget that all hooks will start deactivated... * The following ensures that all threads are intercepted: * Note: you must do this for each hook. */ Direct3DDevice_EndSceneHook.Activate(); Hooks.Add(Direct3DDevice_EndSceneHook); Direct3DDevice_PresentHook.Activate(); Hooks.Add(Direct3DDevice_PresentHook); if (_supportsDirect3D9Ex) { Direct3DDeviceEx_PresentExHook.Activate(); Hooks.Add(Direct3DDeviceEx_PresentExHook); } Direct3DDevice_ResetHook.Activate(); Hooks.Add(Direct3DDevice_ResetHook); this.DebugMessage("Hook: End"); } /// /// Just ensures that the surface we created is cleaned up. /// public override void Cleanup() { lock (_lockRenderTarget) { _resourcesInitialised = false; RemoveAndDispose(ref _renderTargetCopy); _renderTargetCopyLocked = false; RemoveAndDispose(ref _resolvedTarget); RemoveAndDispose(ref _query); _queryIssued = false; RemoveAndDispose(ref _font); RemoveAndDispose(ref _overlayEngine); } } /// /// The IDirect3DDevice9.EndScene function definition /// /// /// [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)] delegate int Direct3D9Device_EndSceneDelegate(IntPtr device); /// /// The IDirect3DDevice9.Reset function definition /// /// /// /// [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)] delegate int Direct3D9Device_ResetDelegate(IntPtr device, ref PresentParameters presentParameters); [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)] unsafe delegate int Direct3D9Device_PresentDelegate(IntPtr devicePtr, SharpDX.Rectangle* pSourceRect, SharpDX.Rectangle* pDestRect, IntPtr hDestWindowOverride, IntPtr pDirtyRegion); [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)] unsafe delegate int Direct3D9DeviceEx_PresentExDelegate(IntPtr devicePtr, SharpDX.Rectangle* pSourceRect, SharpDX.Rectangle* pDestRect, IntPtr hDestWindowOverride, IntPtr pDirtyRegion, Present dwFlags); /// /// Reset the _renderTarget so that we are sure it will have the correct presentation parameters (required to support working across changes to windowed/fullscreen or resolution changes) /// /// /// /// int ResetHook(IntPtr devicePtr, ref PresentParameters presentParameters) { // Ensure certain overlay resources have performed necessary pre-reset tasks if (_overlayEngine != null) _overlayEngine.BeforeDeviceReset(); Cleanup(); return Direct3DDevice_ResetHook.Original(devicePtr, ref presentParameters); } bool _isUsingPresent = false; // Used in the overlay unsafe int PresentExHook(IntPtr devicePtr, SharpDX.Rectangle* pSourceRect, SharpDX.Rectangle* pDestRect, IntPtr hDestWindowOverride, IntPtr pDirtyRegion, Present dwFlags) { _isUsingPresent = true; DeviceEx device = (DeviceEx)devicePtr; DoCaptureRenderTarget(device, "PresentEx"); return Direct3DDeviceEx_PresentExHook.Original(devicePtr, pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags); } unsafe int PresentHook(IntPtr devicePtr, SharpDX.Rectangle* pSourceRect, SharpDX.Rectangle* pDestRect, IntPtr hDestWindowOverride, IntPtr pDirtyRegion) { _isUsingPresent = true; Device device = (Device)devicePtr; DoCaptureRenderTarget(device, "PresentHook"); return Direct3DDevice_PresentHook.Original(devicePtr, pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion); } /// /// Hook for IDirect3DDevice9.EndScene /// /// Pointer to the IDirect3DDevice9 instance. Note: object member functions always pass "this" as the first parameter. /// The HRESULT of the original EndScene /// Remember that this is called many times a second by the Direct3D application - be mindful of memory and performance! int EndSceneHook(IntPtr devicePtr) { Device device = (Device)devicePtr; if (!_isUsingPresent) DoCaptureRenderTarget(device, "EndSceneHook"); return Direct3DDevice_EndSceneHook.Original(devicePtr); } RenderHookAPI.Hook.DX9.DXOverlayEngine _overlayEngine; /// /// Implementation of capturing from the render target of the Direct3D9 Device (or DeviceEx) /// /// void DoCaptureRenderTarget(Device device, string hook) { this.Frame(); try { #region Screenshot Request // If we have issued the command to copy data to our render target, check if it is complete bool qryResult; if (_queryIssued && _requestCopy != null && _query.GetData(out qryResult, false)) { // The GPU has finished copying data to _renderTargetCopy, we can now lock // the data and access it on another thread. _queryIssued = false; // Lock the render target SharpDX.Rectangle rect; SharpDX.DataRectangle lockedRect = LockRenderTarget(_renderTargetCopy, out rect); _renderTargetCopyLocked = true; // Copy the data from the render target System.Threading.Tasks.Task.Factory.StartNew(() => { lock (_lockRenderTarget) { ProcessCapture(rect.Width, rect.Height, lockedRect.Pitch, _renderTargetCopy.Description.Format.ToPixelFormat(), lockedRect.DataPointer, _requestCopy); } }); } // Single frame capture request if (this.Request != null) { DateTime start = DateTime.Now; try { using (Surface renderTarget = device.GetRenderTarget(0)) { int width, height; //If resizing of the captured image, determine correct dimensions if (Request.Resize != null && (renderTarget.Description.Width > Request.Resize.Value.Width || renderTarget.Description.Height > Request.Resize.Value.Height)) { if (renderTarget.Description.Width > Request.Resize.Value.Width) { width = Request.Resize.Value.Width; height = (int)Math.Round((renderTarget.Description.Height * ((double)Request.Resize.Value.Width / (double)renderTarget.Description.Width))); } else { height = Request.Resize.Value.Height; width = (int)Math.Round((renderTarget.Description.Width * ((double)Request.Resize.Value.Height / (double)renderTarget.Description.Height))); } } else { width = renderTarget.Description.Width; height = renderTarget.Description.Height; } // If existing _renderTargetCopy, ensure that it is the correct size and format if (_renderTargetCopy != null && (_renderTargetCopy.Description.Width != width || _renderTargetCopy.Description.Height != height || _renderTargetCopy.Description.Format != renderTarget.Description.Format)) { // Cleanup resources Cleanup(); } // Ensure that we have something to put the render target data into if (!_resourcesInitialised || _renderTargetCopy == null) { CreateResources(device, width, height, renderTarget.Description.Format); } // Resize from render target Surface to resolvedSurface (also deals with resolving multi-sampling) device.StretchRectangle(renderTarget, _resolvedTarget, TextureFilter.None); } // If the render target is locked from a previous request unlock it if (_renderTargetCopyLocked) { // Wait for the the ProcessCapture thread to finish with it lock (_lockRenderTarget) { if (_renderTargetCopyLocked) { _renderTargetCopy.UnlockRectangle(); _renderTargetCopyLocked = false; } } } // Copy data from resolved target to our render target copy device.GetRenderTargetData(_resolvedTarget, _renderTargetCopy); _requestCopy = Request.Clone(); _query.Issue(Issue.End); _queryIssued = true; } finally { // We have completed the request - mark it as null so we do not continue to try to capture the same request // Note: If you are after high frame rates, consider implementing buffers here to capture more frequently // and send back to the host application as needed. The IPC overhead significantly slows down // the whole process if sending frame by frame. Request = null; } DateTime end = DateTime.Now; this.DebugMessage(hook + ": Capture time: " + (end - start).ToString()); } #endregion var displayOverlays = Overlays; if (this.Config.ShowOverlay && displayOverlays != null) { #region Draw Overlay // Check if overlay needs to be initialised if (_overlayEngine == null || _overlayEngine.Device.NativePointer != device.NativePointer || IsOverlayUpdatePending) { // Cleanup if necessary if (_overlayEngine != null) RemoveAndDispose(ref _overlayEngine); _overlayEngine = ToDispose(new DX9.DXOverlayEngine()); _overlayEngine.Overlays.AddRange((IEnumerable)displayOverlays); _overlayEngine.Initialise(device); IsOverlayUpdatePending = false; } // Draw Overlay(s) if (_overlayEngine != null) { foreach (var overlay in _overlayEngine.Overlays) overlay.Frame(); _overlayEngine.Draw(); } #endregion } } catch (Exception e) { DebugMessage(e.ToString()); } } private SharpDX.DataRectangle LockRenderTarget(Surface _renderTargetCopy, out SharpDX.Rectangle rect) { if (_requestCopy.RegionToCapture.Height > 0 && _requestCopy.RegionToCapture.Width > 0) { rect = new SharpDX.Rectangle(_requestCopy.RegionToCapture.Left, _requestCopy.RegionToCapture.Top, _requestCopy.RegionToCapture.Width, _requestCopy.RegionToCapture.Height); } else { rect = new SharpDX.Rectangle(0, 0, _renderTargetCopy.Description.Width, _renderTargetCopy.Description.Height); } return _renderTargetCopy.LockRectangle(rect, LockFlags.ReadOnly); } private void CreateResources(Device device, int width, int height, Format format) { if (_resourcesInitialised) return; _resourcesInitialised = true; // Create offscreen surface to use as copy of render target data _renderTargetCopy = ToDispose(Surface.CreateOffscreenPlain(device, width, height, format, Pool.SystemMemory)); // Create our resolved surface (resizing if necessary and to resolve any multi-sampling) _resolvedTarget = ToDispose(Surface.CreateRenderTarget(device, width, height, format, MultisampleType.None, 0, false)); _query = ToDispose(new Query(device, QueryType.Event)); } } }