Using Direct3D with High-Level VCL / LCL Component Libraries

This publication is addressed to beginners in the field of computer graphics programming who wish to use the Microsoft DirectX graphics library.
I’ll make a reservation right away: - the topic mentioned probably also applies to OpenGL, but I have not tested it empirically (by creating applications for OpenGL), therefore I only mention Direct3D in the header;
- the code examples given here are related to Delphi / FreePascal languages, but the listed “recipes” are by and large universal within the target OS (Windows) - they can be applied to any programming language and, with high probability, to any high-level library of components besides VCL (Delphi) and LCL (Lazarus);
- This publication does not address the topic of creating a Direct3D wire-frame application and methods for working with DirectX and OpenGL graphics libraries; all these things are well covered in other sources, and I have practically nothing to add to this.

So, closer to the topic. When developing applications with three-dimensional graphics to build the framework of an educational (and even more so working) application, it is usually recommended to use a pure Win32 API ... But if you really want to use the advantages of high-level component libraries in your applications, then welcome to cat.

Introduction to the problem


When using the pure Win32 API, the processing cycle for incoming window messages has to be written “manually”, and usually it looks something like this:

repeat
  if ( PeekMessage(msg, 0, 0, 0, PM_REMOVE) ) then
  // если есть какое-то сообщение в очереди - получаем его и обрабатываем
  begin
    TranslateMessage(msg);
    DispatchMessage(msg);
  end
  // иначе немедленно выполняем очередную отрисовку 3D-сцены
  else
    RenderScene();
  // и вот так повторять до завершения работы приложения
until ( msg.message = WM_QUIT );

This code allows you to implement an endless rendering cycle , in which the next frame almost always begins to be drawn immediately after the previous one, and any animation will be displayed on the screen correctly and smoothly (if the graphics performance is enough).

However, high-level component libraries, such as VCL and LCL, do not require the programmer to implement such a message processing cycle. In their bowels there is already, in one form or another, the implementation of such a cycle, so the question arises: how to implement an infinite rendering cycle without violating the principles of working with these libraries, and at the same time ensure the correct operation of all the binding code of these libraries? It is this question that I intend to illuminate in the future to the best of my own understanding.

Exception masking derogation


I was surprised when I couldn’t normally launch a project compiled in Lazarus using Direct3D, stably getting exceptions when starting the program that “nod” to floating point calculations. Having spent some time studying the problem, I did not find direct information on the Internet about this problem, but noticed that if I compile the project in Delphi for 64-bit architecture, I get an error that is very similar in essence. Examination of the contents of the Debug-mode windows in Delphi showed that for the FPU extension of the processor, the MXCSR exception mask register has different meanings in all cases considered. Even after that, it was not possible to google anything worthwhile, except for mentioning that the OpenGL module from the standard Delphi package contains a line in the “initialization” section,

The masking of FPU exceptions is not relevant to the topic of this publication, so I will not focus much on it. I will give only the simplest example: when the multiplication of very large floating-point numbers leads to overflow, then in this case one of two things occurs: the result of the multiplication becomes INFINITY (or -INFINITY) if masking of the corresponding exception is enabled; or the processor generates a floating point overflow exception (which must be processed by the program in the try except block) if the masking of the corresponding exception is disabled.

As a result, after trying to set exception masking in my projects as it was done in the standard OpenGL module, I made sure that my Direct3D applications work in both Lazarus and Delphi (including the 64-bit platform) without problems.

Unfortunately, I couldn’t find in MSDN or other sources (maybe I’ve looked badly?) For instructions on how to do it this way and not otherwise, but nevertheless, I recommend readers to write the following code in their Direct3D projects:

uses Math;
...
INITIALIZATION
  Math.SetExceptionMask([exInvalidOp..exPrecision]);
END.

It should be noted that masking exceptions will have certain side effects that must be taken into account. For example, dividing by zero becomes "possible" (people have encountered such a problem, for example, here ), therefore, when performing floating point calculations, it is necessary to check the intermediate results.

However, if you want to receive exceptional situations when performing floating point calculations, as you are accustomed to before, then nothing prevents you from using the design of something like this in the right places:

var
  mask: TArithmeticExceptionMask;
begin
  mask := SetExceptionMask([]);  // отключаем всю (или как Вам нужно) маскировку исключений
  try
    // все необходимые вычисления с плавающей запятой
  finally
    SetExceptionMask(mask);  // возвращаем обратно предыдущие флаги маскировки исключений
  end;
end;

On this, I end with the question of masking exceptions.

Another digression - why do we need this?


There can be many goals for creating Direct3D applications using high-level component libraries. For example, debugging some moments, such as shaders and effects. Or maybe you are creating your own 3D engine and need a definition file editor, based on which the engine will load resources and render scenes? In such cases, I would like to be able to see the result right away, and if necessary - edit something on the fly using the “sane” user interface with menu bars, modal dialogs, etc. etc.

For this publication, I have prepared a relatively primitive program that displays a single triangle in the main window (using the DirectX 11 API), and at the same time allows editing and applying vertex and pixel shaders used in rendering the scene. To do this, it was necessary to place the necessary set of components on the main form of the application - a multi-line input field and a button. I warn you right away - the program is exclusively demo (for this publication), so you should not expect anything special from it. A link to the source code is provided at the end of the text of this publication.

This is where the digression ends, and I move on to the main topic.

The trivial way is the TForm.OnPaint event, the Windows.InvalidateRect () function


Programmers who are familiar not only with high-level component libraries, but also with a clean Win32 API, have probably already put together a simple scheme in their head: “you need to draw a Direct3D scene in a form event handler (or another component) called OnPaint, and there, at the end rendering, call the InvalidateRect () function from the Win32 API to provoke the system to send a new WM_PAINT message, which will cause the OnPaint handler to be called again, and so on we will go around in an endless rendering cycle, not forgetting to react to the rest about tional message. "

In general, that's right.

Here is a sample code plan for the OnPaint handler:

procedure TFormMain.FormPaint(Sender: TObject);
begin
  // отрисовка Direct3D-сцены
  // ...
  // вывод результатов на экран с помощью интерфейса IDXGISwapChain
  pSwapChain.Present ( 0, 0 );
  // генерация следующего события WM_PAINT для бесконечного цикла отрисовки
  InvalidateRect ( Self.Handle, nil, FALSE );
end;

But, as they say, "it was smooth on paper."

Let's see what happens (I remind you that at the end of the text there will be a link to the source codes - after downloading them, the reader can find the subdirectory “01 - OnPaint + InvalidateRect”, compile and run the programs and make sure that the example is not working correctly).

Problem 1: when compiling the application in Delphi and subsequent launch, the Direct3D scene is drawn as expected, but the user interface controls do not want to be displayed normally. Until you change the location or size of the program window, neither the labels, nor the contents of the multi-line edit field, nor the status bar, nor the button want to appear normally ... Well, let’s say the multi-line edit field is more or less normally redrawn when we start scrolling and editing it contents, but overall unsatisfactory result. And if the program in the process of opening the dialog boxes (or at least the primitive MessageBox), they either don’t want to close normally or display on the screen (you can close the MessageBox blindly with the space bar, but close the dialog window inherited from TForm at I can’t do it anymore).

Problem 2 : when compiling the application in Lazarus and subsequent launch, in addition to the problems described above (as if they were not enough), the inability to exit the program is added - it does not respond to either the standard close button in the header (“X”) or the menu item “Exit” ... For the program to terminate itself, without the “help” of the task manager or the combination of “Ctrl + F2” in the IDE, you need to minimize the program to the taskbar (I wonder why?) After clicking on the close button on the window.

To get rid of the last problem is actually very simple, you just need to add an additional condition before calling the InvalidateRect () function, something like this:

  if ( not ( Application.Terminated ) ) then
    InvalidateRect(Self.Handle, nil, FALSE);

But here it is so easy to solve the first problem, alas, it will not work.

Conclusion: the method described in this subheading disrupts the normal operation of the Windows window message queue, preventing a number of window messages from being processed on time, and this is especially evident when using a high-level component library (at least this applies to VCL and LCL in their versions at the time of writing publication).

Note: in MSDN you can find a description of the GetMessage function , where it is mentioned that the WM_PAINT message has a low priority compared to other window messages (except WM_TIMER - its priority is even lower), and is processed after all other window messages.

Total: the fact, as they say, is obvious. If not in all OS versions, then at least in the now popular Windows 7 operating system (in which I ran all the sample programs attached to the publication), the situation with the priority in processing the WM_PAINT message will be somewhat more complicated than I would like, especially if the application uses a high-level component library, and therefore you cannot rely on the priority specified in MSDN.

With this, we could move on to the next way of organizing an infinite rendering cycle, but I will make another short digression, one small paragraph.

The VCL and LCL libraries offer the programmer, in classes inherited from TWinControl, the Invalidate () method. In the VCL library, its call is reduced to calling the above-mentioned InvalidateRect () function of the pure Win32 API, but in general the behavior of this method depends on the implementation in a particular library. So, in LCL, this method calls another Win32 API function called RedrawWindow () - this function gives approximately the same result (a new window rendering will be performed), but some nuances differ. Therefore, in order not to focus on the nuances, I immediately proposed to turn to the InvalidateRect () function from the Win32 API.

The method is more successful - we use the Application.OnIdle event


Since the previous method is unsuccessful, since it disrupts the normal operation of the Windows window message queue, it is logical to try to make the rendering of the application window strictly after processing all other window applications. At first glance (at least if you don’t look at the insides of the libraries in detail), this task may seem impossible without modifying the window message processing cycle hidden in the bowels of the VCL and LCL libraries, but in reality it is not.

The Application object has an OnIdle event, which is raised every time when it is discovered that there are no new window messages, and moreover, the handler of this event may inform that it wants to process this event again (in a loop) until finally new ones appear messages. After the new messages are processed, the Application.OnIdle event handler will be called again ... And so on until the application terminates. In general, the Application.OnIdle event is quite suitable for organizing an infinite rendering cycle, albeit with its own nuances (for more detailed information on this event, I advise you to refer to the help in the development environment you are using).

Now we can remove the call to the InvalidateRect () API function from the OnPaint handler and transfer it to the Application.OnIdle event handler.

The result is a code that looks something like this:

procedure TFormMain.FormCreate(Sender: TObject);
begin
  Application.OnIdle := OnApplicationIdle;
  // прочий код инициализации
  // ...
end;
procedure TFormMain.FormPaint(Sender: TObject);
begin
  // отрисовка Direct3D-сцены
  // ...
  // вывод результатов на экран с помощью интерфейса IDXGISwapChain
  pSwapChain.Present ( 0, 0 );
  // генерации следующего события WM_PAINT здесь больше нет — она перенесена в OnApplicationIdle()
end;
procedure TFormMain.OnApplicationIdle(Sender: TObject; var Done: Boolean);
begin
  if ( Application.Terminated )  // выполняется завершение работы приложения
     or ( { другие условия, при которых не нужно продолжать бесконечный цикл отрисовки } ) then
  begin
    // перерисовка не нужна, завершаем цикл обработки OnIdle()
    Done := TRUE;
    Exit;
  end;
  // будем обрабатывать OnIdle() повторно для обеспечения бесконечного цикла отрисовки
  Done := FALSE;
  // обеспечить сообщение WM_PAINT для последующей отрисовки
  InvalidateRect ( Self.Handle, nil, FALSE );
end;

In the sources attached to the publication, you can find the subdirectory “02 - OnPaint + OnApplicationIdle” and make sure that the program works much better by updating the contents of all controls in a timely manner and correctly displaying all modal dialog boxes.

I want to add one more thing to the above: if you minimize the program window to the taskbar and open the task manager, you can see that the program “eats” at least one processor core completely, and this despite the fact that there is nothing and no need to draw the program by and large . If you want your program to assign CPU resources to other applications in such cases, and also not cause glitches in open modal windows (I saw this only in Lazarus), then you can modify the Application.OnIdle event handler in the following way:

procedure TFormMain.OnApplicationIdle(Sender: TObject; var Done: Boolean);
begin
  if ( Application.Terminated )  // выполняется завершение работы приложения
     or ( Application.ModalLevel > 0 )  // открыты модальные окна
     or ( Self.WindowState = wsMinimized )  // окно программы свернуто
     or ( { другие условия, при которых не нужно продолжать бесконечный цикл отрисовки } ) then
  begin
    // перерисовка не нужна, завершаем цикл обработки OnIdle()
    Done := TRUE;
    Exit;
  end;
  // будем обрабатывать OnIdle() повторно
  Done := FALSE;
  // обеспечить сообщение WM_PAINT для последующей отрисовки
  InvalidateRect ( Self.Handle, nil, FALSE );
end;

However, even if the Application.OnIdle event is handled, it is not possible to achieve a perfect endless rendering cycle. For example, when the main menu of the window is open, the Application.OnIdle event will not be called during navigation through it, and, accordingly, the animation of the Direct3D scene will “stop”. The same thing will happen if the program opens a modal dialog or MessageBox window.

Of course, such problems can also be overcome. For example, put a TTimer object on the form, set it to fire every 50 milliseconds, and call the same InvalidateRect () function in its event handler - then we can hope that when navigating the main menu and working with modal dialogs, the loop rendering will continue its work, but at these moments it will no longer be possible to adequately evaluate the FPS and rendering performance of the 3D scene as a whole. However, it is unlikely that the user will be interested in those moments when he opens the main menu and dialog boxes, so I do not focus on continuity an infinite rendering cycle - the main thing is that it is up and running in those moments when the user's attention is focused on the window with the Direct3D scene, and the rest is not so important and is left to the reader - those who wish can realize the moment with TTimer on their own and make sure that it works in the expected way.

Drawing a 3D scene in a separate control


When a part of the program window is reserved for rendering Direct3D scenes, and the other for user interface controls, it will not be entirely correct to allocate video memory for the entire program window.

It would be more logical to create a panel (or other control), which, if necessary, will change its size together with the program window (it is convenient to use the Align property to automatically adjust the control size) and relieve “shamanism” with transformation matrices when rendering Direct3D scenes.

Unfortunately, I could not find standard low-functional controls such as TPanel that would have a “public” OnPaint event handler, so I had to implement the TCustomControl successor component (you can also use other classes) and overload its Paint () method.

Such an implementation is extremely simple, and the source codes attached to the publication contain a similar example in the subdirectory “03 - TCustomControl descendant”.

Using Windows skins and double buffering when rendering windows


In the source codes attached to the publication, Delphi projects include a manifest in their settings to support Windows themes, and therefore the programs compiled in Delphi have a completely modern look at runtime.

As for Lazarus projects, setting up such a manifest for them is disabled, and this, unfortunately, is not an accident - I set it intentionally, and now I will explain why.

VCL and LCL component libraries can use double buffering when rendering windows. In the sample projects, you can see a line in the FormCreate () handler that disables double buffering.
Why is it important to disable double buffering when we draw a window using Direct3D? Because the drawing of windows and controls is performed by these libraries using GDI tools. And since Direct3D in the sample programs directly outputs to the window, bypassing any “custom” double buffering, it turns out that the component library, when double buffering is enabled, will receive just a black rectangle in its on-screen buffers - we don’t have anything in the off-screen buffer painted! Thus, with each drawing, with double buffering turned on, the following scenario will happen: the library creates an off-screen buffer, clears it in black, then our OnPaint () handler draws the scene using Direct3D and displays it on the screen, bypassing the screen buffer created by the component library ... and after executing the OnPaint () handler, the component library draws its empty buffer (black rectangle) on top of the picture that we received using Direct3D. As a result, by enabling double buffering, we will have a very noticeable (up to a black window with rare "flashes") flickering program window. This can be checked in the sample programs by changing the corresponding line in the FormCreate () handler in any of the projects.

Probably, you’ve already thought about it - if double-buffering is disabled in all example programs, what problems can there be?
I tell you - even when the DoubleBuffered property (or target control) is set to FALSE , programs created in Lazarus using the LCL component library will still use double buffering when the program has themes for Windows (using the manifest file mentioned above) .
The proof of this is very simple, in the LCL library's win32callback.inc header file , in the code of the WindowProc () function, there is a line:
useDoubleBuffer := (ControlDC = 0) and (lWinControl.DoubleBuffered or ThemeServices.ThemesEnabled);

pay attention to the last part of the condition - it eloquently explains the uselessness of disabling the DoubleBuffered property with available Windows themes.

As for the VCL library in Delphi, it can do without double buffering when using themes, here is a similar line from the bowels of VCL:
if not FDoubleBuffered or (Message.DC <> 0) then


That's all for now, thank you for your attention.
Useful additions to the material and constructive criticism are waiting in the comments.

Source code


The sample programs for this publication are available at the following link:
github.com/yizraor/PubDX_VCL_LCL
spoiler
(I have not used a github before, I hope that it turned out correctly)

Also popular now: