Wednesday, December 21, 2011

DirectX Applications in WinRT

Windows 8 Metro Style apps can either use XAML as I discussed before, or they can use DirectX. This is great for Delphi because our in our newest framework FireMonkey the canvas is powered by DirectX on Windows platforms. So once we can create a DirectX, we should be able to turn that into a FireMonkey canvas, and the full power of FireMonkey should be available to the Metro Style application.

Let's look at what Visual Studio generates when you specify a new DirectX application, and try to recreate that with Delphi. Earlier I looked into creating an application using Windows.UI.Xaml. Application, but in order to use DirectX, you must activate the lower level Windows.ApplicationModel.Core.CoreApplication class, and construct your graphics device and surface from the ground up.

As an aside, internally, the Xaml Application has a CoreApplication as well. At //build/, Matt Merry gave a talk on Windows Runtime internals: understanding "Hello World" where he shows the internals of a simple hello world Xaml application, debugging down to the CoreApplication level. This is a must watch talk if you want to truly understand the inner workings of a WinRT application.

Back to Delphi. Like my first post about Xaml, this is all pretty rough and I don't think the code is ready to share as a complete project. But I'll discuss the finer points of most of it.

procedure Main;
var
  insp: IInspectable;
  factory: TViewProviderFactory;
begin
  Set8087CW($133f);  // Because we're using DirectX, disable all FPU exceptions
  factory := TViewProviderFactory.Create;

  OleCheck(RoGetActivationFactory(TWindowsString(SCoreApplication), ICoreApplicationInitialization, insp));
  (insp as ICoreApplicationInitialization).Run(factory);
end;
There's a bit of a difference here between the Xaml application and this one. Here, we're accessing ICoreApplicationInitialization from Windows.ApplicationModel.Core.CoreApplication. ICoreApplicationInitialization represents a set of static methods that is available for this class -- there is no instance of the CoreApplication object. Instead, we just need to provide the CoreApplication class with a IViewProviderFactory which knows how to create a runnable view. My implementation of IViewProviderFactory is simple:
type
  TViewProviderFactory = class(TInspectableObject, IViewProviderFactory)
    function CreateViewProvider: IViewProvider; safecall;
  end;

{ TViewProviderFactory }

function TViewProviderFactory.CreateViewProvider: IViewProvider;
begin
  Result := TViewProvider.Create as IViewProvider;
end;
The ViewProvider is also pretty boilerplate:
type
  TActivationEntryPoint = ( Unknown, DirectXApplication );  
  TViewProvider = class(TInspectableObject, IViewProvider)
  private
    FWindow: ICoreWindow;
    FView: ICoreApplicationView;
    FActivationPoint: TActivationEntryPoint;
  public
    procedure Initialize(window: ICoreWindow; applicationView: ICoreApplicationView); safecall;
    procedure Load(entryPoint: HSTRING); safecall;
    procedure Run; safecall;
    procedure Uninitialize; safecall;
  end;

procedure TViewProvider.Initialize(window: ICoreWindow; applicationView: ICoreApplicationView);
begin
  FWindow := window;
  FView := applicationView;
  FActivationPoint := TActivationEntryPoint.Unknown;
end;

procedure TViewProvider.Load(entryPoint: HSTRING);
begin
  if string(entryPoint) = 'DirectXApplication.App' then
    self.FActivationPoint  := TActivationEntryPoint.DirectXApplication;
end;

procedure TViewProvider.Run;
var
  View: TD3DView;
begin
  if FActivationPoint = TActivationEntryPoint.DirectXApplication then
  begin
    View := TD3DView.Create(FWindow, FView);
    try
      View.Run;
    finally
      View.Free;
    end;
  end;
end;
Once the application successfully gets a ViewProvider, It calls it's Initialize providing a Window and an ApplicationView. It then calls Load, specifying an entryPoint. This entry point is associated with Windows Application Contracts, and for now we're just looking at the launch contract. The entry point for the launch contract is going to be the entry point you specified in your appxmanifest, in the Application node. Here I've specified 'DirectXApplication.App'. After it's been Loaded, Run gets called, and we construct and run a View.

type
  TD3DView = class(TInspectableObject)
  private
    FWindow: ICoreWindow;
    FView: ICoreApplicationView;
    FRenderer: TD3DRender;
  public
    constructor Create(window: ICoreWindow; applicationView: ICoreApplicationView);
    procedure Run;

    procedure OnResize(sender: ICoreWindow; args: IWindowSizeChangedEventArgs);
    procedure OnDpiChanged(sender: IInspectable);
  end;

constructor TD3DView.Create(window: Windows_UI_Core_ICoreWindow;
  applicationView: Windows_ApplicationModel_Core_ICoreApplicationView);
var
  insp: IInspectable;
begin
  FWindow := window;
  FView := applicationView;

  // The default mouse cursor is the busy wait cursor, switch to a normal pointer.
  RoGetActivationFactory(TWindowsString(SCoreCursor), ICoreResourceFactory, insp);
  FWindow.PointerCursor := (insp as ICoreResourceFactory).CreateCursor(CoreCursorType.Arrow, 0);

  // Hookup events to update display if the Window size changes or the DPI changes
  FWindow.add_SizeChanged(TResizeHandler.Create(Self.OnResize));
  RoGetActivationFactory(TWindowsString(SDisplayProperties), IDisplayPropertiesStatics, insp);
  (insp as IDisplayPropertiesStatics).add_LogicalDpiChanged(TLogicalDpiChangedHandler.Create(OnDpiChanged));

  // Create a D3D render target
  FRenderer := TD3DRender.Create(FWindow);
end;

procedure TD3DView.Run;
var
  Timer: TStopwatch;
  lastTime, currentTime, frequency: Int64;
  timeTotal, timeDelta: single;
  insp: IInspectable;
begin
  FWindow.Activate;

  RoGetActivationFactory(TWindowsString(SDisplayProperties), IDisplayPropertiesStatics, insp);
  FRenderer.SetDPI( (insp as IDisplayPropertiesStatics).LogicalDpi );

  Timer := TStopwatch.StartNew;
  lastTime := 0;
  frequency := TStopwatch.Frequency;
  while True do
  begin
    currentTime := Timer.ElapsedTicks;
    timeTotal := (currentTime) / frequency;
    timeDelta := (currentTime - lastTime) / frequency;
    lastTime := currentTime;

    FWindow.Dispatcher.ProcessEvents(CoreProcessEventsOption.ProcessAllIfPresent);
    FRenderer.Update( timeTotal, timeDelta);
    FRenderer.Render;
  end;
end;
Here we see that the view plays a similar role as a message loop in a Win32 application. As long as we're running, we tell the application to process events that have occurred, update the render target, and finally present it to the screen. Here I'm using System.Diagnostics.TStopwatch to notify the render target how long it has been since the last update. Microsoft's DirectX Application template does something similar with QueryPerformanceCounter, which is actually the same thing TStopwatch does behind the scenes, although TStopwatch has the benefits of falling back on lower resolution timers if no high resolution is available, and it is available cross platform. It might be useful in your apps as well.

The details of my TD3DRender are pretty standard Direct3D calls, create a device, render target, back buffer stencil, swap chain, back buffer texture, and the only painting I'm doing is clearing to blue. The code is essentially a direct translation of everything in the Visual Studio's template D3DRenderer.cpp, and I don't think it adds anything by including it here. The only new thing is there's a new method on IDXGIFactory2 for D3D11, CreateSwapChainForImmersiveWindow. In Metro Style apps, you don't have access to Window handles, so we can't create the render target in with the usual arguments; instead we have to specify the IWindow.

With that, I have a DirectX application written in Delphi. One thing I noticed is I cannot run it as a normal executable the way I could with the XAML application I made. The call to Windows.ApplicationModel.Infrastructure.CoreApplication.Run(factory) fails with HRESULT $80004005, "Unspecified error". I'm not sure what happens differently when running from the Desktop, but I assume there is some initialization that doesn't happen. So in order to actually run this, I had to package and install it. I suspect it might be related to new compiler/linker features being needed for Windows 8. csc.exe and cl.exe both have new options specific for Windows8. "/t:appcontainerexe" to build an Appcontainer executable makes me think there might be something different they're linking in as a hint to Explorer; that's clearly not something the Delphi compiler is doing but something we need to investigate.

5 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Well done , Nice article .I heard that WinRT native UI controls cannot be used with Direct X apps(At least until now).I feel if we go behind DirectX we may need to lose many features of Metro UI.

    Again, well done for your involvement in Delphi and WinRT

    ReplyDelete
  3. By "native UI controls" you mean the XAML controls? Check out my older posts on the viability of Delphi using and consuming XAML based Metro UIs, it's very possible.

    I think you should be able to create a XAML view and a DirectX view in the same application and swap between them (don't quote me on that :-) ), but yes they are mainly mutually exclusive. The reason DirectX is interesting to Delphi is because DirectX backs FireMonkey, and the features from FireMonkey would then be available to you.

    And thanks for the feedback!

    ReplyDelete
  4. Wow, such a big and detailed instruction, very like such things! Yesterday I tried to do like u wrote, and fortunately I could fixed my problem! Thanks a lot, and in return I would like to recommend u one nice site https://yumdownload.com/directx where u can always find only the best versions of all soft including DirectX!

    ReplyDelete