C # / WPF + Pascal + Assembler: how I restored my first game
I rumbled once in my source code of school times, and found the following there:
A game on QBasic about a spacecraft shooting asteroids. Creepy code under dos, but sprites are animated in 3ds max.
Pascal / Assembler graphic library with good speed
Licensed TMT Pascal compiler that can build Win32 code
Do not disappear the same good! Next is the story of all this, a bit of nostalgia, and the details of the implementation of the “modern” version of the game using old sprites and graphics code.
A bit of history
Basic
I first encountered programming at the school where we were taught Logo , then Basic, and then Pascal.
It was on Basic that an interest in development came to me, and of course, I wanted to write my own game! A screenshot of it is posted at the beginning of the post. 640x480, 256 colors, all sprites are animated (rotate in pseudo-3d), sound. The Future library is used (you can still google on qbasic future). The source code is preserved - 1552 lines, 19 uses of the GOTO operator. The game was called Lander, by analogy with the classic game, where you need to land a spaceship on the planet. But it’s boring to land a ship, you want to shoot and explode, so before landing you have to break through the asteroid belt with two types of weapons on board.
')
Sprites drew himself in 3DS Max (asteroids are spheres with Fractal Noise, the rest are combinations of simple shapes, explosions through Combustion). Unfortunately, the original max files are somehow lost, but the rendered sprites are preserved.
Pascal
The next step was Pascal and Assembler embedded in it. At school, we were engaged in 386 machines, and there was felt the full power of micro-optimizations in assembly inserts. Sprite output via REP MOWSW worked much faster than Pascal cycles. Code alignment, shift multiplication, maximum work in registers and minimum in memory.
Protected Mode
All of this was terribly interesting and fun, I wrote some demos, I studied Ralf Brown's Interrupt List , experimented with SVGA graphics modes, suffered with bank switching .
And then the computer science teacher (thanks to him very much), who saw all these entertainments, introduced me to his friend, who worked in the PC assembly department in a large network of computer shops in the city. He needed software under DOS with a graphical interface, preparing the hard disk of the assembled computers in a certain way. A real job as a programmer! The first task was to make the window graphics with buttons, text fields and so on. Surely such solutions already existed, but I did not even think about it and was eager to write my own bicycle.
First of all, I improved the existing module for drawing primitives, sprite output and text. All on assembly inserts. Then, having a little experience picking with Visual Basic 6 under Windows, I implemented windows and controls on Pascal in the same way, and after some time presented the result:
Everything works, windows are dragged, controls respond to MouseOver. Instead of the Windows approach with drawing dirty regions, it went ahead and redrawed everything - it worked fast enough thanks to the assembler.
In response, I heard that 320x200 is not good, and you need to make all the elements look like in the new Windows XP at the time. There are problems with large resolutions in real mode , since it is possible to address linearly no more than 64 kilobytes, to output pictures with higher resolution, you need to switch memory banks , and in general there is little memory (the notorious 640 kilobytes). Therefore, the Borland compiler has been replaced by TMT Pascal , which can handle 32 bits out of the box and protected mode via dos4gw. This solved the problems with memory and graphics, the interface was redrawn, the business logic was run and the project was completed. I do not go into details, since this is already a deviation from the topic.
Our days
Sorting backups, I came across my old code. Took DOSBox, started, brushed away a tear. After many years, C # again wanted to feel "closer to the gland." So I drew a plan - to take an assembler code to draw graphics in memory, then output the result in WPF. TMT Pascal knows how to build Win32 dll, it took only minor changes (throw out too much, add stdcall to signatures).
For example, this is the code for outputting a sprite with transparency (pixels of the color TransparentColor are not displayed):
Without a glass you will not understand, original comments
Procedure tPut32 conv arg_stdcall (X,Y,TransparentColor:DWord;Arr:Pointer);Assembler; {Transparent PUT} Var IMSX, IMSY :DWord; Asm Cmp Arr, 0 Je @ExitSub {Check ON-SCREEN POS} Mov Eax, ScreenSY; Mov Ebx, ScreenSX Cmp Y, Eax; Jl @PUT1; Jmp @ExitSub; @PUT1: Cmp X, Ebx; Jl @PUT2; Jmp @ExitSub; @PUT2: {--------} Mov Edi, LFBMem {Set Destination Loct} {Get Sizes} Mov Esi, Arr LodsD; Mov IMSX, Eax LodsD; Mov IMSY, Eax Add Esi, SizeOfSprite-8 {Check ON-SCREEN POS} Mov Eax, IMSY; Neg Eax; Cmp Eax, Y; Jl @PUT3; Jmp @ExitSub; @PUT3: Mov Eax, IMSX; Neg Eax; Cmp Eax, X; Jl @PUT4; Jmp @ExitSub; @PUT4: {VERTICAL Clipping} Mov Eax, Y {Clipping Bottom} Add Eax, IMSY Cmp Eax, ScreenSY Jl @SkipClipYB Sub Eax, ScreenSY Cmp Eax, IMSY Jl @DoClipYB Jmp @ExitSub @DoClipYB: Sub IMSY, Eax @SkipClipYB: Cmp Y, -1 {Clipping Top} Jg @SkipClipYT Xor Eax, Eax Sub Eax, Y Cmp Eax, IMSY Jl @DoClipYT Jmp @ExitSub @DoClipYT: Sub IMSY, Eax Add Y, Eax Mov Ebx, IMSX Mul Ebx Shl Eax, 2 {<>} Add Esi, Eax @SkipClipYT: {End Clipping} {Calculate Destination MemLocation} Mov Eax, Y; Mov Ebx, ScreenSX; Mul Ebx Add Eax, X Shl Eax, 2 {<>} Add Edi, Eax Mov Ecx, IMSY {Size Y} Mov Ebx, IMSX {Size X} Mov Edx, ScreenSX Sub Edx, Ebx {HORIZ.CLIPPING} Push Edx Xor Eax, Eax {RIGHT} Sub Edx, X Cmp Edx, 0 Jge @NoClip1 {IF EDX>=0 THEN JUMP} Mov Eax, Edx; Neg Eax; Sub Ebx, Eax @NoClip1: Pop Edx {LEFT} Cmp X, 0 Jge @NoClip2 Sub Edi, X; Sub Esi, X // \ Sub Edi, X; Sub Esi, X // \ Sub Edi, X; Sub Esi, X //32 bit!!! Sub Edi, X; Sub Esi, X // / Sub Eax, X; Sub Ebx, Eax @NoClip2: {bitshifts} Shl Eax, 2 {<>} Shl Edx, 2 {<>} ALIGN 4 @PutLn: {DRAW!!!!!} Push Ecx; Push Eax; Mov Ecx, Ebx ALIGN 4 @PutDot: LodsD; Cmp Eax, TransparentColor //Test Al, Al Je @NextDot {if Al==0} StosD; Sub Edi, 4 {<>} @NextDot: Add Edi, 4 {<>} Dec Ecx; Jnz @PutDot {Looping is SLOW} Pop Eax; Add Esi, Eax Add Edi, Edx; Add Edi, Eax Pop Ecx Dec Ecx; Jnz @PutLn {Looping is SLOW} @ExitSub: End;
Memory for sprites is allocated and released on the unmanaged side, the same can be done through Marshal.AllocHGlobal. The sprite is the following structure (ha, the source tag on the habr does not support Pascal - we write Delphi):
Type TSprite = Packedrecord W : DWord; H : DWord; Bpp : DWord; RESERVED : Array[0..6] of DWORD; End;
So, now we can draw the “scene” in the frame buffer. Here I was waiting for plugging with performance. FPS rendering of several hundred sprites in memory was measured in thousands, but to quickly display the result on the Windows window was not so easy. I tried WriteableBitmap, I tried DirectX (via SlimDX), the fastest was through InteropBitmap: Sprite.GetOrUpdateBitmapSource
As you can see, dark magic with FileMapping is called only once, and then we have a direct pointer to the piece of memory that is displayed on the window. You can update it from any stream, in the UI stream you only need to call InteropBitmap.Invalidate ().
The way from the well-known Lincoln6EchoWPF post , WinForms: draw a Bitmap with> 15000 FPS, in fact, produces only 120 fps, if you expand the window to full screen on a full-hd monitor. InteropBitmap in the same conditions gives ~ 800 fps. The game itself on the same machine (core i5) in an expanded window gives about 300 fps, if you remove the sync by CompositionTarget.Rendering.
To avoid screen tearing, unnecessary load on the processor, and to attach to the standard 60 frames per second in WPF, use the CompositionTarget.Rendering event. Rendering takes place in a background thread, so as not to load the main stream and let WPF do its job with GameViewModel.RunGameLoop () .
Game information (health, weapons, glasses) is easily and pleasantly displayed on top of the game picture using WPF: MainWindow.xaml . In the screenshot you can also notice the additive imposition of explosions, implemented using MMX ( PADDUSB instruction)
All game logic is made in C #. He left only shooting at the asteroids, from a horizontal one he converted into a vertical scroller. SlimDX is used only for sound.
Total
The game, as such, did not complete it - interest was lost, trivial tasks remained, and who would play it. It was nice to breathe new life into old crafts. “Closer to the hardware” - the whole rendering does not depend on any frameworks, is performed in a separate thread, rests mainly on the speed of working with memory (from the profiler: 40% of the render time is spent on cleaning the framebuffer and 40% on copying it into InteropBitmap) .