Difference between revisions of "Workshop:Amiga, Pascal, graphics.library and timer.device"

From Freepascal Amiga wiki
Jump to navigation Jump to search
(→‎Heading text: Add content for chapter Pascal and AmigaOS)
(→‎Heading text: Add content for chapter: Here we go)
Line 181: Line 181:
 
So, now it should be clear why our test.pas reads as it does: It will check whether our compiler installation is complete so we can use both the standard library and the Amiga system calls.
 
So, now it should be clear why our test.pas reads as it does: It will check whether our compiler installation is complete so we can use both the standard library and the Amiga system calls.
  
== Heading text ==
+
== Here we go ==
 +
 
 +
What do we actually want to write right now ? Here is a quick description: We want to open a simple, resizable window on the Workbench where we can draw graphics using the graphics library - using accurate timed animation. Of course this should all be implemented in a system-friendly manner e.g. it should immediately respond to user interaction and consume as less computer time and resources as necessary. That sounds easier than it actually is therefor we will implement this step-by-step.
 +
 
 +
We will begin with our main entry-point. We do not want add too much code in there, but the main entry-point is a perfect place to check the presence of required libraries. For our implementation that would be intuition.library that is used for our window and graphics.library that is needed for our drawing commands.
 +
 
 +
First we define two global variables that represent the base address for the libraries:
 +
 
 +
<source lang="pascal">
 +
//* our system libraries addresses */
 +
var
 +
  GfxBase      : PGfxBase      absolute AGraphics.GfxBase;
 +
  IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;
 +
</source>
 +
 
 +
Did you remember that these variables are already defined in our Pascal support units ? That is why we map them to their original variable by using the keyword absolute.
 +
 
 +
(Red: usually you would not have to do this mapping and you can use the variables GfxBase and IntuitionBase from their units directly, but a) we want to stay as close to the original c-source as possible and b) there currently is a tiny incompatibility with the type definition amongst supported platforms. Remember that this source can be compiled for Amiga, AmigaOS, AROS and MorphOS).
 +
 
 +
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.
 +
 
 +
Instead we check in our main entry-point if indeed the libraries were opened successfully and if they match required version. That looks like this:
 +
 
 +
<source lang=pascal>
 +
var
 +
  result : Integer;
 +
begin
 +
  //* as long we did not execute RunEngine() we report a failure */
 +
  result := RETURN_FAIL;
 +
 
 +
  //* we need at least 1.2 graphic.library's drawing functions */
 +
  if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then
 +
  begin
 +
    //* we need at least 1.2 intuition.library for our window */
 +
    if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then
 +
    begin
 +
      //* All libraries needed are available, so let's run... */
 +
      result := RETURN_OK;
 +
//* Closing Intuition library and setting its baseaddress to nil */
 +
      //* is not necessary as Pascal does that automatically for us    */
 +
    end;
 +
    //* Closing Graphics library and setting its baseaddress to nil */
 +
    //* is not necessary as Pascal does that automatically for us  */
 +
  end;
 +
 
 +
  //* Pascal uses System variable ExitCode to report back a value to caller
 +
  ExitCode := result;
 +
end;
 +
</source>
 +
 
 +
As soon as we've made sure that the libraries where opened successfully, we initialize our own data, open the window and jump into the main loop. We will do this in our own function named RunEngine(). We also define a record structure where we store our run-time data, so that we can easily pass along a pointer to all functions involved. This avoids the use of ugly global variables:
 +
 
 +
<source lang=pascal>
 +
type
 +
  PRenderEngineData = ^TRenderEngineData;
 +
  TRenderEngineData =
 +
  record
 +
    window : PWindow;
 +
    run    : boolean;
 +
  end;
 +
 
 +
function RunEngine: integer;
 +
var
 +
  rd        : PRenderEngineData;
 +
  newWindow : TNewWindow;
 +
begin
 +
  //* as long we did not enter our main loop we report an error */
 +
  result := RETURN_ERROR;
 +
 
 +
  (* 
 +
    allocate the memory for our runtime data and initialize it
 +
    with zeros
 +
  *}
 +
  rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));
 +
  if assigned(rd) then
 +
  begin
 +
    //* now let's open our window */
 +
    with newWindow do
 +
    begin
 +
      LeftEdge    :=  0;            TopEdge  := 14;
 +
      Width      := 320;            Height  := 160;
 +
      DetailPen  := UBYTE(not(0));  BlockPen := UBYTE(not(0));
 +
      IDCMPFlags  := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;
 +
      Flags      := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;
 +
      FirstGadget := nil;            CheckMark := nil;
 +
      Title      := 'Gfx Workshop';
 +
      Screen      := nil;
 +
      BitMap      := nil;
 +
      MinWidth    := 96;            MinHeight := 48;
 +
      MaxWidth    := UWORD(not(0));  MaxHeight := UWORD(not(0));
 +
      WType      := WBENCHSCREEN_f;
 +
    end;
 +
 
 +
    rd^.window := OpenWindow(@newWindow);
 +
    if Assigned(rd^.window) then
 +
    begin
 +
      //* the main loop will run as long this is TRUE */
 +
      rd^.run := TRUE;
 +
 
 +
      result := MainLoop(rd);
 +
 
 +
      //* cleanup: close the window */
 +
      CloseWindow(rd^.window);
 +
      rd^.window := nil;
 +
    end;
 +
 
 +
    //* free our runtime data */
 +
    ExecFreeMem(rd, sizeof(TRenderEngineData));
 +
    rd := nil;
 +
  end;
 +
end;
 +
</source>
 +
 
 +
The trained eye would have spotted immediately that we first allocate the memory for our RenderEngineData, initially filling the structure with zero's, and then open the window. This is a simple refresh window, which is why we also request that we want to receive IDCMP_REFRESHWINDOW messages from intuition.library and which allows us to redraw the contents of the window. Because we are going to redraw the window several times per second, using a smartrefresh window (where intuition would take care of redrawing) would be superfluous (red: counterproductive ?)
 +
 
 +
If everything worked out as intended then we jump into our MainLoop():
 +
 
 +
<source lang=pascal>
 +
function  MainLoop(rd: PRenderEngineData): integer;
 +
var
 +
  winport : PMsgPort;
 +
  winsig  : ULONG;
 +
 
 +
  msg    : PMessage;
 +
begin
 +
  //* remember the window port in a local variable for more easy use */
 +
  winport := rd^.window^.UserPort;
 +
 
 +
  //* create our waitmask for the window port */
 +
  winSig := 1 shl winport^.mp_SigBit;
 +
 
 +
  //* our main loop */
 +
  while (rd^.run) do
 +
  begin
 +
    //* let's sleep until a message from our window arrives */
 +
    Wait(winSig);
 +
 
 +
    {*
 +
      our window signaled us, so let's harvest all its messages
 +
      in a loop...
 +
    *}
 +
    while true do
 +
    begin
 +
      msg := GetMsg(winport);
 +
      if not assigned(msg) then break;
 +
 
 +
      //* ...and dispatch and reply each of them */
 +
      DispatchWindowMessage(rd, PIntuiMessage(msg));
 +
      ReplyMsg(msg);
 +
    end;
 +
  end;
 +
  result := RETURN_OK;
 +
end;
 +
</source>
 +
 
 +
We stay inside our main loop as long as rd^.run flag remains TRUE. We want to set this flag to false as soon as the user clicks on the close gadget of our window. Inside the main loop we'll wait until we get a signal from the window which is send by a message, modify that message, and reply to this message(s) using a loop. The modification of the message takes part inside function DispatchWindowMessage() as follows:
 +
 
 +
<source lang=pascal>
 +
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);
 +
begin
 +
  case (msg^.IClass) of
 +
    IDCMP_CLOSEWINDOW:
 +
    begin
 +
      {*
 +
        User pressed the window's close gadget: exit the main loop as
 +
        soon as possible
 +
      *}
 +
      rd^.run := FALSE;
 +
    end;
 +
    IDCMP_REFRESHWINDOW:
 +
    begin
 +
      BeginRefresh(rd^.window);
 +
      EndRefresh(rd^.window, TRUE);
 +
    end;
 +
  end;
 +
end;
 +
</source>
 +
 
 +
Here we react to IDCMP_CLOSEWINDOW by setting our run-flag to false, which will cause us to leave our main loop as soon as all the other messages have been processed.
 +
 
 +
Because we still have nothing to draw, we simply call Beginrefresh()/EndRefresh() on a IDCMP_REFRESHWINDOW, by which we tell intuition that we have successfully redrawn our window.
 +
 
 +
If we compile all the above code (engine1.pas) Then we get an empty, resizable window, which we can close again. Great, isn't it? ;-)
 +
 
 +
fpc engine1.pas
 +
 
 +
[ you should be looking at a picture here ]
 +
 
 
== Heading text ==
 
== Heading text ==
 
== Heading text ==
 
== Heading text ==

Revision as of 18:20, 17 September 2017


Respect the copyright

This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer.

The workshop you read here is a translation into English from the work done by Kai. The original workshop was aimed at the c-programmer and also this part has been rewritten here to address the Pascal programmer instead.

That means that the workshop here contains some changes in comparison to the original work done by Kai. The translation and changes respects and upholds original authors copyright.

Let's start with clarifying something first: Everything read in this lead section is written by me (the translator). That also means that text listed in and after the table of contents is based on the work written by original author.

This should hopefully clear things up with regards of the use of the words "I", "we" and "me".


This document is a translation (from German to English, changed programming language from c to Pascal) of a programming workshop for the Amiga, originally written by Kai Scherrer.

The original author wrote this tutorial for the c programming language as well as introduced the reader to different c-compilers for the Amiga as well as discussed their advantages/disadvantages and/or usage.

Because this document is targeting users that (want to) program using the Pascal language, there are many difference in comparison to the original documentation. As you perhaps might have noticed, these differences begin right from the start including this foreword.


notes with regards to Free Pascal

This documentation is aimed at those using the Free Pascal compiler. This compiler is able to run on a variety of operating systems including Amiga, AmigaOS, AROS and MorphOS (so you can use the compiler natively), but can also be used to cross-compile f.e. from Windows, Mac and/or Linux to target the aforementioned platforms.

Another wicked alternative for compiling single-file projects is using the online compiler. There is even a special version of the online compiler for old browsers that don't quite handle javascript

Note that the original author used vbcc for his workshop and that Free Pascal is able to use the same back-end (vasm/vlink) that is used by vbcc to create executables. In fact this is default when compiling natively on Amiga for example.

Free Pascal uses some defaults that might not always be obvious for most. For example, current API units automatically opens and closes libraries for you when you include such a unit in your project. The auto-opening and closing is something that usually isn't done for most programming languages targeting the Amiga platform.

Another note worth mentioning is the fact that Pascal does not has a dedicated program entry point by the name of main. As such, there is also no main header declaration. But, if you have your roots in c-programming and can't live without main() then this can easily be accommodated, for example:

// c main like entry-point.
function main(argc: Integer; argv: PPChar): integer;
begin
  if EverthingElseWentOk() 
    then result := RETURN_OK 
      else result := RETURN_FAIL;
end;

// This is the Pascal equivalent of main program entry point
begin
  ExitCode := main(ArgC, ArgV);
end.

Note that Pascal uses the identifier ExitCode to return a value to the shell but also realize that ArgC and ArgV can't be used to distinguish between program-startup from shell or WB (red: is that true ?)


Foreword

As a typing exercise i wrote a simple and small Graphics-Engine. Actually "engine" is perhaps a bit exaggerated, but for the sake of simplicity and lack of a better word, my little baby has been written :-)

This gave me the idea to write a small workshop that handles the topic of Amiga Programming. In this workshop i describe the function and development progress of the engine, as well as explain some details about some of the components of AmigaOS.

The engine itself uses double-buffering to display the graphics: drawing operations are performed on a non-visible bitmap and only when a image is completely finished drawing, it is then copied to the visible bitmap of the window in one go. Later in the workshop, I would like to use this technique to display a full-screen image that does not copy the contents of the bitmaps, but uses the bitmaps themselves to display.

The desired frame rate is freely adjustable and is controlled by timer.device. In addition, we will control each animation based on the actual time that past, so that animations can be played at the correct speed even if the computer fails to keep up with the frame rate.

Our engine is designed to operate in a system-friendly and multitasking environment that runs on OS 1.2 and up (red: Free Pascal currently only provide headers that match OS3.x). As of OS 3.0, functionality is used which improves performance for graphics cards. However, in the current version there is no further support for such RTG-systems: Our renderer is therefor limited to 8-bit graphics. Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.

For those there is another nice play-field where you can play and experiment with the functions of graphics.library and bitplanes - and in principle and without much changes, the obtained results can also be incorporated in 'real' demo's, games or programs.

To accomplish this I will introduce some basic functions from graphics.library to develop a simple 2d vector renderer that is even able to reach acceptable performance on stock 68000 systems.

In order to be able to follow this workshop you need a working Pascal compiler. Therefor i will start with a short explanation on the installation and use of Free Pascal.

In addition, you should have at least some rudimentary knowledge of the Pascal programming language. I will not provide too much background information otherwise. Although it would be nice to have everything explained all in one place, on the other hand we do not want to dwell too much into known details. So if you don't understand something from this workshop then don't hesitate to ask. At best you would have me revise he relevant posts or add some digression.

And now for some fun!

A quick view on Pascal compilers

There are quite a few Pascal compilers available for the Amiga. Unfortunately almost none of them are are kept up to date. A notable exception is Free Pascal, which is constantly improving by its developers. Amongst those developers are also a few that keep an eye on Amiga supports. I'll briefly go over a few important compilers here:


UCSD Pascal

More research required.


Amiga Pascal

Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).


AmigaPascal

A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.


HSPascal

This Pascal seem to have appeared around 1990 and produced executables for Amiga and Atari. It was developed by Christen Fihl and sold under different names as MAXON Pascal (by MAXON Computers) and as HighSpeed Pascal (by HiSOFT, staff aquired by MAXON Computers in 2003). Note that MAXON Computers also sold another Pascal language related product named Kick Pascal. At this point in time it's unclear (red: to me the translator) what the relation (if any) is between the different branding.

HighSpeed Pascal

See HSPascal. Closed source commercial product. Development seized.

Kick Pascal

See HSPascal. Closed source commercial product. Development seized.


MAXON Pascal

See HSPascal. Closed source commercial product. Development seized.


PCQ Pascal

Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.


Free Pascal

And we kept the best for last. The Free Pascal compiler initially started out as FPK (by it's author initials Florian Paul Klampfl). People also refer to it as FPC.

The sources presented in this workshop are Free Pascal compatible. Don't try to use any of the other aforementioned compilers unless you know what you're doing.

Free Pascal installation on AmigaOS

At least one archive is required in order to be able to use the compiler for Amiga projects.

This archive can be found (red: link missing).

Extract the archive. (red: assign and path ?), then reboot.

Now we make a quick test to verify your setup:

Create a file named test.pas with the following content:

program test;

uses
  AmigaDOS;

var
  hello : PChar;

begin
  hello := 'Hello Amiga!' + sLinebreak;
  DOSWrite(DOSOutput, hello, Length(hello));
  WriteLn('Hello Pascal!');
  ExitCode := RETURN_OK;
end.

You can compile that with FPC using the following statement:

fpc test.pas

If this is compiled without error, then start your test. This should simply output two lines:

Hello Amiga!
Hello Pascal!

If this test was successful then you can continue the workshop with your compiler setup.

Pascal and AmigaOS

Because it fits perfectly here, I would like to take the opportunity to point out how Pascal and AmigaOS works interchangeably. In our test.pas we are immediately confronted by three different situations:

  • First we have the core Pascal language itself. Located in our example, you see the use of a basic type such as PChar and predefined constant sLineBreak.
  • Then we have the functions from the standard Pascal library. In our example these are the functions Length() and WriteLn(), which are declared in the system unit. These functions are available on any system and are typically part of the compiler package itself.
  • And last but not least, we have the AmigaOS system calls. These are of course only available on Amiga systems and are supplied via the additional platform specific units. From the Pascal programming point of view, AmigaOS looks like a large collection of functions and data types. In our example, these are the two functions DOSWrite() and DOSOutput() from dos.library, as well as the constant RETURN_OK, which are all declared in the unit AmigaDOS. These units can be found in the packages folder packages/amunits. Note that the the ominous amiga.lib is not required for these functions as quite recently the use of this unit is deprecated (red: since unreleased yet Free Pascal version 3.2.x, that is why you should use FPC trunk 3.1.1)

So, now it should be clear why our test.pas reads as it does: It will check whether our compiler installation is complete so we can use both the standard library and the Amiga system calls.

Here we go

What do we actually want to write right now ? Here is a quick description: We want to open a simple, resizable window on the Workbench where we can draw graphics using the graphics library - using accurate timed animation. Of course this should all be implemented in a system-friendly manner e.g. it should immediately respond to user interaction and consume as less computer time and resources as necessary. That sounds easier than it actually is therefor we will implement this step-by-step.

We will begin with our main entry-point. We do not want add too much code in there, but the main entry-point is a perfect place to check the presence of required libraries. For our implementation that would be intuition.library that is used for our window and graphics.library that is needed for our drawing commands.

First we define two global variables that represent the base address for the libraries:

//* our system libraries addresses */
var
  GfxBase       : PGfxBase       absolute AGraphics.GfxBase;
  IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;

Did you remember that these variables are already defined in our Pascal support units ? That is why we map them to their original variable by using the keyword absolute.

(Red: usually you would not have to do this mapping and you can use the variables GfxBase and IntuitionBase from their units directly, but a) we want to stay as close to the original c-source as possible and b) there currently is a tiny incompatibility with the type definition amongst supported platforms. Remember that this source can be compiled for Amiga, AmigaOS, AROS and MorphOS).

Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.

Instead we check in our main entry-point if indeed the libraries were opened successfully and if they match required version. That looks like this:

var
  result : Integer;
begin
  //* as long we did not execute RunEngine() we report a failure */
  result := RETURN_FAIL;

  //* we need at least 1.2 graphic.library's drawing functions */
  if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then
  begin
    //* we need at least 1.2 intuition.library for our window */
    if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then
    begin
      //* All libraries needed are available, so let's run... */
      result := RETURN_OK;
	//* Closing Intuition library and setting its baseaddress to nil */
      //* is not necessary as Pascal does that automatically for us    */
    end;
    //* Closing Graphics library and setting its baseaddress to nil */
    //* is not necessary as Pascal does that automatically for us   */
  end;

  //* Pascal uses System variable ExitCode to report back a value to caller
  ExitCode := result;
end;

As soon as we've made sure that the libraries where opened successfully, we initialize our own data, open the window and jump into the main loop. We will do this in our own function named RunEngine(). We also define a record structure where we store our run-time data, so that we can easily pass along a pointer to all functions involved. This avoids the use of ugly global variables:

type
  PRenderEngineData = ^TRenderEngineData;
  TRenderEngineData = 
  record
    window : PWindow;
    run    : boolean;
  end;

function RunEngine: integer;
var
  rd        : PRenderEngineData;
  newWindow : TNewWindow;
begin
  //* as long we did not enter our main loop we report an error */
  result := RETURN_ERROR;

  (*  
    allocate the memory for our runtime data and initialize it
    with zeros 
  *}
  rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));
  if assigned(rd) then
  begin
    //* now let's open our window */
    with newWindow do
    begin
      LeftEdge    :=   0;            TopEdge  := 14;
      Width       := 320;            Height   := 160;
      DetailPen   := UBYTE(not(0));  BlockPen := UBYTE(not(0));
      IDCMPFlags  := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;
      Flags       := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;
      FirstGadget := nil;            CheckMark := nil;
      Title       := 'Gfx Workshop';
      Screen      := nil;
      BitMap      := nil;
      MinWidth    := 96;             MinHeight := 48;
      MaxWidth    := UWORD(not(0));  MaxHeight := UWORD(not(0));
      WType       := WBENCHSCREEN_f;
    end;

    rd^.window := OpenWindow(@newWindow);
    if Assigned(rd^.window) then
    begin
      //* the main loop will run as long this is TRUE */
      rd^.run := TRUE;

      result := MainLoop(rd);

      //* cleanup: close the window */
      CloseWindow(rd^.window);
      rd^.window := nil;
    end;

    //* free our runtime data */
    ExecFreeMem(rd, sizeof(TRenderEngineData));
    rd := nil;
  end;
end;

The trained eye would have spotted immediately that we first allocate the memory for our RenderEngineData, initially filling the structure with zero's, and then open the window. This is a simple refresh window, which is why we also request that we want to receive IDCMP_REFRESHWINDOW messages from intuition.library and which allows us to redraw the contents of the window. Because we are going to redraw the window several times per second, using a smartrefresh window (where intuition would take care of redrawing) would be superfluous (red: counterproductive ?)

If everything worked out as intended then we jump into our MainLoop():

function  MainLoop(rd: PRenderEngineData): integer;
var
  winport : PMsgPort;
  winsig  : ULONG;
  
  msg     : PMessage;
begin
  //* remember the window port in a local variable for more easy use */
  winport := rd^.window^.UserPort;

  //* create our waitmask for the window port */
  winSig := 1 shl winport^.mp_SigBit;

  //* our main loop */
  while (rd^.run) do
  begin
    //* let's sleep until a message from our window arrives */
    Wait(winSig);

    {* 
      our window signaled us, so let's harvest all its messages
      in a loop... 
    *}
    while true do
    begin
      msg := GetMsg(winport);
      if not assigned(msg) then break;

      //* ...and dispatch and reply each of them */
      DispatchWindowMessage(rd, PIntuiMessage(msg));
      ReplyMsg(msg);
    end;
  end;
  result := RETURN_OK;
end;

We stay inside our main loop as long as rd^.run flag remains TRUE. We want to set this flag to false as soon as the user clicks on the close gadget of our window. Inside the main loop we'll wait until we get a signal from the window which is send by a message, modify that message, and reply to this message(s) using a loop. The modification of the message takes part inside function DispatchWindowMessage() as follows:

procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);
begin
  case (msg^.IClass) of
    IDCMP_CLOSEWINDOW:
    begin
      {* 
        User pressed the window's close gadget: exit the main loop as
        soon as possible
      *}
      rd^.run := FALSE;
    end;
    IDCMP_REFRESHWINDOW:
    begin
      BeginRefresh(rd^.window);
      EndRefresh(rd^.window, TRUE);
    end;
  end;
end;

Here we react to IDCMP_CLOSEWINDOW by setting our run-flag to false, which will cause us to leave our main loop as soon as all the other messages have been processed.

Because we still have nothing to draw, we simply call Beginrefresh()/EndRefresh() on a IDCMP_REFRESHWINDOW, by which we tell intuition that we have successfully redrawn our window.

If we compile all the above code (engine1.pas) Then we get an empty, resizable window, which we can close again. Great, isn't it? ;-)

fpc engine1.pas

[ you should be looking at a picture here ]

Heading text

Heading text

Heading text

Heading text

Heading text

Heading text

Heading text

Heading text

Heading text