📜 ⬆️ ⬇️

Using Direct3D with High-Level VCL / LCL Component Libraries

This publication is addressed to computer graphics beginners who want to use the Microsoft DirectX graphics library. Immediately make a reservation:
- the topic touched, for sure, also applies to OpenGL, but I didn’t check it by experience (creating applications for OpenGL), so I only mention Direct3D in the title;
- the code examples cited here relate to Delphi / FreePascal languages, but the listed “recipes” are largely universal within the target OS (Windows) - they can be applied to any programming language and, with a high probability, to any high-level library of components besides VCL (Delphi) and LCL (Lazarus);
- this publication does not affect the topic of creating a frame application Direct3D and methods of working with graphics libraries DirectX and OpenGL; 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, it is usually recommended to use pure Win32 API to build a learning (and even less working) application framework ... 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 pure Win32 API, the cycle of processing incoming window messages has to be written “manually”, and usually it looks 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 infinite rendering cycle , in which the next frame almost always starts drawing immediately after the previous one, and any animation will be displayed on the screen correctly and smoothly (if there is enough graphics performance).
')
However, high-level component libraries, such as VCL and LCL, do not require the programmer to implement such a message processing loop. In their depths 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 the entire binding code of these libraries? It is this question that I intend to further illuminate to the best of my own understanding.

Exceptions about masking exceptions


I was surprised when I couldn’t normally run a project compiled into Lazarus using Direct3D, consistently getting exceptions nodding to floating-point calculations when starting the program. Having spent some time studying the problem, I did not find direct information on this problem on the Internet, but I noticed that if I compile a project in Delphi for a 64-bit architecture, I’m getting a very similar error. Examining the contents of the Debug mode windows in Delphi showed that for the FPU processor extension, the MXCSR exception masking register has different values ​​in all the cases considered. Even after that, nothing standing was also possible to google, except for the mention that the OpenGL module from the standard Delphi delivery contains in the “initialization” section a line that sets concealment exceptions for all possible cases.

Masking exceptions FPU does not relate to the topic of this publication, so I will not strongly focus on it. I will give only the simplest example: when multiplying very large floating-point numbers leads to overflow, in this case one of two things happens: the result of multiplication becomes equal to INFINITY (or -INFINITY), if masking of the corresponding exception is on; or the processor generates an exception "floating point overflow" (which must be processed by the program in the "try except" block) if the masking of the corresponding exception is disabled.

As a result, having tried to set the exception masking in my projects as it is done in the standard OpenGL module, I ensured that my Direct3D applications worked in both Lazarus and Delphi (including the 64-bit platform) without problems.

Unfortunately, I could not find in MSDN or other sources (maybe I was looking bad?) Indications on what to do exactly this way and that, 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 the masking of exceptions will have certain side effects that must be taken into account. For example, division by zero becomes “possible” (people have encountered such a problem, for example, here ), therefore, when performing floating-point calculations, it is necessary to check intermediate results.

However, if you want to get exceptional situations in floating-point calculations, as you’ve been used to, then nothing prevents you from using a structure like this in the right places:

 var mask: TArithmeticExceptionMask; begin mask := SetExceptionMask([]); //   (   )   try //       finally SetExceptionMask(mask); //       end; end; 

At this point I’m rounding up with the question of the disguise of exceptions.

Another digression - why do we need it?


There are many goals to create 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 you 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 immediately, and, if necessary, edit something on the fly using the “sane” user interface with menu lines, modal dialogs, etc. etc.

I have prepared a relatively primitive program for this publication, which displays a single triangle in the main window (using API DirectX 11), and at the same time allows you to edit and apply the vertex and pixel shaders used in drawing the scene. To do this, it was necessary to place the necessary set of components on the main application form - a multi-line input field and a button. Immediately I warn you - the program is only demonstration (for this publication), so you should not expect anything special from it. Reference to the source code is provided at the end of the text of this publication.

This is where the digressions end, and I turn to the main topic.

Trivial method - event TForm.OnPaint, Windows function. InvalidateRect ()


Programmers who are familiar not only with high-level component libraries, but also with pure Win32 API have probably already put together a simple scheme: “You need to draw a Direct3D scene in a form (or other component) event handler, called OnPaint, and there, draw, call the InvalidateRect () function from the Win32 API to provoke the system to send a new WM_PAINT message that will cause the OnPaint handler to be called again, and so we will go around in an endless loop of drawing, not forgetting to react to the rest of the 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 - by downloading them, the reader can find the “01 - OnPaint + InvalidateRect” subdirectory, compile and run the programs and make sure that the example is not very correct).

Problem 1 : when compiling an application in Delphi and then running, the Direct3D scene is rendered as expected, but the controls of the user interface do not normally appear. Until you change the location or size of the program window, neither the captions nor the contents of the multiline edit field, nor the status bar, nor the button want to be displayed normally ... Well, let’s say, the multiline edit field is more or less normally redrawn when we start scrolling and editing it content, but overall the result is unsatisfactory. And if the program opens dialogs (or at least a primitive MessageBox) during work, they either don’t want to close normally or appear on the screen (MessageBox can be closed blindly with the space button, but the dialog box inherited from TForm can be closed I already can not get it). To demonstrate this problem, I added the items “Extras -> About (MessageBox)” and “Extras -> About About (TForm)” to the sample program's main menu.

Problem 2 : when compiling an application in Lazarus and then starting it, in addition to the problems described above (as if they are not enough), the inability to close the program is added - it does not respond to either the standard close button in the header (“X”) or the menu item "Exit" ... In order for the program to complete itself, without the "help" of the task manager or the "Ctrl + F2" combination in the IDE, you must minimize the program to the taskbar (I wonder why?) After clicking on the close button of the window.

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

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

But to solve the first problem so easily, alas, will not work.

Conclusion: The method described in this subtitle disrupts the normal operation of the Windows window message queue, making it difficult for a number of window messages to be processed on time, and this is especially evident when using a high-level library of components (at least this refers to VCL and LCL in their versions at the time of writing) publications).

Note: in MSDN, you can find the description of the GetMessage function, which mentions 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 all versions of the OS, then at least in the currently 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 we would like, especially if the application It uses a high-level library of components and therefore cannot rely on the priority specified in MSDN.

At this point, one could proceed to the next method of organizing an infinite drawing cycle, but I will make another short digression, into one small paragraph.

The VCL and LCL libraries offer the Invalidate () method to classes in classes inherited from TWinControl. In the VCL, calling it comes down to calling the above-mentioned InvalidateRect () function of a 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 about the same result (a new window drawing will be performed), but some of the nuances are different. Therefore, in order not to focus attention on the nuances, I immediately suggested accessing the InvalidateRect () function from the Win32 API.

A better way is to use the Application.OnIdle event.


Since the previous method fails, because it disrupts the normal operation of the window message queue of Windows, it is logical to try to make the application window drawn strictly after processing all other window applications. At first glance (at least, if you do not peer into the interior of the libraries in detail) this task may seem impossible without modifying the window message processing cycle hidden in the depths of the VCL and LCL libraries, but in reality this is not the case.

The Application object has an OnIdle event, which is called whenever a new window message is detected, and moreover, the event handler can report that it wants to process this event repeatedly (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 your development environment).

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

The result is a code 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 source code attached to the publication, you can find the “02 - OnPaint + OnApplicationIdle” subdirectory 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.

To the above, I want to add one more thing: 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 is despite the fact that there is nothing to draw a program and . If you want your program to yield CPU resources to other applications in similar cases, and also not cause glitches in open modal windows (I only saw this 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 in the event of handling the Application.OnIdle event, it is impossible to achieve a perfect infinite rendering cycle. For example, when the window’s main menu is open, the Application.OnIdle event will not be triggered while navigating through it, and, accordingly, the animation of the Direct3D scene “stops”. 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 the TTimer object on the form, set it to fire every 50 milliseconds, and call the same InvalidateRect () function in its event handler - then you can hope that when navigating through the main menu and when working with modal dialogs, the loop Rendering will continue its work, but in these moments it will not be possible to adequately evaluate the FPS and the rendering performance of the 3D scene as a whole. However, it is unlikely to interest the user in those moments when he opens the main menu and dialog boxes, so I do not focus on the continuity of the infinite rendering cycle - the main thing is that it was and worked in those moments when the user's attention is focused on the window with Direct3D - with the stage, and the rest is not so important and is left to the reader - anyone can realize the moment with TTimer on their own and make sure that it works in a quite expected way.

Drawing the 3D scene in a separate control


When part of the program window is set aside for drawing the Direct3D scene, and the other is under the control of the user interface, it will not be entirely correct to allocate the video memory for the entire program window.

It would be more logical to create a socket (or another control), which, if necessary, will change its size along with the program window (it is convenient to use the Align property to automatically adjust the size of the control) and eliminate the "shamanism" with the transformation matrices when drawing the Direct3D scene.

Unfortunately, I did not manage to find standard low-functional TPanel controls that would have a “public” OnPaint event handler, so I had to implement a TCustomControl inheritor component (from other classes as well) and overload it with the 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 themes and double buffering when drawing windows


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

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

Libraries of the VCL and LCL components can use double buffering when drawing windows. In sample projects, you can see a line in the FormCreate () handler, which disables double buffering.
Why is it important to disable double buffering when we draw a window using Direct3D? Because the windows and controls are drawn by these means of GDI. And since Direct3D in the example programs performs output to the window directly, bypassing any “custom” double buffering, it turns out that when the double buffering is enabled, it will receive just a black rectangle in its back buffers - we don’t drew! Thus, with each drawing, with double buffering enabled, the following scenario will occur: the library creates a 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 components ... and after the execution of the OnPaint () handler, the component library draws its empty buffer (black rectangle) on top of the image that we obtained using Direct3D. , , ( «») . -, FormCreate() .

, — - , ?
— DoubleBuffered ( ) FALSE , , Lazarus LCL, , Windows ( -).
, win32callback.inc LCL, WindowProc(), :
 useDoubleBuffer := (ControlDC = 0) and (lWinControl.DoubleBuffered or ThemeServices.ThemesEnabled); 

— DoubleBuffered Windows.

VCL Delphi, , VCL:
 if not FDoubleBuffered or (Message.DC <> 0) then 


, .
— .


- :
github.com/yizraor/PubDX_VCL_LCL
spoiler
( , , )

Source: https://habr.com/ru/post/256871/


All Articles