Monday 12 May 2014

TFireMonkeyContainer bugfix for instances created at runtime

TFireMonkeyContainer is a small open-source VCL component that can host a FMX form, allowing you to embed FireMonkey forms inside your VCL application.  It works with XE3 and above.
A 3D FireMonkey form embedded in a VCL application,
using TFireMonkeyContainer

This post is to note a bugfix that handles the case where instances of TFireMonkeyContainer are freed in a different order to the order in which they were created.  This is most likely to occur if you create instances dynamically at runtime, such as in a tabbed multidocument application (like a web browser): creating one on a new tab, but freeing them in different order, such as if a user closes a different tab.

If running in debug, you would get an assert in this situation.  If running in release, you would notice focus issues, where the host form may not have drawn its title bar as though it was the active form when the FMX form inside it was focused.  If you only ever had one embedded FMX form per VCL form or if you never created an instance at runtime, ie only used it by placing a TFireMonkeyContainer on a form at designtime, you wouldn't run into this bug, although this update does clean up the code and you should use this latest version anyway.

The bug was due to the control needing to subclass the VCL form on which it sits.  In some slightly silly code the implications of which I overlooked, this was done once per TFireMonkeyContainer instance, but should have been done only once for a given VCL form no matter how many embedded FMX forms were on that host VCL form, and removed only when the last TFireMonkeyContainer instance was freed.

If you haven't used TFireMonkeyContainer before, and you want to make use of FMX in your VCL app, try it out!  You can find the project on Google Code, and check out or update the source from SVN.  Read more posts about it on the Code & Components page.

Tuesday 6 May 2014

Mysteries of IDE plugins: Painting in the code editor (Part 1)

Part 1 of a series on how to write an IDE plugin that can paint on the IDE code editor line-by-line along with the code.

Introduction: IDE plugins


IDE plugins are a mysterious topic.  With little official documentation, most material is scattered across a variety of blogs and websites, often out of date and referring to pre-Galileo IDEs.  But the documentation is improving and there are a number of great websites (try David G Hoyle's - the best website on the the 'Open Tools API' I have seen) delving into the interfaces that OTAPI makes available.

If you want to write a normal IDE plugin, such as a dockable window containing your own material, something that interacts with the projects or project groups, adds or removes text to source code in the editor, implements a debug visualizer, etc, then the above collection of links plus some Googling should get you going.

But there are some things that the IDE's API simply does not expose, things that advanced plugins need to do - like painting on the code editor.  There is no painting access or interface in the published API.  So how do open-source (like GExperts or CnPack) or commercial (like Castalia) plugins do it?

I can't answer how commercial plugins work, since I don't know, though I suspect very similarly to the technique presented here.  But I can tell you a technique I figured out with the help of looking at how CnPack achieved it to solve a few issues.  It works well, and seems stable, reliable, and fast.

This is the first of a series of posts on advanced Delphi IDE plugin functionality.  There are several goals: now I've found out how to do some of these trickier things, I want to share the information.  Second, I am writing a family of IDE plugins - things to fix parts of the IDE that after many years of usage I wish worked differently, or to add functionality that doesn't exist.  And while I want to sell these plugins for a (small to trivial!) cost - I hope you, Delphi readers, will find them as useful as I will - I don't want to hide the information I learned about how to build them.  Quite the opposite: I'd love to see a healthy plugin ecosystem develop around Delphi and Appmethod, rather than the small number of open source and commercial plugins that currently exist.

The following assumes you have a rough idea of how to create an IDE plugin - ie how to create a BPL that it loaded by the IDE.  But even if you don't, you can read on anyway :)

Contents


  • Introduction, and a few quick notes on IDE plugins (above)
  • What is TEditControl?
  • Painting unsuccessfully: by hooking windows and messages
  • Painting successfully: patching IDE methods at runtime
By the end of this article, if you already know how to write an IDE plugin, you should be able to make your plugin paint on the code editor in an event that occurs each time a line is painted.  Completing the functionality, such as drawing in the right spot for each line, handling the gutter area, handling folded code etc, will be discussed in future articles in this series.

What is TEditControl?


Delphi/C++Builder is written mostly in Delphi, and so uses VCL menus, actions, and controls, an Application object, etc.  You can see this when writing standard plugins, where you use a descendant of TForm (eg a TDockableForm) to make a dockable window in the IDE, add images to the IDE's image list, add menu items, etc.

This extends to the code editor itself, an instance of TEditControl, an internal Embarcadero-only descendant of TWinControl.  Here you can see it in Spy++:

An instance of TEditControl seen inside the IDE.

There is a single instance of the edit control shared by all source code tabs in the main IDE window - the tabs are more like a TTabControl than a TPageControl.  However, you can have multiple edit windows, each with an edit control, even showing the same source code.

The relationship between TEditControls and the documented OTAPI interfaces, such as IOTAEditView, can get complicated and will be addressed in another article.

But how do you find this control and paint on it?

Painting unsuccessfully: by hooking messages


I tried a number of techniques that didn't work and I won't delve into great detail.  My rough approach was to hook the window messages or events related to painting, and also to scrolling or other events in order to update internal information in my own plugins.

For example, I tried setting the control's WindowProc to my own, in order to catch WM_PAINT messages - none were caught.  You could also try finding an OnPaint event or similar.  I also tried catching WM_HSCROLL and WM_VSCROLL messages via WindowProc, installing an application OnMessage handler (note if you try this approach that it uses a multicast event dispatcher, so don't assign your OnMessage event to Application.OnMessage directly), and installing a message hook, and none of those methods caught those messages either.  I don't know why: it's possible that I was hooking the wrong control (despite it being the edit control) or that the messages are handled somewhere else.  I don't know the internal structure of how the IDE interacts with the edit controls.

I would recommend you do not go down this road, since I had no success with this or several variations of it.  (That's one short sentence that describes quite a bit of spent time.)

To do any of these, you also need to find instances of the edit control itself, wherever it is (or they are) in the IDE, another minor complication.  But it turns out that purely for painting, you don't need to know about any specific instance of the edit control.  Instead...

Painting successfully: patching IDE methods at runtime


One method of implementing painting I had read about online was hooking or patching methods of the IDE classes.  This was referred to obliquely, as 'you can patch the methods', with no details about how.  So figuring out how was my next approach.  There are two steps: how to patch any arbitrary method, and then finding the right method(s) to patch.

How to patch an arbitrary method at runtime


A method has an address: a location in memory at which it starts.  Virtual methods or thunked (eg most Windows API) methods jump to this from another location in a variety of ways.  To patch a specific method, you need to find its true location (following the virtual call or the thunk) and then overwrite the first few bytes of the method itself with a call to jump to your replacement.  If your replacement does everything you want, that's all you need to do, but if you want to call the original implementation as well then you need to replace the bytes you overwrote with their original contents and then call the original method by its original address.

Luckily there are several libraries out there for doing this at runtime in Delphi.

The patching code I use is a variant of Chau Chee Yang's code.  (You could probably also use another, such as the Delphi Detours Library.)  Using it, you can patch a method with code similar to:

  FOriginal := TCodeRedirect.GetActualAddr(...address of a method...);
  FHooked := TCodeRedirect.Create(@FOriginal, @ReplacementMethod);

GetActualAddr is a method that checks for an indirect jump.  I have not investigated the details of how well this handles both virtual methods and WinAPI-style thunked methods - in fact while I know the theory of how both are implemented I'm hazy on the exact details since I've never had to examine them closely.  This code works as-is for the purposes of the patching done here.

My version of the above code includes a bugfix for the Disable method: the code on the linked page attempts to write to memory to patch back the method, but it fails because it does not change the protection of the memory first allowing it to be written to. It also does not re-protect it once written, or flush the instruction cache afterwards.  A simple modification based on Enable gives:

procedure TCodeRedirect.Disable;
var
  OldProtect: Cardinal;
  P: pointer;
  n: NativeUInt;
begin
  if FInjectRec.Jump <> 0 then begin
    // David M - based on Enable()
    P := GetActualAddr(FSourceProc);
    if VirtualProtect(P, SizeOf(TInjectRec), PAGE_EXECUTE_READWRITE, OldProtect) then begin
      WriteProcessMemory(GetCurrentProcess, GetActualAddr(FSourceProc), @FInjectRec, SizeOf(FInjectRec), n);
      VirtualProtect(P, SizeOf(TInjectRec), OldProtect, @OldProtect);
      FlushInstructionCache(GetCurrentProcess, P, SizeOf(TInjectRec));
    end;
  end;
end;


Finding the right method to patch


You can now patch a method on a Delphi class live at runtime.  But which method?

At this point, I started searching through the methods in coreideX.bpl looking for appropriate ones to patch.  It looked like being a long search with many false starts and crashes based on incorrect method prototypes.  But I realised that there are open-source projects which implement editor painting already and they may well use this or another method, and so I looked at one to see how they achieved it: CnPack, which I had already downloaded based on David Heffernan's suggestion for how to keep track of code folding - a topic I will return to in a later article.
Note: I have to thank the authors of CnPack for their code, which helped me solve some problems with my own.  It took me quite some time to figure this out, and I gratefully used their code for reference and help. 
CnPack does indeed patch IDE methods - in fact it patches a lot of IDE methods, many more than just this one.  They patch one in particular, though: not a generic paint method for the control, but a method PaintLine - which looked perfect, since the purpose of painting on the edit control is (probably) to paint on specific lines of code, not to paint 'in general'.

Remember, patching the TEditControl.PaintLine method means that whenever any instance of TEditControl tries to paint a line it calls your implementation instead of its own.  You don't need to know about specific instantiations, just the class, because you are writing a replacement method for the class.  Your method will then call into the original patched-out code, in order to draw normally, as well as performing whatever painting it wants to do.

Note: this is completely unsupported by Embarcadero (or me.)  Their internal code can change at any time, even in the one version of the IDE.  Nevertheless, this specific method seems to be stable since at least Delphi 2010.  CnPack has a number of different method prototypes for different versions of the IDE, so this has changed in the past and may in the future.

The method in question is Editorcontrol.TCustomEditControl.PaintLine.  It resides in coreide*.bpl, such as coreide160.bpl, which is always loaded into the IDE.

PaintLine seen in Dependency Viewer
In the above screenshot, you can see PaintLine in Dependency Viewer with a "mangled" name.  Once demangled via tdump, it is (given in C++ format):

__fastcall Editorcontrol::TCustomEditControl::PaintLine(Ek::TPaintContext&, int, int, int)

We don't know the structure of TPaintContext (which would be very useful) nor the meanings of those three integer parameters (yet; actually CnPack has deciphered two of them.)  Also not shown is the implicit Self parameter.  But this is enough to create a stub.

Hooking PaintLine and using it to paint


We already have a prototype for the method, but in the form of an object method not a standalone procedure.  To hook, we need to define a compatible method with the extra Self parameter:

TPaintLineProc = function(Self: TWinControl; A: Pointer; B, C, D: Integer): Integer; register;

A is a pointer / reference to TPaintContext, which we don't have the definition for so cannot use.  B, C and D are the three int params.  However, the first parameter is the normally-hidden Self parameter; that is, the object on which this method is called.  Note the register calling convention, which so far as I can tell is fastcall by another name.  This allows you to implement an object-oriented method - one called on an object, that is, with a Self parameter - in procedural style.

Create a stub method using this prototype:

function HookedIDEPaintLine(Self: TWinControl; A: Pointer; B, C, D: Integer): Integer;
begin
  // ...
end;

Note: 'Self' here is a TWinControl, since we know that the edit control is a TWinControl descendant, and we can assume that the patch works and will only be called with a TEditControl 'Self' parameter.  You could also make it a TObject and check its type at runtime.  I did this in my first implementation.

To be completely clear, this is not an object method but a plain, non-OO procedure.

And to get it to be called, hook the IDE's method:

var
  FOriginalPaintLine: TPaintLineProc;
  FPaintLineHook : TCodeRedirect;

  ...

  FOriginalPaintLine := TCodeRedirect.GetActualAddr(GetProcAddress(FCoreIDEHandle, '@Editorcontrol@TCustomEditControl@PaintLine$qqrr16Ek@TPaintContextiii'));
  // Patch the code editor's PaintLine to use our method
  FPaintLineHook := TCodeRedirect.Create(@FOriginalPaintLine, @HookedIDEPaintLine);

Note: FCoreIDEHandle is the module handle of coreide*.bpl, loaded into the current process.  Finding this is left as an exercise for the reader, and there are variety of ways.  This method may get you started.

Now, fill in the stub method to call the original method:

function HookedIDEPaintLine(Self: TWinControl; A: Pointer; B, C, D: Integer): Integer;
begin
  FPaintLineHook.Disable;
  try
    Result := FOriginalPaintLine(Self, A, B, C, D);
  finally
    FPaintLineHook.Enable;
  end;
end;

You need to disable the hook before calling through the pointer to the method because otherwise you will recursively call back into your own hooked implementation, because you are calling the address of the patched code that jumps to your method (because that is the address of the real original method, the first few bytes of which were overwritten.)  Unpatch it back to what it was before calling it.

Note: Another exercise for the reader: call back into your own painting class to call your own code, rather than remain in procedural code only.

At this point you have a lot of work complete, but no visible result at all because your hooked method still doesn't do any painting and just calls the original.  To paint, you need a canvas, and luckily we are dealing with VCL controls and know that the edit control is a descendant of TWinControl.
Note: Since Delphi is written in Delphi, you can find out quite a lot of information when debugging the IDE in a second instance of the IDE - one example is inspecting the Self object (the edit control) above if you break in your method.

There is a handy VCL class to represent a canvas on a TWinControl: TControlCanvas.  Create and use one:

  Canvas := TControlCanvas.Create;
  Canvas.Control := Self; // The TEditControl
  Canvas.TextOut(0, 0, 'Hello world');

The results

Voila!  You now have text on the code editor:

Hello world!

Next steps


There are a number of important missing elements that need to be addressed in order to turn this proof-of-concept code into useful code:
  • It always draws at (0, 0), not at the line that is supposedly being painted.  Since this is called for every line, the above 'Hello world' text is actually drawn over itself in the same palce many times, once for each line the code editor paints.  We need to extract the line information and paint at the correct offset down for each line, and correct offset right to handle the code gutter
  • It doesn't have any idea what unit or text it is actually drawing over
  • It doesn't have any idea what lines of text are visible
    • ...and when it does, that is going to have to include handling code folding
Luckily, these are also solvable and the next article will address these problems too.  Until then, consider we have made great progress: we can now paint on the code editor from an IDE plugin!


Why am I investigating these topics?


I'm glad you asked :)

I've used Delphi since it was Turbo Pascal, first as a teenager/student and then over the past decade in action as my day-to-day IDE - including C++ Builder.  There are things about the IDE I wish worked differently: mistakes I keep making because I expect certain behaviour, even though I should know by now it works subtly differently.  There are also features I wish it had, and feature tweaks that would just make it a bit 'slicker', something it needs in order to compare to IDEs like Visual Studio which have a very polished UI.  Polish matters in an application you use all day, both visually and in behaviour.

I run a small Delphi consulting company called Parnassus, through which I solve problems and write good code.  I'm expanding and am writing a family of small IDE plugins, each of which improves the IDE by changing current behaviour or adding new features.  The first of these, the one through which I have learned about the problems and techniques documented in this blog series, is small - it was my learning project - but useful: a new implementation of bookmarks, one much better than the bookmarks in the IDE or GExperts.

Preview of an alpha, pre-release version of Parnassus Bookmarks

It will be available soon, and if you are interested in beta-testing please contact me by email or by commenting below.

Saturday 3 May 2014

Useful reference about disabling specific compiler warnings

Earlier today while working on an IDE plugin, I got the following compiler warnings:
[dcc32 Warning] W1029 Duplicate constructor 'TLineDifference.CreateEqual' with identical parameters will be inacessible [sic] from C++ 
[dcc32 Warning] W1029 Duplicate constructor 'TLineDifference.CreateAdded' with identical parameters will be inacessible [sic] from C++  
This is caused by a record type having two or more constructors taking the same parameters.  (For interest, the type TLineDifference here represents a single difference in two text files, ie one of many calculated when diff-ing source code, for example.  The type of difference - a line being added, removed or equal - could be a parameter but when using the type in code, it is more readable and clear-in-intent for it to be part of the constructor name.)

In the past I've dealt with C++ compatibility warnings, in code that is not intended for C++ consumption, by making sure the "Project Options > Delphi Compiler > Output - C/C++ > C++ Output file generation" option was set to "DCUs only".  But in this case, it didn't have any effect.  And since the code in question is (a) internal to the BPL and (b) is intended for Delphi use only I wanted to disable the warning.

Enter this very useful blog post on Delphi's Hidden Hints and Warnings.

For each warning code (eg W1029) there is a corresponding name you can use either on the command line or in a {$WARN} directive to enable or disable it, restore it to its default on/off state, or promote it to an error.  For example, using the name for W1029 gives:
{$WARN DUPLICATE_CTOR_DTOR OFF}
The page has a table of code to names - very useful - and some other miscellaneous information.

Some side notes:
  • You have to use the name not the warning code.  I would prefer to write something like {$WARN W1029 OFF} because it's then self-documenting what warning it is that is being blocked, but neither this nor variations of this syntax work.  You must use the name not code.
  • This specific warning, W1029, has to be turned off in the project file not in the unit that generates the warning (in fact, the warning doesn't specific a file or line number.)
  • The blog has a number of other interesting articles on Delphi topics, such as using generics on enumerated types which do not have RTTI.
  • In a 'it's a small world' coincidence, while writing this from Estonia, I believe I knew the blog author Marc ten or more years ago as a friend-of-a-friend when I lived in the same town in Australia.  I don't think either of us knew the other used Delphi.  It is a small world indeed.

Sunday 23 February 2014

"Hidden Features of the Delphi IDE" on Stack Overflow - a call for useful answers

Recently Jeroen Pluimers and Jim McKeeth posted about three "Hidden Features of..." questions on Stack Overflow that were either deleted or nominated for deletion.  They are:
The third one was deleted some time ago - this was not the first time I have followed a link to that question and found it deleted.  So I asked it be undeleted, and now it has.  (Turns out it was reopened, but reopening a deleted question doesn't undelete it.)

However, the discussion on my please-reopen question makes a good point: this question does not have many high-quality answers.  The vast majority seem to be sourced from a single keyboard-shortcuts page. Many aren't well formatted. Some are useful, eg little-known timesavers. Some are not. I now feel slightly embarrassed for having asked it be undeleted. As is, in its current state I'd agree it should be closed, although I'd still disagree with the deletion because I don't believe in deleting any useful content.

The same question for other IDEs, such as this one for XCode, shows the level of quality such a question can have and the useful resource answers can be for users of an IDE. I personally find the good "Hidden Features of..." threads amazingly interesting.  So this is a call to action: can we, Delphi users, improve the answers and show that such a question can be worth keeping alive?

(One useful possibility: the question is from 2010.  There have been five versions of Delphi since then.  I think Embarcadero will have added some useful stuff in that time. Let's get it visible!)

Update a couple of hours later: Hidden Features of the Delphi Language (the first link, and a really cool set of answers) has been deleted.  Hidden Features of Oxygene is on hold.  Hidden Features of XCode, a page I used to demonstrate that the Hidden features of IDE X questions can produce high-quality resources, is now locked.  All these have occurred since I asked for the initial question to be reopened.  I am sorry to think that by asking for one question to be undeleted, I may have inadvertently caused great damage to others.

For the IDE Features page: I have edited some posts in the initial question, but the question is now locked and I cannot edit it to provide links to answers, for example, to follow the way this question about Python is organised, suggested in the comments to my undelete request. This means I can't improve the question, and no-one can add new answers.

Sunday 26 January 2014

DWS Mandelbrot Explorer Mark II, and random notes about FireMonkey and threads

The DWS Mandelbrot Explorer, which renders tiles generated by Eric Grange's tile server, has been updated.

The following is a braindump of information about the app, about FireMonkey, about threading, and about how they all interrelate. I think some points will be interesting to you.

Miscellaneous things I've learned

  • It turns out that you cannot reliably use two FireMonkey Direct2D TBitmaps in two different threads at the same time. The code had a background thread to download the fractal data and create a tile bitmap, which was then passed to the main thread to draw. (Only one thread accessed each TBitmap at a time, but several threads used independent TBitmaps concurrently.) Sometimes this results in silent failures to update the texture object backing the TBitmap data. The DirectX methods CopyResource and DrawBitmap, and possibly others, can fail.
    I spent a long time investigating this, seeing what I might be able to do to hack in enough thread-awareness into the library that using two theoretically-independent bitmaps at the same time would work. The code is strongly designed around shared objects, used for all operations. I've looked into:
    • Direct3D factories and using the ID2D1MultiThread interface to synchronize (only Windows 7 and up though);
    • the (different) ID3D10Multithread interface for synchronization, which works brilliantly right up until it deadlocks;
    • surface sharing between APIs;
    • DXGI shared surfaces;
    • per-thread instances of render targets and textures;
    • hand-rolled synchronization around specific areas; 
    • ...you name it. Several things have been almost successful but nothing is reliable, and the more I read, especially when there are caveats about certain functionality not working on Vista but only on 7, or only on hardware and not on WARP, the more I understand why it makes sense for the FireMonkey Direct2D canvas implementation not to have even tried to implement it.
      Because of this, there's a lot more processing in the app's main thread, generating the bitmaps as well as just drawing everything onscreen. This is not ideal. It seems FMX apps will unfortunately have to stay away from second-thread graphics processing but this is due to the underlying graphics libraries, specifically here Direct2D. Ie, it's not really FireMonkey's fault. If you really need to, you can use a specific graphics library in your other threads, just don't use lots of plain TCanvases and TBitmaps and expect it to work - keep them in one thread. Graphics32 and VPR might be worth investigating.
  • Also, hacking thread support into a non-threaded library you're not overly familiar with is a hard task. Just saying. Fun though.
  • There seems to be no cross-platform Delphi RTL equivalent of WaitForMultipleObjects. The TEvent class wraps an event, but what happens when you want to wait for two of them? This would make a good open source class, I think (a list of events, with wait methods?) but right now this app's code re-uses one event for several things making it a bit complicated.
  • If you want to interrupt a TIdHTTP downloading (via blocking call TidHTTP.Get), call TIdHTTP.Disconnect. The object seems to be reusable afterwards for a subsequent Get call. I use Disconnect to achieve fast termination of thread downloading when I want to stop the background thread objects and need to wait on them, so need them to stop quickly.
  • The Quartz canvas on OSX is noticeably slower than Direct2D.  I think, without measuring, it might even be slower than GDI+. I am curious why this is and if anyone else has seen the same thing in their FireMonkey apps.
  • TImageControl on OSX does not render correctly when you write to its Bitmap. I traced through the code and am not sure what it's doing - the code looks valid - but I can state that after drawing onto the Bitmap what was displayed onscreen, a blank solid color, was wrong. Since I just needed a canvas to draw on and a control to give mouse events, I ended up just using a TPanel.
  • A FireMonkey TForm is missing some seemingly obvious events: there is no OnClick or OnDblClick.  I shouldn't need a client-aligned panel to give mouse click events, but I did.
  • The look of FireMonkey on Windows has improved greatly since XE2, and is quite close to how native Windows controls / the VCL looks. The following image (click to expand) has exactly the same controls placed in the same position with the same dimensions on a XE2 FMX form, a XE4 FMX form and VCL FMX form.  The XE2 one doesn't look very native; the XE4 one is quite similar to the VCL.

    In some cases I think FireMonkey is better. Look how the TTrackBar's edges align nicely in FireMonkey, for example, but don't in the genuine native control - something that bugs me every time I use the real one.

Notes about the app itself

FireMonkey in practice

  • The point of the app was a write a non-trivial FireMonkey app and see, in practice, what issues arose. I can confidently state I have learned a lot about FireMonkey building this app. That was the goal.
    Has there been anything particularly bad? I don't think so. There were three areas:
    • Bugs: none serious. Graphics performance is easily fixable.
    • Cross-platform: some code is slightly less clear than it could be, because I've stuck to using the cross-platform RTL and FMX only.  (For example, had I been able to drop back to using WaitForMultipleObjects(Ex) some code might have been clearer.)
    • Framework: FireMonkey is different to the VCL, and I limited myself to sticking to it and the Delphi RTL / libraries only rather than using, say, Windows API code. This is no different to learning and using any other framework: the same problems occur learning the .Net libraries, Cocoa, etc.
  • On the whole, FireMonkey is a good framework the details of which you have to learn, like any other. It's backed up by the Delphi RTL which is fairly comprehensive. Put it this way, if you can write a VCL app using Delphi, you can write a FireMonkey one.

Other

  • Finally, thanks to François Piette, who submitted some changes to port from XE2 to XE5. I integrated his changes which improved the code - especially bitmap generation from the downloaded data - greatly. Thanks François!
  • Download version 1.1 or find the source code here. Windows 32, 64 and OSX 32.

Next up will be something completely different.

Saturday 25 January 2014

A unit to enable Direct2D in FireMonkey where possible

A few days ago I posted about FireMonkey's choice of canvas classes, where it would choose to render via GDI+ instead of via Direct2D.  There were two fixes: one (untested and possibly dangerous) enabled hardware rendering on DirectX9-class hardware, but required editing the FireMonkey source; the second (known safe and tested) enables optimised software rendering via WARP when DirectX10 hardware support is not available.  This last isn't as good when you have DX9 hardware, but it doesn't required editing any FireMonkey source and WARP renders surprisingly fast - it's certainly good enough for 2D/HD applications.

This code is now available as a unit you can include in your FireMonkey apps.  It is ifdef-ed so it will only function on Windows when compiled with XE4 and XE5. (Thanks to commenter Skamradt in the original post for suggesting this.) That means you can include and use it when compiling for OSX or Android without having to worry about it not compiling on those platforms, and that it will also only apply for known IDE / RTL versions that require this patch.  I have only briefly tested it on XE4 (where it works) and XE2 (where of course it doesn't, but compiles anyway.)  Suggestions / changes are welcome.

To use it, add the unit to your .dpr file and then add a call to TryUseDirect2D before Application.Initialize, like so:
program Project1;

uses
  FMX.Forms,
  Unit1 in 'Unit1.pas' {Form1},
  FMXDirect2DFix in 'FMXDirect2DFix\FMXDirect2DFix.pas';

{$R *.res}

begin
  FMXDirect2DFix.TryUseDirect2D; // <-- The key method

  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

The code is currently checked in to the source of my DWS MandelbrotExplorer app - the rest of the code of which is in a halfway state, so no point looking at it right now :)

  • You can find the unit here.
  • It's MPL licensed, so useable in both commercial and open-source software.
  • It makes a big speed difference for FireMonkey apps on my Windows 7, non-DirectX-10-hardware.  It should make a noticeable difference for anyone on a recently patched (with the Platform Update) Vista or 7 without DirectX10 hardware, which includes those running Windows in a virtual machine like Fusion.

Monday 20 January 2014

FireMonkey canvas classes and a bugfix to speed up your apps

Everything you need to know about FireMonkey canvases - and a performance boost bugfix for some people as well!

I recently posted my first real-world FireMonkey app, which gave a zoomable, scrollable, very interactive view of the Mandelbrot fractal using the precomputed DWS Mandelbrot tiles. It worked fine on my computer.

Those are famous last words.

Soon the comments on that page were filled with people saying it didn't work: the UI said tiles were downloading etc, but it drew only a blank solid colour where the fractal should have been. I made an educated guess that the problem only happened when using the Direct2D canvas, and put out a "fix" that restricted it to drawing using GDI+. This fix worked - it draws - but GDI+ is slow, and the app as it's currently available is not of a quality I feel personally comfortable having publicly available with my name attached. Clearly I need to fix it. But how?

This is a perfect example of why being aware of the different canvases in FireMonkey matters. You need to test with each one that your app could possibly end up using on an end-user's machine, which means you need to know what they are, when they're chosen by FMX to be used, and how to force a specific choice in order to test each case. Moreover, there is (IMO) a bug in Firemonkey's logic about which class to choose when, which results in your apps rendering much more slowly than they need to in some use cases, and you may want to tweak some code in order to fix this and make your app render faster.

What's in this article?

  • The role of canvases in FireMonkey rendering
  • Overview of each possible Windows canvas class: GDI+, Direct2D, and GPU
  • How does FireMonkey choose which canvas class to use?
    • Investigating when Direct2D is chosen vs GDI+, and we find a bug
    • Fixing the bug - three possible solutions
  • For testing: how to force the selection of a specific class
    • Checking what class you are using
  • Summary
This is a long article - two and a half thousand words - so let's get going.

The role of canvases in FireMonkey rendering


FireMonkey is a cross-platform UI toolkit. As such it needs to be able to render everything onscreen independent of the underlying graphics framework - it needs one API you and I can code against that runs on Windows and OSX and iOS and Android.

It achieves this by using a variety of different canvas classes.  That is, when you access a TCanvas such as Form.Canvas or TBitmap.Canvas, due to the wonderfulness of polymorphism the actual class you are using can vary widely.  Here are the possibilities:
  • TCanvasGDIPlus (Windows)
  • TCanvasD2D (Windows)
  • TCanvasGpu (Windows)
  • TCanvasQuartz (OSX)
  • TCanvasQuartz (iOS, implementation appears independent of the OSX class with the same name)
  • At least one more for Android in XE5+. 
Let's write off the platforms that only have a single canvas implementation - OSX, iOS, and probably Android. (I don't have XE5 and googling didn't show much about the underlying code.) If you're using one of those platforms, you are by default testing using the only canvas class and this is a non-issue. But that leaves three possible canvas classes that your app could end up using on Windows. (Even if you know about the Direct2D and GDI+ canvases, I bet you didn't know about the 'GPU' canvas. I sure didn't.)

Each Windows canvas class


GDI+


TCanvasGDIPlus is the default, fallback canvas. It uses GDI+, a software-only, fairly slow, API provided by Microsoft in the Windows XP days. It will run on anything, but your rendering performance may not be great. For example, in my fractal app which draws anywhere from four to a few dozen 256x256 tiles at various scales on the window with every paint, at the default small window size click-dragging to navigate is fast. But if you maximise the window, and the rendering area becomes much larger, scrolling around - which invalidates with every mouse movement, effectively drawing as fast as possible - is painfully laggy. This is not FireMonkey's fault. It is one of the problems with using GDI+, and I have experienced the same problem drawing complex interactive UIs with GDI+ before.

If your app is rendering using this class - I show how to find out which class later - I strongly recommend you find out why and do what you can to fix it. In general, avoid using this class if possible.

You will always end up using the class on Windows XP, since it's the only one supported. On all other versions of Windows, Vista and above, 99% of the time you will be able to use TCanvas2D instead (once you fix a problem with when it's chosen) and I highly recommend you do this.

Direct2D


TCanvasD2D uses Direct2D, a 2D API implemented over Direct3D, which is available on Vista SP2+ and above. It is hardware-accelerated and fast, and theoretically the default. The quick answer is that you want to use this class if at all possible, but you may need to make some code changes to do it. Without some very small tweaks, there are cases where FireMonkey will choose a GDI+ canvas instead of a D2D one on hardware where D2D would run faster - much, much faster. This is rare, but my setup is one where it occurs.

GPU


TCanvasGpu is turned off by default, and is only used if the global FMX.Types.GlobalUseGPUCanvas is true. (Set this in your project file before Application.Initialize.) It's quite neat in that it uses a base class TContext3D to do its work, which has a very similar system for choosing which subclass is appropriate to instantiate as the canvas system. There are context classes for D3D9, D3D10, GLES and Quartz.

The first time I tried this out, it crashed immediately - FillText ends up calling TCharHelper.ConvertToUtf32 with an empty string, which raises an exception. Reading the preceding code, which seems to implement text wrapping, I don't understand why it's trying to do what it is.

TCanvasGpu running on Windows 7 on DX9
hardware. Yes, there is a whole TTrackBar
between those two buttons. (See it? Me either.)
This class is turned off by default and I do not
 recommend manually enabling it.
On my machine, it uses a TDX9Context to draw. There are noticeable severe rendering bugs. In my fractal app one control, a TTrackBar, doesn't draw at all. Text draws 'bold', which looks similar to the effect you get drawing antialiased text over itself many times. Buttons had one-pixel-wide edges missing.

I don't know how much of this is due to the TDX9Context it was using, and how much is due to TCanvasGPU itself. Since on DX10-class hardware FireMonkey will use Direct2D, DirectX-9 class hardware is the only use case on Windows for this class. (As it turns out, we should normally use Direct2D even for DirectX9 hardware. More information below.) The severity of the bugs are, I suspect, why it is turned off by default. I do not recommend manually turning it on.

How does FireMonkey choose which canvas class to use?


In FMX.Types.pas is a method TCanvasManager.GetDefaultCanvas. This returns a metaclass which is used to instantiate the actual canvas class. The first time this method is called, it assembles a list of possible, valid canvases which the current platform supports and then from that list it chooses which one is best to instantiate. There are some complex if statements about whether a class is the default and whether to try to use a software canvas, but in my testing these didn't make any practical difference.

The key is in the TPlatformWin.RegisterCanvasClasses method, which out of the GPU, D2D and GDI+ canvas classess tests which can be used and where possible adds them to this list. It only 'registers' (adds to the list) the GPU canvas if GlobalUseGPUCanvas is true, and by default it is false (see above.) That leaves D2D and GDI+.

Investigating when Direct2D is chosen over GDI+, and a FMX bugfix


First off, the easy case: GDI+ is the fallback, and is always available on machines that meet the FireMonkey requirements. It is always registered. This means that if the Direct2D class is not registered, your app will end up using GDI+.

Direct2D is trickier. And remember, any bug or quirk here that invalidly thinks D2D is not the right choice will cause the GDI+ canvas to be chosen instead, and that's bad.

Fmx.Canvas.D2D.pas's RegisterCanvasClasses method checks the Direct3D 10 capabilities reported by DirectX, and registers the D2D canvas if the D3D10 driver type is either hardware or WARP. This latter is interesting: the Windows Advanced Rasterization Platform is a software rasterizer supporting Direct3D 9.1 through 10.1 feature levels, and by all accounts is a very good one.  It is part of the DX11 runtime which you need to have installed, which is part of the platform update for Vista or Windows 7. You should already have these automatically through Windows Update.
Direct2D applications benefit from hardware-accelerated rendering on modern mainstream GPUs. Hardware acceleration is also achieved on earlier Direct3D 9 hardware by using Direct3D 10-level-9 rendering. This combination provides excellent performance on graphics hardware on existing Windows PCs.
...
When rendering in software, applications that use Direct2D experience substantially better rendering performance than with GDI+ and with similar visual quality. 

- MSDN Direct2D page
In other words, on DirectX 9.1 hardware there is a high-performance hardware rasterizer available and on lesser hardware there is still a high-performance software rasterizer available. Now, for DirectX 10 and above, it's simple: Direct2D will be chosen. But for DirectX9-class hardware, there is a choice between two software renderers: GDI+, an old and slow API, or WARP, a speedy, very technically impressive API. Clearly, where possible, FireMonkey should choose to use it, falling back to GDI+ only if nothing else whatsoever is possible. As you've no doubt guessed if you've read this far, it doesn't, and this is what we need to investigate and fix.

The problem lies in TCustomDX10Context.CheckDevice. An edited version of the problematic portion of code is:
if ...{can create a D3D hardware device} then
begin
  FDriverType := D3D10_DRIVER_TYPE_HARDWARE;
end else if
  not TCustomDX9Context.HardwareSupported and
  Succeeded(D3D10CreateDevice1Ex(D3D10_DRIVER_TYPE_WARP, D3D10_CREATE_DEVICE_BGRA_SUPPORT, g_pd3dDevice)) then
begin
  // Switch to software mode
  FDriverType := D3D10_DRIVER_TYPE_WARP;
end;
It's this else statement that is problematic. It basically says to use WARP if it's supported (fine) but only if Direct3D9-class hardware is not supported (not fine.) Almost all computers since about 2005 will support D3D9, and this API is available on Vista and above. The only reason I can think of for this is that TCanvasGpu with D3D9 support is expected to be the fallback here before GDI+. However, as we've seen, not only is that class buggy but it is disabled by default (probably because it's buggy.) This means that anyone with D3D9 hardware (but not D3D10+ hardware), and that includes people running on virtual machines like VMWare Fusion, which only supports D3D9 emulation, will end up using GDI+ when they could be using the much faster WARP.

How can we fix it?


A global switch

Normally the best way to force FireMonkey to choose a particular graphics path is via one of the globals at the top of FMX.Types.pas. There is a potentially suitable one: GlobalUseDX10Software. (Remember you need to set these in your project file before you call Application.Initialize.) It's false by default but if you set it to true, you will get WARP. Unfortunately, this means you will always get WARP when possible, even when hardware DX10 support is available. No matter how good WARP is we should choose the hardware-accelerated option when possible, and so this is a no go.

Edit the FireMonkey source


The second option is to edit the FMX source. To do this, make a local copy of FMX.Context.DX10.pas in your program's source folder. (I do not recommend editing RTL source directly and trying to recompile FMX - leave it alone and make your changes separately. If you add your local file to the project it will be used in preference to the RTL version. Just make sure you document what you've changed for future you.)

Add this local file to your project, and remove the 'not' from the else if statement above. It should look something like this:
end else if {not TCustomDX9Context.HardwareSupported and} ...
Recompile and you should get a Direct2D canvas. If you were using the GDI+ canvas before, you should notice a significant difference.

Manual code


The final - and probably best - option is to add some code to try to create a D3D10 context and check the driver type, and if it returns WARP then turn on the above switch. The following slightly ugly method (it's 1AM...) does the trick; call it before Application.Initialize in the project file. This method depends on  FMX.Types, Winapi.D3D10_1, Winapi.D3D10, and WinAPI.Windows.

Because of the method's dependencies and for code cleanliness, I would suggest putting this in a separate unit from the main project source, and ifdef both the unit being included, and the method being called, out completely if you are not compiling for Windows (the MSWINDOWS constant.) As suggested by a commenter below, it is probably also a good idea to ifdef for your specific Delphi version in case this is fixed in future.

The below code doesn't check for D3D9 hardware support, assuming that if it can create a WARP device that's enough. Feel free to add back in additional checks.
procedure TryUseWARPCanvas;
var
  DX10Library : THandle;
  TestDevice : ID3D10Device1;
begin
  DX10Library := LoadLibrary(Winapi.D3D10_1.D3D10_1_dll);
  if DX10Library = 0 then Exit;

  try
    SaveClearFPUState; // Copy from FMX.Context.DX10
    try
      if GetProcAddress(DX10Library, 'D3D10CreateDevice1') = nil then Exit;

      // If there's no hardware D3D10 support, but there /is/ WARP (software support)
      // force that to be used. Don't bother checking DX9 support, just go for WARP.
      if not Succeeded(D3D10CreateDevice1(nil, D3D10_DRIVER_TYPE_HARDWARE, 0, D3D10_CREATE_DEVICE_BGRA_SUPPORT, D3D10_FEATURE_LEVEL_10_1, D3D10_1_SDK_VERSION, TestDevice)) and
        Succeeded(D3D10CreateDevice1(nil, D3D10_DRIVER_TYPE_WARP, 0, D3D10_CREATE_DEVICE_BGRA_SUPPORT, D3D10_FEATURE_LEVEL_10_1, D3D10_1_SDK_VERSION, TestDevice))
        then begin
          FMX.Types.GlobalUseDX10Software := true;
        end;
    finally
      TestDevice := nil;
      RestoreFPUState; // Copy from FMX.Context.DX10
    end;
  finally
    FreeLibrary(DX10Library);
  end;
end;

Tweaks to this code


You might want to change a few things about this code:
  • Editing the FMX code: to match the manual code, I changed it to remove the DX9 check entirely. It either sees if it can create a D3D10 hardware device, or otherwise tries to create a WARP device. Thanks Remy for the suggestion.
  • Untested useful tweak: Direct2D is still hardware-accelerated on DX9-class hardware. Try changing the feature level to D3D10_FEATURE_LEVEL_9_1 to see if it has this level hardware support on your computer. You will need to change both the manual code test (if you use it) and the FireMonkey code creating the devices in the same area as above to match. I haven't tested this and it's just an idea for further investigation; the current code goes either either with D3D10-hardware or WARP, which I know for sure will work and are good, safe modifications to make. Changing this will always require editing the FMX source.
  • XP support: with Microsoft dropping XP support on April 8, 2014, it's quite possible the next version of Delphi will not need to support XP at all - or at least, will only do so as a legacy option. I would suggest that Embarcadero make some changes requiring the Platform Update be installed as prerequisite for FireMonkey apps, and then using one of the D3D10, D3D9-feature-level, or software WARP Direct2D canvases as the only option on Vista and above, and only using GDI+ on XP. There should never be a case on Vista or Windows 7 where GDI+ is the chosen canvas.

For testing: how to force the selection of a specific class


I stated at the beginning that you should test with each possible canvas type, in order to catch code tht works with one and doesn't with another.  How?
  • To force GDI+, set FMX.Types.GlobalUseDirect2D to false.
  • To force Direct2D (using the WARP software rasterizer even with hardware support - so only for testing) set FMX.Types.GlobalUseDX10Software to true.
  • To force the GPU canvas (unnecessary for testing, since it's off by default) set FMX.Types.GlobalUseGPUCanvas to true

How to check what class you are actually using


This is fairly simple. Find a valid normal canvas (such as Form.Canvas) and check its ClassName. It will be one of the above classes.

Summary


  • FireMonkey has several underlying graphics classes depending on the platform and, on Windows, on the capabilities of the platform
  • You need to test each one, because code that works on one can fail on another
  • On Windows, if you (or a user) have D3D9 hardware (but not D3D10 or higher hardware) FireMonkey will use GDI+ to render where it probably shouldn't, which will make your program noticeably slower when run on (a) D3D9-class hardware, or (b) in a virtual machine like VMWare Fusion. It should use Direct2D's software rasterizer instead. Fix this with one of the three ways above; I recommend with the sample code I showed above.