http://fpcamigawiki.alb42.de/api.php?action=feedcontributions&user=Molly&feedformat=atomFreepascal Amiga wiki - User contributions [en]2024-03-28T23:15:02ZUser contributionsMediaWiki 1.35.1http://fpcamigawiki.alb42.de/index.php?title=Specifics&diff=880Specifics2018-02-11T18:45:03Z<p>Molly: /* Compiler defines */ Alphabetical tree</p>
<hr />
<div>== Specific implementations/changes for Free Pascal on Amiga systems==<br />
<br />
=== in Relation to other Free Pascal implementations ===<br />
----<br />
==== Compiler defines ====<br />
There are several [//www.freepascal.org/docs-html/prog/progap7.html#x329-344000G compiler defines (table G.4)] available for Free Pascal. More on multiplatform programming can be found [//wiki.freepascal.org/Multiplatform_Programming_Guide here].<br />
{| class="wikitable"<br />
|-<br />
!Definition HASAMIGA<br />
|With this definition you can can determine if you are on any of the Amiga platforms (also future proof for future Amiga platforms). Use it to encapsulate code parts (or uses) for all Amiga-style systems {$ifdef HASAMIGA} {$endif} or disable parts which does not work on all Amiga-style systems via {$ifndef HASAMIGA} {$endif}<br />
|-<br />
!Definition AMIGA<br />
|This definition exists in FreePascal for Amiga m68k and Amiga PowerPC (OS4) only. Do separate Amiga m68k and OS4 you can use the processor defines '''M68K''' and '''POWERPC''' or the specialized defines '''AMIGA68k''' and '''AMIGAOS4'''.<br />
|-<br />
!Definition MORPHOS<br />
|This definition exists in FreePascal for MorphOS.<br />
|-<br />
!Definition AROS<br />
| This definition exists in FreePascal for AROS. This enables you to determine if your program is compiled/targeted for AROS or not. You can use {$IFDEF AROS} or {$IFNDEF AROS} to make use of this define. ABI define '''AROS_ABIv0''' and '''AROS_ABIv1'''<br />
NOTE: There is no dedicated AROS m68k Version, the released Version is the Amiga m68k version because AROS m68k is binary compatible to Amiga classic.<br />
|}<br />
<br />
Tree structure of Amiga related platform defines:<br />
<br />
* '''HASAMIGA''' (defined since FPC 3.0)<br />
** '''AMIGA''' (Classic and new)<br />
*** '''AMIGA68K''' (Classic Amiga OS, defined since FPC 3.0.2)<br />
*** '''AMIGAOS4''' (Amiga OS4)<br />
** '''AROS''' (AROS)<br />
*** '''AROS_ABIv0''' (AROS v0 ABI)<br />
*** '''AROS_ABIv1''' (AROS v1 ABI)<br />
** '''MORPHOS''' (MorphOS)<br />
<br />
==== Additional System unit contents ====<br />
'''System''' unit has some additional functions and variables:<br />
<br />
Be warned if you change any of the variables then your program will crash and most likely your computer as well.<br />
<br />
{| class="wikitable"<br />
|-<br />
!SysDebug(Msg: ShortString); SysDebugLn(Msg: ShortString);<br />
|Write a message to debug output of the system. To read this debug messages you need a special tool to catch the messages (AROS: sashimi, hosted AROS: consoleoutput; MorphOS/AmigaOS4: LogTool) SysDebugLn() adds a return after the message, SysDebug() does not. '''NOTE''': The ''Msg'' parameter is a ShortString, so must not be longer than 255 chars.<br />
|-<br />
!AOS_WbMsg: Pointer;<br />
|Real Type is PWBStartup (from "Workbench" unit) can be used to determine if the program was started from WB (if this pointer is Nil then it was started from CLI)<br />
|-<br />
!AOS_ExecBase: Pointer;<br />
|Real type is: PExecBase (from "Exec" Unit) can be used to call all exec.library calls, or to inspect system basic features<br />
|-<br />
!AOS_DOSBase: Pointer;<br />
|Real type is: PDOSBase (from "AmigaDos" Unit) can be used to call all dos.library calls<br />
|-<br />
!AOS_UtilityBase: Pointer;<br />
|Real type is: PUtilityBase (from "Utility" Unit) can be used to call all utility.library calls<br />
|-<br />
!AOS_heapPool: Pointer;<br />
|Its a Pool Header created with (CreatePool from exec), all memory allocations are created in this pool, could be used to AllocPooled memory as well (even AllocMem, New and so on do it automatically)<br />
|}<br />
<br />
==== Threading ====<br />
{| class="wikitable"<br />
|-<br />
!athreads<br />
|This unit is needed if the program wants to use threads. It must be added as very first uses in the main source file. it is comparable to the ''cthreads'' units of freepascal for linux.<br />
|}<br />
<br />
==== Syscalls ====<br />
<br />
For all Amiga systems a special calling convention for calls into a Amiga-style (.library) with the keyword ''syscall''. A typical Library call looks like this:<br />
<source lang="pascal"><br />
procedure FktName(parameters); syscall Base Offset;<br />
</source><br />
{| class="wikitable"<br />
|-<br />
|''Parameter''<br />
|'''AROS(i386, x86_64, ARM), AmigaOS4''': Parameter of the function with type: '''param: type'''<br />
'''AmigaOS3, MorphOS''': Parameter of the function with type and location: '''param: type location 'register' ''' register is a standard m68k register 'd0'-'d7','a0'-'a6'<br />
|-<br />
|''Base''<br />
|'''AmigaOS3, AROS, MorphOS''': LibraryBase from OpenLibrary();<br />
'''AmigaOS4:''' Interface of the Library from GetInterface();<br />
|-<br />
|''Offset''<br />
|'''AmigaOS3, MorphOS, AmigaOS4''': Offset relative to the ''Base''<br />
'''AROS''': Offset relative to the ''Base'' divided by the size of a Pointer (therefore its the same value for 64 bit and 32bit)<br />
|}<br />
[[Call Library]] shows how to create syscalls for the different platforms and how to gather the needed informations<br />
<br />
Further informations about syscalls: [http://wiki.freepascal.org/Amiga#SysCalls AmigaOS3], [http://wiki.freepascal.org/AmigaOS#Library_interfaces_and_Syscalls AmigaOS4], [http://wiki.freepascal.org/MorphOS#SysCalls MorphOS]<br />
<br />
=== Relation to other languages on AROS/Amiga ===<br />
----<br />
Not really compiler related, but more about how ones program should behave, look and act on an amiganoid system.<br />
<br />
<br />
There is a document called "Amiga User Interface Style Guide" [http://amigaos.pl/amiga_user_interface_style_guide/index.html] that cover some of the basic techniques that should be practised when programming for amiganoid systems. Of course, it would be impossible to follow them all (if even for the GUI differences) but there are a couple of interesting topics in there:<br />
{| class="wikitable"<br />
|-<br />
!Embedded version IDs: [http://amigaos.pl/amiga_user_interface_style_guide/embedded_version_ids.html]<br />
|Gives the programmer the ability to let the system 'interrogate' your program (executable) and displays version information (from either commandline tool version or by using the icon information menu from the workbench).<br><br />
To put it simply, add a const string in generic version format:<br />
$VER: <name> <version>.<revision> (<d>.<m>.<y>) <br />
|}<br />
----<br />
==== Changed Unit Names ====<br />
{| class="wikitable"<br />
|+<br />
!Library name in AROS !! corresponding Free Pascal Unit !! Collision with<br />
|-<br />
|graphics.libray || agraphics || Graphics unit from LCL<br />
|-<br />
|dos.library || amigados || Dos unit from RTL<br />
|}<br />
<br />
Also note that the usage of unit amigalib is deprecated. The functions that resided in this unit can now be found in their respective counterpart units e.g. graphic related function can now be found inside unit AGraphics, Intuition related functions can now be found inside unit Intuition, etc.<br />
<br />
==== Changed constants/function Names ====<br />
{| class="wikitable"<br />
|+ <br />
!type !! Original Name !! Free Pascal Name !! Collision with<br />
|-<br />
| colspan=4 | '''Exec.Library (exec) '''<br />
|-<br />
|function || AllocMem || ExecAllocMem || AllocMem() in System<br />
|-<br />
|procedure || FreeMem || ExecFreeMem || FreeMem() in System<br />
|-<br />
|procedure || Insert || ExecInsert || Insert() in System<br />
|-<br />
|procedure || Exception || ExecException || Exception keyword<br />
|-<br />
| colspan=4 | '''dos.Library (amigados)'''<br />
|-<br />
|procedure || Close || DOSClose || Collides with Close() function in unit System<br />
|-<br />
| function || CreateDir || DOSCreateDir || Collides with CreateDir() function in unit SysUtils<br />
|-<br />
| function || DateToStr || DOSDateToStr || Collides with DateToStr() function in unit SysUtils<br />
|-<br />
|procedure || Delay || DOSDelay || Collides with Delay() function in unit CRT<br />
|-<br />
|procedure || DeleteFile || DOSDeleteFile || Collides with DeleteFile() function in unit SysUtils<br />
|-<br />
|procedure || Exit || DOSExit || Collides with Exit() function in unit System<br />
|-<br />
|procedure || Flush || DOSFlush || Collides with Flush() function in unit System<br />
|-<br />
| function || Format || DOSFormat || Collides with Format() function in unit SysUtils<br />
|-<br />
|variable || Input || DOSInput || Collides with Input() variable in unit System<br />
|-<br />
|procedure || Open || DOSOpen || For consistency with other dos functions<br />
|-<br />
|variable || Output || DOSOutput || Collides with Output() variable in unit System<br />
|-<br />
|procedure || Read || DOSRead || Collides with Read() function in unit System<br />
|-<br />
|procedure || Rename || DOSRename || Collides with Rename() function in unit System<br />
|-<br />
|procedure || Seek || DOSSeek || Collides with Seek() function in unit System<br />
|-<br />
| function || StrToDate || DOSStrToDate || Collides with StrToDate() function in unit SysUtils<br />
|-<br />
| function || System || DOSSystem || For consistency with other dos functions<br />
|-<br />
|procedure || Write || DOSWrite || Collides with Write() function in unit System<br />
|-<br />
| colspan=4 | '''Graphics.Library (agraphics)'''<br />
|-<br />
|procedure || Move || GfxMove || Move() in System<br />
|-<br />
|procedure || Text || GfxText || Text type<br />
|-<br />
| colspan=4 | '''Intuition.Library (intuition)'''<br />
|-<br />
|const || SINGLE || SINGLE_PT || Single type<br />
|-<br />
|const || FANFOLD || FANFOLD_PT || (renamed because of Single renaming)<br />
|-<br />
|const || WBENCHSCREEN || WBENCHSCREEN_f ||<br />
|-<br />
|const || PUBLICSCREEN || PUBLICSCREEN_f ||<br />
|-<br />
|const || CUSTOMSCREEN || CUSTOMSCREEN_f ||<br />
|-<br />
|const || SCREENTYPE || SCREENTYPE_f ||<br />
|-<br />
|const || SHOWTITLE || SHOTITLE_f ||<br />
|-<br />
|const || BEEPING || BEEPING_f ||<br />
|-<br />
|const || CUSTOMBITMAP || CUSTOMBITMAP_f ||<br />
|-<br />
|const || SCREENBEHIND || SCREENBEHIND_f ||<br />
|}</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Specifics&diff=879Specifics2018-02-11T18:40:34Z<p>Molly: /* Compiler defines */ Add AROS ABI platform defines</p>
<hr />
<div>== Specific implementations/changes for Free Pascal on Amiga systems==<br />
<br />
=== in Relation to other Free Pascal implementations ===<br />
----<br />
==== Compiler defines ====<br />
There are several [//www.freepascal.org/docs-html/prog/progap7.html#x329-344000G compiler defines (table G.4)] available for Free Pascal. More on multiplatform programming can be found [//wiki.freepascal.org/Multiplatform_Programming_Guide here].<br />
{| class="wikitable"<br />
|-<br />
!Definition HASAMIGA<br />
|With this definition you can can determine if you are on any of the Amiga platforms (also future proof for future Amiga platforms). Use it to encapsulate code parts (or uses) for all Amiga-style systems {$ifdef HASAMIGA} {$endif} or disable parts which does not work on all Amiga-style systems via {$ifndef HASAMIGA} {$endif}<br />
|-<br />
!Definition AMIGA<br />
|This definition exists in FreePascal for Amiga m68k and Amiga PowerPC (OS4) only. Do separate Amiga m68k and OS4 you can use the processor defines '''M68K''' and '''POWERPC''' or the specialized defines '''AMIGA68k''' and '''AMIGAOS4'''.<br />
|-<br />
!Definition MORPHOS<br />
|This definition exists in FreePascal for MorphOS.<br />
|-<br />
!Definition AROS<br />
| This definition exists in FreePascal for AROS. This enables you to determine if your program is compiled/targeted for AROS or not. You can use {$IFDEF AROS} or {$IFNDEF AROS} to make use of this define. ABI define '''AROS_ABIv0''' and '''AROS_ABIv1'''<br />
NOTE: There is no dedicated AROS m68k Version, the released Version is the Amiga m68k version because AROS m68k is binary compatible to Amiga classic.<br />
|}<br />
<br />
Tree structure of Amiga related platform defines:<br />
<br />
* '''HASAMIGA''' (defined since FPC 3.0)<br />
** '''AMIGA''' (Classic and new)<br />
*** '''AMIGA68K''' (Classic Amiga OS, defined since FPC 3.0.2)<br />
*** '''AMIGAOS4''' (Amiga OS4)<br />
** '''MORPHOS''' (MorphOS)<br />
** '''AROS''' (AROS)<br />
*** '''AROS_ABIv0''' (AROS v0 ABI)<br />
*** '''AROS_ABIv1''' (AROS v1 ABI)<br />
<br />
==== Additional System unit contents ====<br />
'''System''' unit has some additional functions and variables:<br />
<br />
Be warned if you change any of the variables then your program will crash and most likely your computer as well.<br />
<br />
{| class="wikitable"<br />
|-<br />
!SysDebug(Msg: ShortString); SysDebugLn(Msg: ShortString);<br />
|Write a message to debug output of the system. To read this debug messages you need a special tool to catch the messages (AROS: sashimi, hosted AROS: consoleoutput; MorphOS/AmigaOS4: LogTool) SysDebugLn() adds a return after the message, SysDebug() does not. '''NOTE''': The ''Msg'' parameter is a ShortString, so must not be longer than 255 chars.<br />
|-<br />
!AOS_WbMsg: Pointer;<br />
|Real Type is PWBStartup (from "Workbench" unit) can be used to determine if the program was started from WB (if this pointer is Nil then it was started from CLI)<br />
|-<br />
!AOS_ExecBase: Pointer;<br />
|Real type is: PExecBase (from "Exec" Unit) can be used to call all exec.library calls, or to inspect system basic features<br />
|-<br />
!AOS_DOSBase: Pointer;<br />
|Real type is: PDOSBase (from "AmigaDos" Unit) can be used to call all dos.library calls<br />
|-<br />
!AOS_UtilityBase: Pointer;<br />
|Real type is: PUtilityBase (from "Utility" Unit) can be used to call all utility.library calls<br />
|-<br />
!AOS_heapPool: Pointer;<br />
|Its a Pool Header created with (CreatePool from exec), all memory allocations are created in this pool, could be used to AllocPooled memory as well (even AllocMem, New and so on do it automatically)<br />
|}<br />
<br />
==== Threading ====<br />
{| class="wikitable"<br />
|-<br />
!athreads<br />
|This unit is needed if the program wants to use threads. It must be added as very first uses in the main source file. it is comparable to the ''cthreads'' units of freepascal for linux.<br />
|}<br />
<br />
==== Syscalls ====<br />
<br />
For all Amiga systems a special calling convention for calls into a Amiga-style (.library) with the keyword ''syscall''. A typical Library call looks like this:<br />
<source lang="pascal"><br />
procedure FktName(parameters); syscall Base Offset;<br />
</source><br />
{| class="wikitable"<br />
|-<br />
|''Parameter''<br />
|'''AROS(i386, x86_64, ARM), AmigaOS4''': Parameter of the function with type: '''param: type'''<br />
'''AmigaOS3, MorphOS''': Parameter of the function with type and location: '''param: type location 'register' ''' register is a standard m68k register 'd0'-'d7','a0'-'a6'<br />
|-<br />
|''Base''<br />
|'''AmigaOS3, AROS, MorphOS''': LibraryBase from OpenLibrary();<br />
'''AmigaOS4:''' Interface of the Library from GetInterface();<br />
|-<br />
|''Offset''<br />
|'''AmigaOS3, MorphOS, AmigaOS4''': Offset relative to the ''Base''<br />
'''AROS''': Offset relative to the ''Base'' divided by the size of a Pointer (therefore its the same value for 64 bit and 32bit)<br />
|}<br />
[[Call Library]] shows how to create syscalls for the different platforms and how to gather the needed informations<br />
<br />
Further informations about syscalls: [http://wiki.freepascal.org/Amiga#SysCalls AmigaOS3], [http://wiki.freepascal.org/AmigaOS#Library_interfaces_and_Syscalls AmigaOS4], [http://wiki.freepascal.org/MorphOS#SysCalls MorphOS]<br />
<br />
=== Relation to other languages on AROS/Amiga ===<br />
----<br />
Not really compiler related, but more about how ones program should behave, look and act on an amiganoid system.<br />
<br />
<br />
There is a document called "Amiga User Interface Style Guide" [http://amigaos.pl/amiga_user_interface_style_guide/index.html] that cover some of the basic techniques that should be practised when programming for amiganoid systems. Of course, it would be impossible to follow them all (if even for the GUI differences) but there are a couple of interesting topics in there:<br />
{| class="wikitable"<br />
|-<br />
!Embedded version IDs: [http://amigaos.pl/amiga_user_interface_style_guide/embedded_version_ids.html]<br />
|Gives the programmer the ability to let the system 'interrogate' your program (executable) and displays version information (from either commandline tool version or by using the icon information menu from the workbench).<br><br />
To put it simply, add a const string in generic version format:<br />
$VER: <name> <version>.<revision> (<d>.<m>.<y>) <br />
|}<br />
----<br />
==== Changed Unit Names ====<br />
{| class="wikitable"<br />
|+<br />
!Library name in AROS !! corresponding Free Pascal Unit !! Collision with<br />
|-<br />
|graphics.libray || agraphics || Graphics unit from LCL<br />
|-<br />
|dos.library || amigados || Dos unit from RTL<br />
|}<br />
<br />
Also note that the usage of unit amigalib is deprecated. The functions that resided in this unit can now be found in their respective counterpart units e.g. graphic related function can now be found inside unit AGraphics, Intuition related functions can now be found inside unit Intuition, etc.<br />
<br />
==== Changed constants/function Names ====<br />
{| class="wikitable"<br />
|+ <br />
!type !! Original Name !! Free Pascal Name !! Collision with<br />
|-<br />
| colspan=4 | '''Exec.Library (exec) '''<br />
|-<br />
|function || AllocMem || ExecAllocMem || AllocMem() in System<br />
|-<br />
|procedure || FreeMem || ExecFreeMem || FreeMem() in System<br />
|-<br />
|procedure || Insert || ExecInsert || Insert() in System<br />
|-<br />
|procedure || Exception || ExecException || Exception keyword<br />
|-<br />
| colspan=4 | '''dos.Library (amigados)'''<br />
|-<br />
|procedure || Close || DOSClose || Collides with Close() function in unit System<br />
|-<br />
| function || CreateDir || DOSCreateDir || Collides with CreateDir() function in unit SysUtils<br />
|-<br />
| function || DateToStr || DOSDateToStr || Collides with DateToStr() function in unit SysUtils<br />
|-<br />
|procedure || Delay || DOSDelay || Collides with Delay() function in unit CRT<br />
|-<br />
|procedure || DeleteFile || DOSDeleteFile || Collides with DeleteFile() function in unit SysUtils<br />
|-<br />
|procedure || Exit || DOSExit || Collides with Exit() function in unit System<br />
|-<br />
|procedure || Flush || DOSFlush || Collides with Flush() function in unit System<br />
|-<br />
| function || Format || DOSFormat || Collides with Format() function in unit SysUtils<br />
|-<br />
|variable || Input || DOSInput || Collides with Input() variable in unit System<br />
|-<br />
|procedure || Open || DOSOpen || For consistency with other dos functions<br />
|-<br />
|variable || Output || DOSOutput || Collides with Output() variable in unit System<br />
|-<br />
|procedure || Read || DOSRead || Collides with Read() function in unit System<br />
|-<br />
|procedure || Rename || DOSRename || Collides with Rename() function in unit System<br />
|-<br />
|procedure || Seek || DOSSeek || Collides with Seek() function in unit System<br />
|-<br />
| function || StrToDate || DOSStrToDate || Collides with StrToDate() function in unit SysUtils<br />
|-<br />
| function || System || DOSSystem || For consistency with other dos functions<br />
|-<br />
|procedure || Write || DOSWrite || Collides with Write() function in unit System<br />
|-<br />
| colspan=4 | '''Graphics.Library (agraphics)'''<br />
|-<br />
|procedure || Move || GfxMove || Move() in System<br />
|-<br />
|procedure || Text || GfxText || Text type<br />
|-<br />
| colspan=4 | '''Intuition.Library (intuition)'''<br />
|-<br />
|const || SINGLE || SINGLE_PT || Single type<br />
|-<br />
|const || FANFOLD || FANFOLD_PT || (renamed because of Single renaming)<br />
|-<br />
|const || WBENCHSCREEN || WBENCHSCREEN_f ||<br />
|-<br />
|const || PUBLICSCREEN || PUBLICSCREEN_f ||<br />
|-<br />
|const || CUSTOMSCREEN || CUSTOMSCREEN_f ||<br />
|-<br />
|const || SCREENTYPE || SCREENTYPE_f ||<br />
|-<br />
|const || SHOWTITLE || SHOTITLE_f ||<br />
|-<br />
|const || BEEPING || BEEPING_f ||<br />
|-<br />
|const || CUSTOMBITMAP || CUSTOMBITMAP_f ||<br />
|-<br />
|const || SCREENBEHIND || SCREENBEHIND_f ||<br />
|}</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Specifics&diff=878Specifics2018-02-11T18:16:57Z<p>Molly: /* Compiler defines */ Add Amiga platform define tree</p>
<hr />
<div>== Specific implementations/changes for Free Pascal on Amiga systems==<br />
<br />
=== in Relation to other Free Pascal implementations ===<br />
----<br />
==== Compiler defines ====<br />
There are several [//www.freepascal.org/docs-html/prog/progap7.html#x329-344000G compiler defines (table G.4)] available for Free Pascal. More on multiplatform programming can be found [//wiki.freepascal.org/Multiplatform_Programming_Guide here].<br />
{| class="wikitable"<br />
|-<br />
!Definition HASAMIGA<br />
|With this definition you can can determine if you are on any of the Amiga platforms (also future proof for future Amiga platforms). Use it to encapsulate code parts (or uses) for all Amiga-style systems {$ifdef HASAMIGA} {$endif} or disable parts which does not work on all Amiga-style systems via {$ifndef HASAMIGA} {$endif}<br />
|-<br />
!Definition AMIGA<br />
|This definition exists in FreePascal for Amiga m68k and Amiga PowerPC (OS4) only. Do separate Amiga m68k and OS4 you can use the processor defines '''M68K''' and '''POWERPC''' or the specialized defines '''AMIGA68k''' and '''AMIGAOS4'''.<br />
|-<br />
!Definition MORPHOS<br />
|This definition exists in FreePascal for MorphOS.<br />
|-<br />
!Definition AROS<br />
| This definition exists in FreePascal for AROS. This enables you to determine if your program is compiled/targeted for AROS or not. You can use {$IFDEF AROS} or {$IFNDEF AROS} to make use of this define. ABI define '''AROS_ABIv0''' and '''AROS_ABIv1'''<br />
NOTE: There is no dedicated AROS m68k Version, the released Version is the Amiga m68k version because AROS m68k is binary compatible to Amiga classic.<br />
|}<br />
<br />
Tree structure of Amiga related platform defines:<br />
<br />
* '''HASAMIGA''' (defined since FPC 3.0)<br />
** '''AMIGA''' (Classic and new)<br />
*** '''AMIGA68K''' (Classic Amiga OS, defined since FPC 3.0.2)<br />
*** '''AMIGAOS4''' (Amiga OS4)<br />
** '''MORPHOS''' (MorphOS)<br />
** '''AROS''' (AROS)<br />
<br />
==== Additional System unit contents ====<br />
'''System''' unit has some additional functions and variables:<br />
<br />
Be warned if you change any of the variables then your program will crash and most likely your computer as well.<br />
<br />
{| class="wikitable"<br />
|-<br />
!SysDebug(Msg: ShortString); SysDebugLn(Msg: ShortString);<br />
|Write a message to debug output of the system. To read this debug messages you need a special tool to catch the messages (AROS: sashimi, hosted AROS: consoleoutput; MorphOS/AmigaOS4: LogTool) SysDebugLn() adds a return after the message, SysDebug() does not. '''NOTE''': The ''Msg'' parameter is a ShortString, so must not be longer than 255 chars.<br />
|-<br />
!AOS_WbMsg: Pointer;<br />
|Real Type is PWBStartup (from "Workbench" unit) can be used to determine if the program was started from WB (if this pointer is Nil then it was started from CLI)<br />
|-<br />
!AOS_ExecBase: Pointer;<br />
|Real type is: PExecBase (from "Exec" Unit) can be used to call all exec.library calls, or to inspect system basic features<br />
|-<br />
!AOS_DOSBase: Pointer;<br />
|Real type is: PDOSBase (from "AmigaDos" Unit) can be used to call all dos.library calls<br />
|-<br />
!AOS_UtilityBase: Pointer;<br />
|Real type is: PUtilityBase (from "Utility" Unit) can be used to call all utility.library calls<br />
|-<br />
!AOS_heapPool: Pointer;<br />
|Its a Pool Header created with (CreatePool from exec), all memory allocations are created in this pool, could be used to AllocPooled memory as well (even AllocMem, New and so on do it automatically)<br />
|}<br />
<br />
==== Threading ====<br />
{| class="wikitable"<br />
|-<br />
!athreads<br />
|This unit is needed if the program wants to use threads. It must be added as very first uses in the main source file. it is comparable to the ''cthreads'' units of freepascal for linux.<br />
|}<br />
<br />
==== Syscalls ====<br />
<br />
For all Amiga systems a special calling convention for calls into a Amiga-style (.library) with the keyword ''syscall''. A typical Library call looks like this:<br />
<source lang="pascal"><br />
procedure FktName(parameters); syscall Base Offset;<br />
</source><br />
{| class="wikitable"<br />
|-<br />
|''Parameter''<br />
|'''AROS(i386, x86_64, ARM), AmigaOS4''': Parameter of the function with type: '''param: type'''<br />
'''AmigaOS3, MorphOS''': Parameter of the function with type and location: '''param: type location 'register' ''' register is a standard m68k register 'd0'-'d7','a0'-'a6'<br />
|-<br />
|''Base''<br />
|'''AmigaOS3, AROS, MorphOS''': LibraryBase from OpenLibrary();<br />
'''AmigaOS4:''' Interface of the Library from GetInterface();<br />
|-<br />
|''Offset''<br />
|'''AmigaOS3, MorphOS, AmigaOS4''': Offset relative to the ''Base''<br />
'''AROS''': Offset relative to the ''Base'' divided by the size of a Pointer (therefore its the same value for 64 bit and 32bit)<br />
|}<br />
[[Call Library]] shows how to create syscalls for the different platforms and how to gather the needed informations<br />
<br />
Further informations about syscalls: [http://wiki.freepascal.org/Amiga#SysCalls AmigaOS3], [http://wiki.freepascal.org/AmigaOS#Library_interfaces_and_Syscalls AmigaOS4], [http://wiki.freepascal.org/MorphOS#SysCalls MorphOS]<br />
<br />
=== Relation to other languages on AROS/Amiga ===<br />
----<br />
Not really compiler related, but more about how ones program should behave, look and act on an amiganoid system.<br />
<br />
<br />
There is a document called "Amiga User Interface Style Guide" [http://amigaos.pl/amiga_user_interface_style_guide/index.html] that cover some of the basic techniques that should be practised when programming for amiganoid systems. Of course, it would be impossible to follow them all (if even for the GUI differences) but there are a couple of interesting topics in there:<br />
{| class="wikitable"<br />
|-<br />
!Embedded version IDs: [http://amigaos.pl/amiga_user_interface_style_guide/embedded_version_ids.html]<br />
|Gives the programmer the ability to let the system 'interrogate' your program (executable) and displays version information (from either commandline tool version or by using the icon information menu from the workbench).<br><br />
To put it simply, add a const string in generic version format:<br />
$VER: <name> <version>.<revision> (<d>.<m>.<y>) <br />
|}<br />
----<br />
==== Changed Unit Names ====<br />
{| class="wikitable"<br />
|+<br />
!Library name in AROS !! corresponding Free Pascal Unit !! Collision with<br />
|-<br />
|graphics.libray || agraphics || Graphics unit from LCL<br />
|-<br />
|dos.library || amigados || Dos unit from RTL<br />
|}<br />
<br />
Also note that the usage of unit amigalib is deprecated. The functions that resided in this unit can now be found in their respective counterpart units e.g. graphic related function can now be found inside unit AGraphics, Intuition related functions can now be found inside unit Intuition, etc.<br />
<br />
==== Changed constants/function Names ====<br />
{| class="wikitable"<br />
|+ <br />
!type !! Original Name !! Free Pascal Name !! Collision with<br />
|-<br />
| colspan=4 | '''Exec.Library (exec) '''<br />
|-<br />
|function || AllocMem || ExecAllocMem || AllocMem() in System<br />
|-<br />
|procedure || FreeMem || ExecFreeMem || FreeMem() in System<br />
|-<br />
|procedure || Insert || ExecInsert || Insert() in System<br />
|-<br />
|procedure || Exception || ExecException || Exception keyword<br />
|-<br />
| colspan=4 | '''dos.Library (amigados)'''<br />
|-<br />
|procedure || Close || DOSClose || Collides with Close() function in unit System<br />
|-<br />
| function || CreateDir || DOSCreateDir || Collides with CreateDir() function in unit SysUtils<br />
|-<br />
| function || DateToStr || DOSDateToStr || Collides with DateToStr() function in unit SysUtils<br />
|-<br />
|procedure || Delay || DOSDelay || Collides with Delay() function in unit CRT<br />
|-<br />
|procedure || DeleteFile || DOSDeleteFile || Collides with DeleteFile() function in unit SysUtils<br />
|-<br />
|procedure || Exit || DOSExit || Collides with Exit() function in unit System<br />
|-<br />
|procedure || Flush || DOSFlush || Collides with Flush() function in unit System<br />
|-<br />
| function || Format || DOSFormat || Collides with Format() function in unit SysUtils<br />
|-<br />
|variable || Input || DOSInput || Collides with Input() variable in unit System<br />
|-<br />
|procedure || Open || DOSOpen || For consistency with other dos functions<br />
|-<br />
|variable || Output || DOSOutput || Collides with Output() variable in unit System<br />
|-<br />
|procedure || Read || DOSRead || Collides with Read() function in unit System<br />
|-<br />
|procedure || Rename || DOSRename || Collides with Rename() function in unit System<br />
|-<br />
|procedure || Seek || DOSSeek || Collides with Seek() function in unit System<br />
|-<br />
| function || StrToDate || DOSStrToDate || Collides with StrToDate() function in unit SysUtils<br />
|-<br />
| function || System || DOSSystem || For consistency with other dos functions<br />
|-<br />
|procedure || Write || DOSWrite || Collides with Write() function in unit System<br />
|-<br />
| colspan=4 | '''Graphics.Library (agraphics)'''<br />
|-<br />
|procedure || Move || GfxMove || Move() in System<br />
|-<br />
|procedure || Text || GfxText || Text type<br />
|-<br />
| colspan=4 | '''Intuition.Library (intuition)'''<br />
|-<br />
|const || SINGLE || SINGLE_PT || Single type<br />
|-<br />
|const || FANFOLD || FANFOLD_PT || (renamed because of Single renaming)<br />
|-<br />
|const || WBENCHSCREEN || WBENCHSCREEN_f ||<br />
|-<br />
|const || PUBLICSCREEN || PUBLICSCREEN_f ||<br />
|-<br />
|const || CUSTOMSCREEN || CUSTOMSCREEN_f ||<br />
|-<br />
|const || SCREENTYPE || SCREENTYPE_f ||<br />
|-<br />
|const || SHOWTITLE || SHOTITLE_f ||<br />
|-<br />
|const || BEEPING || BEEPING_f ||<br />
|-<br />
|const || CUSTOMBITMAP || CUSTOMBITMAP_f ||<br />
|-<br />
|const || SCREENBEHIND || SCREENBEHIND_f ||<br />
|}</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=877Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-30T12:29:07Z<p>Molly: Try to get rid of syntax parsing errors</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copper-lists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
[[File:HighSpeed Pascal 1.10.jpg|thumb|right|175px|HighSpeed Pascal 1.10]]<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
[[File:FPClogogif.gif|thumb|right|175px|Free Pascal]]<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have the word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practice even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rastports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast() you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is received, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and EndRefresh(): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
// [...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Our image learns to walk ==<br />
<br />
Now we want to animate our image by drawing only one iteration of our loop in RenderBackbuffer() per frame. To accomplish this we store the current counter in our RenderEngineData structure.<br />
<br />
<source lang="pascal"><br />
currentStep : integer;<br />
</source><br />
<br />
The RenderBackbuffer() function is modified so that it only uses the current value for four lines per call, and then increments the value by one:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* setup our maximum coordinates and our step width */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
//* compute our current coordinates */<br />
pos.x := rd^.currentStep * lineStep.x;<br />
pos.y := rd^.currentStep * lineStep.y;<br />
<br />
//* increase our step for the next frame */<br />
rd^.currentStep := rd^.currentStep + 1;<br />
if (rd^.currentStep >= stepCount) then<br />
begin<br />
rd^.currentStep := 0;<br />
end;<br />
<br />
//* now draw our line pattern */<br />
SetAPen(rastPort, 1);<br />
GfxMove(rastPort, 0, SLONG(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0 );<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(rastPort, 0 , LongInt(pos.y) );<br />
end;<br />
end;<br />
</source><br />
<br />
As soon as currentStep becomes greater or equal to StepCount, we will have to reset the value back to 0 again.<br />
<br />
The only thing left is to make sure that our RenderBackbuffer () is called regularly. In order to accomplish this we ignore the DoRender flag inside our loop and simply always draw in this situation.<br />
<br />
The Wait() is replaced by a setsignal() and allows us to query if a message from the windows was received but without the need to wait for such a message to arrive.<br />
<br />
<source lang="pascal"><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
signals : ULONG;<br />
<br />
sig : ULONG;<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* combine it with the CTRL-C signal */<br />
signals := winSig or SIGBREAKF_CTRL_C;<br />
<br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
sig := SetSignal(0, signals);<br />
<br />
if (sig and winSig) <> 0 then<br />
begin<br />
//* our window signaled us, so let's harvest all its messages in a loop... */<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
<br />
if (sig and SIGBREAKF_CTRL_C) <> 0 then<br />
begin<br />
//* we leave on CTRL-C */<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
result := rd^.returnCode;<br />
end;<br />
</source><br />
<br />
Here is the source engine4.pas, that can be compiled again with<br />
<br />
<source lang="pascal"><br />
fpc engine4.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== What's timer.device got to do with it ==<br />
<br />
As expected, our window now shows an animation. However, in practice this implementation is far from ideal: On the one hand the speed of our animation directly dependents on the computing and graphical performance of the Amiga on which it runs and, on the other hand it will consume all the processor time that is available. So we need a different solution here. We could simply add a call to Delay() which at the least would sort out the problem with consuming CPU speed. However, if we want to display an animation with a single image every 2 seconds, it would cause the window to be blocked during those two seconds and as a result will respond very sluggish when clicking on the close button or other user actions. We would also need to take the time it takes to render and display into account and for that a simple Delay() is not suitable because of its 1/50-second resolution. We need another timer and timer.device provides one that is perfectly suited for our purposes.<br />
<br />
Like any device, timer.device is also controlled by exec functions OpenDevice()/CloseDevice() and SendIO()/WaitIO()/DoIO()/AbortIO(). Additionally timer.device has a small set of functions that are called in a similar way as functions from a library. These functions are declared inside unit timer and require an initialized base pointer as for any library.<br />
<br />
Note that unit timer already provides a variable named TimerBase for this purpose.<br />
<br />
As it should be for any device, also timer.device is controlled by sending IORequests. These IORequests are generated by us and not by the device itself and in order to send these back and forth we also need a MsgPort. Timer.device also defines its own IORequest structure which is a structure named timerequest and is located in unit timer. In addition to the IORequest, there is also a structure TTimeval defined which supports timing with a resolution of microseconds (1/1000000 seconds).<br />
<br />
Therefor we're going to expand our RenderEngineData structure, this time with a MsgPort and a timerequest:<br />
<br />
<source lang="pascal"><br />
timerPort : PMsgPort;<br />
timerIO : Ptimerequest;<br />
</source><br />
<br />
We also need to make sure the following two units are in our uses clause:<br />
<br />
<source lang="pascal"><br />
Uses<br />
Exec, Timer;<br />
</source><br />
<br />
We create our own function to open timer.device:<br />
<br />
<source lang="pascal"><br />
function InitTimerDevice(rd: PRenderEngineData): integer;<br />
begin<br />
//* we do not return success until we've opened the timer.device */<br />
result := RETURN_FAIL;<br />
<br />
//* create a message port through which we will communicate with the timer.device */<br />
rd^.timerPort := CreatePort(nil, 0);<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
//* create a timerequest which we will we pass between the timer.device and ourself */<br />
rd^.timerIO := Ptimerequest(CreateExtIO(rd^.timerPort, sizeof(Ttimerequest)));<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
//* open the timer.device */<br />
if (OpenDevice(TIMERNAME, UNIT_MICROHZ, PIORequest(rd^.timerIO), 0) = 0) then<br />
begin<br />
//* Success: let's set the TimerBase so we can call timer.device's functions */<br />
TimerBase := PLibrary(rd^.timerIO^.tr_node.io_Device);<br />
result := RETURN_OK;<br />
end;<br />
end;<br />
end;<br />
<br />
if (result <> RETURN_OK) then<br />
begin<br />
//* in case of an error: cleanup immediately */<br />
FreeTimerDevice(rd);<br />
end;<br />
end;<br />
</source><br />
<br />
And a corresponding function FreeTimerDevice():<br />
<br />
<source lang="pascal"><br />
procedure FreeTimerDevice(rd: PRenderEngineData);<br />
begin<br />
//* close the timer.device */<br />
if Assigned(TimerBase) then<br />
begin<br />
CloseDevice(PIORequest(rd^.timerIO));<br />
TimerBase := nil;<br />
end;<br />
<br />
//* free our timerequest */<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
DeleteExtIO(PIORequest(rd^.timerIO));<br />
rd^.timerIO := nil;<br />
end;<br />
<br />
//* free our message port */<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
DeletePort(rd^.timerPort);<br />
rd^.timerPort := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
An additional short explanation for UNIT_MICROHZ used above: Timer.device offers several different modes, which mainly differ in their used resolution and accuracy. The mode we use here is characteristic for its high resolution. However it is also quite inaccurate: If you use it to count seconds then you'll notice that it will deviate from a price clock after a few minutes. However, this shortcoming is not important for our purpose.<br />
<br />
Anyhow, we call these functions immediately after the creation of our RenderEngineData and directly before its release respectively. Because we will send our timerequests asynchronously they are therefor not available for us at timer.device operations. Therefor we do not use our freshly created timerrequest but use it as a template only in order to retrieve the actual used request. For now we only need one for our timer, which we will also add to the RenderEngineData structure:<br />
<br />
<source lang="pascal"><br />
tickRequest : Ttimerequest;<br />
</source><br />
<br />
We initialize this field by simply assigning the contents of TimerIO to it:<br />
<br />
<source lang="pascal"><br />
rd^.tickRequest := rd^.timerIO^;<br />
</source><br />
<br />
Finally our RunEngine() that now looks like this:<br />
<br />
<source lang="pascal"><br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
//* allocate the memory for our runtime data and initialize it with zeros */<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
result := InitTimerDevice(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
//* setup our tick request */<br />
rd^.tickRequest := rd^.timerIO^;<br />
rd^.tickRequest.tr_node.io_Command := TR_ADDREQUEST;<br />
<br />
//* now let's open our window */<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
FreeTimerDevice(rd);<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
Inside Mainloop(), because we cannot leave our loop before a request is answered by timer.device, we have to remember whether or not the tickRequest is just waiting or not. We do this by using a simple boolean:<br />
<br />
<source lang="pascal"><br />
tickRequestPending : Boolean;<br />
</source><br />
<br />
We also need to expand our wait mask and include the TimerPort:<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the timer port */<br />
tickSig := 1 shl rd^.timerPort^.mp_SigBit;<br />
<br />
//* combine them to our final waitmask */<br />
signals := winSig or tickSig or SIGBREAKF_CTRL_C;<br />
</source><br />
<br />
We don't have to set DoRender to true, we'll do that later when we receive our ticks.<br />
<br />
Immediately before our main loop we send the first tick-request:<br />
<br />
<source lang="pascal"><br />
{* <br />
we start with a no-time request so we receive a tick immediately<br />
(we have to set 2 micros because of a bug in timer.device for 1.3) <br />
*}<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 2;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
tickRequestPending := TRUE;<br />
</source><br />
<br />
Instead of using SetSignal() we are now waiting properly for the arrival of window messages or timers.device ticks:<br />
<br />
<source lang="pascal"><br />
sig := Wait(signals);<br />
</source><br />
<br />
When we receive a tick signal we send it immediately so that we can be signaled about a 1/25 second later:<br />
<br />
<source lang="pascal"><br />
if (sig and tickSig) <> 0 then<br />
begin<br />
//* our tickRequest signalled us, let's remove it from the replyport */<br />
WaitIO(PIORequest(@rd^.tickRequest));<br />
<br />
if (rd^.run) then<br />
begin<br />
//* if we are running then we immediately request another tick... */<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 1000000 div 25;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
rd^.doRender := TRUE;<br />
end<br />
else<br />
begin<br />
//* ... if not we acknowledge that our tickRequest returned */<br />
tickRequestPending := FALSE;<br />
end;<br />
end;<br />
</source><br />
<br />
Only if we want to leave our main loop we set TickRequestPending to False instead, because we can now safely close the timer.device again.<br />
<br />
We also check whether we want to leave the loop but still have a tick-request pending in which case it must be canceled.:<br />
<br />
<source lang="pascal"><br />
if (not(rd^.run) and tickRequestPending) then<br />
begin<br />
//* We want to leave, but there is still a tick request pending? Let's abort it */<br />
AbortIO(PIORequest(@rd^.tickRequest));<br />
end;<br />
</source><br />
<br />
Now, if we compile and execute engine5.pas, we'll immediately see that our animation is now much more stable and smoother.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== More on timing ==<br />
<br />
Our current implementation has now reached the point where it will provide you with a maximum number of desired frames per second and that any other CPU time can be used by the system. However, the opposite has not yet been taken into account sufficiently: if we are unable to reach the desired frame rate, then our animation will also run slower as it simply advances a fixed amount of frames. This is undesired behavior: the animation will of course continue to stutter in the absence of a frame rate but if for example our rectangle makes a quarter rotation per second then it should always stay that way, whether we run with 50fps or only with 5fps. To accomplish this we still have to consider the factor time when drawing. We also retrieve this from timer.device by sending a corresponding request. Because our first request is already managed by timer.device and waits there until answered we have to create a second request. For this purpose we will fill rd^.timerIO() analogue to the initial first request. We also need to keep track of the time of our last frame. So we're going to expand our RenderEngineData with two entries:<br />
<br />
<source lang="pascal"><br />
getTimeRequest : Ttimerequest;<br />
lastRenderTime : Ttimeval;<br />
</source><br />
<br />
In RunEngine (), we'll initialize them:<br />
<br />
<source lang="pascal"><br />
//* setup our getTime request... */<br />
rd^.getTimeRequest := rd^.timerIO^;<br />
<br />
//* ... get the current time... */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* ... and initialize our lastRenderTime */<br />
rd^.lastRenderTime := rd^.getTimeRequest.tr_time;<br />
</source><br />
<br />
Now we have to rewrite our RenderBackbuffer() function: So far we have stubbornly increased our rectangular coordinates by a 1/32 of the output size per frame but now, instead, we want our coordinates to be incremented every four seconds by the output size. In order to make this as precise as possible we do not declare the current position as a word but as a floating point number.<br />
<br />
While FLOAT is a predefined type for c, it is not for Free Pascal so for consistency we declared a new FLOAT type first:<br />
<br />
<source lang="pascal"><br />
Type<br />
FLOAT = single;<br />
</source><br />
<br />
And define currentStep to be of type FLOAT:<br />
<br />
<source lang="pascal"><br />
currentStep : FLOAT;<br />
</source><br />
<br />
CurrentStep will now save the current position as a value between 0.0 and 1.0, where 0.0 will stand for the minimum coordinate and 1.0 for the maximum coordinate.<br />
<br />
To calculate this value, we need to determine the elapsed time per call to RenderBackbuffer() since the last frame. We do this by retrieving the current time and subtract the time of our last frame:<br />
<br />
<source lang="pascal"><br />
diff : Ttimeval;<br />
<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* get the time passed since our last render call */<br />
diff := rd^.getTimeRequest.tr_time;<br />
SubTime(@diff, @rd^.lastRenderTime);<br />
</source><br />
<br />
Diff now contains the difference in timeval format. This is a bit clumsy for our purposes, we'd rather have it as a floating point number in seconds: <br />
<br />
<source lang="pascal"><br />
secondsPassed : FLOAT;<br />
micros : ULONG;<br />
</source><br />
<br />
Therefor we also need to divide the complete number of microseconds in diff by 1000000.0:<br />
<br />
<source lang="pascal"><br />
if (diff.tv_secs <> 0) then<br />
begin<br />
micros := diff.tv_secs * 1000000;<br />
end<br />
else<br />
begin<br />
micros := 0;<br />
end;<br />
micros := micros + diff.tv_micro;<br />
secondsPassed := FLOAT(micros) / 1000000.0;<br />
</source><br />
<br />
Now we just need to increase currentStep by a quarter of secondsPassed so that we can reach our maximum value of 1.0 every four seconds. Once we have achieved this, we must deduct the absolute value from it. Because in practice we only expect an absolute value of 1.0, we do this with a simple subtraction. The while loop serves only as a safety net, just in case we somehow manage to get a value over 2.0; Normally we will only go through them once:<br />
<br />
<source lang="pascal"><br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
<br />
while (rd^.currentStep >= 1.0) do<br />
begin<br />
rd^.currentStep := rd^.currentStep - 1.0;<br />
end;<br />
</source><br />
<br />
Then we just have to insert the product from our maximum position and currentStep as current position:<br />
<br />
<source lang="pascal"><br />
pos.x := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.x)));<br />
pos.y := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.y)));<br />
</source><br />
<br />
If we start the executable then we can see a smooth rotating rectangle. Now if we would increase the system load (for example, by starting Engine6 several times) then we see that the animation starts to become choppy, but the rectangle rotates at the same speed.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Refactoring ==<br />
<br />
Actually, we're done now: We have an animation that is controlled by timer.device and which does not get disturbed even under full load. However, the different functions are very entangled. We want to separate those so that we are able to use the engine regardless of what we want to (re-)render. We also want to add a few smaller tweaks.<br />
<br />
So we want to isolate the renderer from our current code. This consists mainly of code for the drawing operations in RenderBackbuffer(), but also lastRenderTime and its logic belongs to it. Our renderer should also be able to determine the desired frame rate.<br />
<br />
The above makes it clear that our renderer at least consist of three functions:<br />
<br />
* InitRenderer() to create it and set the frame rate<br />
* RenderFrame() to draw a frame and<br />
* DestroyRenderer() to destroy the renderer again.<br />
<br />
We also want to make sure that both InitRenderer() and RenderFrame() can fail, so we use our integer return value that's proven to work. At DestroyRenderer() Nothing can go wrong (we cannot do any meaningful error handling here anyway), so this procedure remains:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(): integer;<br />
function RenderFrame(): integer;<br />
procedure DestroyRenderer();<br />
</source><br />
<br />
Naturally the renderer must also be able to store its own data during its lifetime. Therefor we enable this functionality and store the information inside the init function by passing it as a var pointer to the userdata and retrieve this information at RenderFrame() and DestroyRenderer() by passing the userdata as parameter:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer): integer;<br />
function RenderFrame(userData: pointer: integer;<br />
procedure DestroyRenderer(userData: pointer);<br />
</source><br />
<br />
It is useful to define a structure for the renderer data, which is then initialized in InitRenderer():<br />
<br />
<source lang="pascal"><br />
type<br />
PRendererData = ^TRendererData;<br />
TRendererData = <br />
record<br />
end;<br />
<br />
function InitRenderer(var userdata: pointer): integer;<br />
begin<br />
userData := AllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source> <br />
<br />
... and accordingly freed in DestroyRenderer():<br />
<br />
<source lang="pascal"><br />
procedure DestroyRenderer(userData: pointer);<br />
begin<br />
ExecFreeMem(userData, sizeof(TRendererData));<br />
end;<br />
</source><br />
<br />
In this renderer structure we copy our data from RenderEngineData too:<br />
<br />
<source lang="pascal"><br />
TRendererData = record<br />
lastRenderTime : Ttimeval;<br />
currentStep : FLOAT;<br />
end;<br />
</source><br />
<br />
To initialize this structure, we also need the current system time. And because we also want to set the framerate, our InitRenderer() looks like this:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer; const sysTime: Ptimeval; refreshRate: Ptimeval): integer;<br />
var<br />
rd : PRendererData;<br />
begin<br />
//* allocate our user data */<br />
userData := ExecAllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
//* set our lastRenderTime to now */<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
//* we would like to get a refresh rate of 25 frames per second */<br />
refreshRate^.tv_secs := 0;<br />
refreshRate^.tv_micro := 1000000 div 25;<br />
<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source><br />
<br />
In RenderFrame we use our previous used drawing operations. We are also tinkering with an auxiliary function to convert the difference between the two timeval structures in seconds as a float:<br />
<br />
<source lang="pascal"><br />
function DiffInSeconds(const early: Ptimeval; const late: Ptimeval): FLOAT;<br />
var<br />
diff : Ttimeval;<br />
micros : ULONG;<br />
begin<br />
diff := late^;<br />
SubTime(@diff, Ptimeval(early));<br />
<br />
if (diff.tv_secs <> 0)<br />
then micros := diff.tv_secs * 1000000<br />
else micros := 0;<br />
micros := micros + diff.tv_micro;<br />
<br />
result := FLOAT(micros) / 1000000.0;<br />
end;<br />
</source><br />
<br />
RenderFrame() looks a bit more tidy:<br />
<br />
check this function, it is the wrong code as it was taken from engine7.pas while the workshop is still preparing things<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; const sysTime: Ptimeval): integer;<br />
var<br />
secondsPassed : FLOAT;<br />
pos : TPoint;<br />
maxPos : TPoint;<br />
rd : PRendererData;<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
secondsPassed := DiffInSeconds(@rd^.lastRenderTime, sysTime);<br />
<br />
rd^.maxPos.x := renderTargetSize^.x - 1;<br />
rd^.maxPos.y := renderTargetSize^.y - 1;<br />
<br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
while (currentStep >= 1.0) do<br />
begin<br />
currentStep := currentStep - 1.0;<br />
end;<br />
<br />
//* now compute our new position */<br />
pos.x := SmallInt(trunc(rd^.currentStep * maxPosX));<br />
pos.y := SmallInt(trunc(rd^.currentStep * maxPosY));<br />
<br />
//* clear our bitmap */<br />
SetRast(renderTarget, 0);<br />
<br />
//* draw our rectangle */<br />
SetAPen(renderTarget, 1);<br />
GfxMove(renderTarget, 0 , LongInt(pos.y) );<br />
Draw(renderTarget , LongInt(maxPos.x - pos.x), 0 );<br />
Draw(renderTarget , LongInt(maxPos.x) , LongInt(maxPos.y - pos.y) );<br />
Draw(renderTarget , LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(renderTarget , 0 , LongInt(pos.y) );<br />
<br />
//* remember our render time */<br />
<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
Now we just have to make sure that these functions are called in the engine at the appropriate places. Because we also have to retrieve the current system time on multiple occasions, we write a separate function for it:<br />
<br />
<source lang="pascal"><br />
procedure UpdateTime(rd: PRenderEngineData);<br />
begin<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
end;<br />
</source><br />
<br />
If necessary, we read the system time from our getTimeRequest.<br />
<br />
In our RenderEngineData we also keep track of the desired refresh time and the UserData pointer for the renderer:<br />
<br />
<source lang="pascal"><br />
refreshRate : Ttimeval;<br />
userData : pointer;<br />
</source><br />
<br />
And we place the InitRenderer() call in RunEngine() immediately before opening our window when jumping into MainLoop():<br />
<br />
<source lang="pascal"><br />
//* get the current time... */<br />
UpdateTime(rd);<br />
<br />
//* ... and initialize our Renderer */<br />
result := InitRenderer(rd^.userData,<br />
@rd^.getTimeRequest.tr_time,<br />
@rd^.refreshRate);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
// [...] //* open window and do MainLoop */<br />
<br />
DestroyRenderer(rd^.userData);<br />
end;<br />
</source><br />
<br />
RenderFrame() is then then simply called in RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize,<br />
@rd^.getTimeRequest.tr_time<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Directly before the call to RenderFrame() we obtain the current time and pass it to RenderFrame().<br />
<br />
This means that we have completely refactored our renderer logic into three functions. In a real program you could now <br />
conveniently place the remaining engine into a separate unit. As part of this workshop, however, we do not want to have to deal with setting up a project or makefile at this time (red: Free Pascal does not require makefiles and/or projects setup (that is, if you do not wish to do so), so it's very easy to store the engine into a separate unit, as long as the compiler is able to locate this unit (which is no problem if a unit is located at the same location as where the main program file is stored).<br />
<br />
Instead, there is still one more thing that is bothering us: it may very well be that a call to RenderFrame determines that it does not actually has to render anything because the contents of the frame hasn't changed since the last call. However, we need to tell whether the last frame is still present in the backbuffer (e.g. because in the meantime the back buffer had to be recreated), otherwise the frame always have to be redrawn. To accomplish this, we expand RenderFrame with two parameters:<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; renderTargetDirty: boolean; const sysTime: Ptimeval; var updateDone: Boolean): integer;<br />
</source><br />
<br />
So RenderTargetDirty lets the renderer know whether the last frame is still present in the renderTarget. UpdateDone informs the caller whether or not the renderer actually drew a frame in renderTarget.<br />
<br />
To determine whether the backbuffer always has to be redrawn or if the previous frame is still intact, our PrepareBackBuffer function can be used. Therefor we also need to expand this function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData; var backBufferDirty: boolean): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
[Allocate new bitmap code snippet...]<br />
<br />
backBufferDirty := TRUE;<br />
end<br />
else<br />
begin<br />
backBufferDirty := FALSE;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
So we set BackBufferDirty to true as soon as we had to create our bitmap again.<br />
<br />
Now we put these two function together in RenderBackBuffer() and return the DoRepaint of RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData; var doRepaint: boolean): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize, backBufferDirty,<br />
@rd^.getTimeRequest.tr_time,<br />
doRepaint<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Now all we have to do is update the rendercall in MainLoop():<br />
<br />
<source lang="pascal"><br />
var<br />
doRepaint: boolean;<br />
<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd, doRepaint);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, set repaint if required */<br />
if not(rd^.doRepaint) then<br />
begin<br />
rd^.doRepaint := doRepaint;<br />
end;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
end;<br />
</source><br />
<br />
It should be noted that we do not overwrite an already set RD^.DoRepaint with a false value.<br />
<br />
<br />
We have now reached our first goal: we have a window in which we can draw using double-buffering and were we can even change the frame rate which can accurately be controlled by timer.device.<br />
<br />
Engine7.pas is compiled with this call:<br />
<br />
<source lang="pascal"><br />
fpc engine7.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=876Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-24T22:03:06Z<p>Molly: typo practise -> practice, Rasports -> Rastports, recieved -> received, Rerefresh() -> EndRefresh, setsignal () -> SetSignal()</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copper-lists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
[[File:HighSpeed Pascal 1.10.jpg|thumb|right|175px|HighSpeed Pascal 1.10]]<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
[[File:FPClogogif.gif|thumb|right|175px|Free Pascal]]<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have the word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practice even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rastports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast() you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is received, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and EndRefresh(): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
[...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Our image learns to walk ==<br />
<br />
Now we want to animate our image by drawing only one iteration of our loop in RenderBackbuffer() per frame. To accomplish this we store the current counter in our RenderEngineData structure.<br />
<br />
<source lang="pascal"><br />
currentStep : integer;<br />
</source><br />
<br />
The RenderBackbuffer() function is modified so that it only uses the current value for four lines per call, and then increments the value by one:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* setup our maximum coordinates and our step width */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
//* compute our current coordinates */<br />
pos.x := rd^.currentStep * lineStep.x;<br />
pos.y := rd^.currentStep * lineStep.y;<br />
<br />
//* increase our step for the next frame */<br />
rd^.currentStep := rd^.currentStep + 1;<br />
if (rd^.currentStep >= stepCount) then<br />
begin<br />
rd^.currentStep := 0;<br />
end;<br />
<br />
//* now draw our line pattern */<br />
SetAPen(rastPort, 1);<br />
GfxMove(rastPort, 0, SLONG(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0 );<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(rastPort, 0 , LongInt(pos.y) );<br />
end;<br />
end;<br />
</source><br />
<br />
As soon as currentStep becomes greater or equal to StepCount, we will have to reset the value back to 0 again.<br />
<br />
The only thing left is to make sure that our RenderBackbuffer () is called regularly. In order to accomplish this we ignore the DoRender flag inside our loop and simply always draw in this situation.<br />
<br />
The Wait() is replaced by a setsignal() and allows us to query if a message from the windows was received but without the need to wait for such a message to arrive.<br />
<br />
<source lang="pascal"><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
signals : ULONG;<br />
<br />
sig : ULONG;<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* combine it with the CTRL-C signal */<br />
signals := winSig or SIGBREAKF_CTRL_C;<br />
<br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
sig := SetSignal(0, signals);<br />
<br />
if (sig and winSig) <> 0 then<br />
begin<br />
//* our window signaled us, so let's harvest all its messages in a loop... */<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
<br />
if (sig and SIGBREAKF_CTRL_C) <> 0 then<br />
begin<br />
//* we leave on CTRL-C */<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
result := rd^.returnCode;<br />
end;<br />
</source><br />
<br />
Here is the source engine4.pas, that can be compiled again with<br />
<br />
<source lang="pascal"><br />
fpc engine4.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== What's timer.device got to do with it ==<br />
<br />
As expected, our window now shows an animation. However, in practice this implementation is far from ideal: On the one hand the speed of our animation directly dependents on the computing and graphical performance of the Amiga on which it runs and, on the other hand it will consume all the processor time that is available. So we need a different solution here. We could simply add a call to Delay() which at the least would sort out the problem with consuming CPU speed. However, if we want to display an animation with a single image every 2 seconds, it would cause the window to be blocked during those two seconds and as a result will respond very sluggish when clicking on the close button or other user actions. We would also need to take the time it takes to render and display into account and for that a simple Delay() is not suitable because of its 1/50-second resolution. We need another timer and timer.device provides one that is perfectly suited for our purposes.<br />
<br />
Like any device, timer.device is also controlled by exec functions OpenDevice()/CloseDevice() and SendIO()/WaitIO()/DoIO()/AbortIO(). Additionally timer.device has a small set of functions that are called in a similar way as functions from a library. These functions are declared inside unit timer and require an initialized base pointer as for any library.<br />
<br />
Note that unit timer already provides a variable named TimerBase for this purpose.<br />
<br />
As it should be for any device, also timer.device is controlled by sending IORequests. These IORequests are generated by us and not by the device itself and in order to send these back and forth we also need a MsgPort. Timer.device also defines its own IORequest structure which is a structure named timerequest and is located in unit timer. In addition to the IORequest, there is also a structure TTimeval defined which supports timing with a resolution of microseconds (1/1000000 seconds).<br />
<br />
Therefor we're going to expand our RenderEngineData structure, this time with a MsgPort and a timerequest:<br />
<br />
<source lang="pascal"><br />
timerPort : PMsgPort;<br />
timerIO : Ptimerequest;<br />
</source><br />
<br />
We also need to make sure the following two units are in our uses clause:<br />
<br />
<source lang="pascal"><br />
Uses<br />
Exec, Timer;<br />
</source><br />
<br />
We create our own function to open timer.device:<br />
<br />
<source lang="pascal"><br />
function InitTimerDevice(rd: PRenderEngineData): integer;<br />
begin<br />
//* we do not return success until we've opened the timer.device */<br />
result := RETURN_FAIL;<br />
<br />
//* create a message port through which we will communicate with the timer.device */<br />
rd^.timerPort := CreatePort(nil, 0);<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
//* create a timerequest which we will we pass between the timer.device and ourself */<br />
rd^.timerIO := Ptimerequest(CreateExtIO(rd^.timerPort, sizeof(Ttimerequest)));<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
//* open the timer.device */<br />
if (OpenDevice(TIMERNAME, UNIT_MICROHZ, PIORequest(rd^.timerIO), 0) = 0) then<br />
begin<br />
//* Success: let's set the TimerBase so we can call timer.device's functions */<br />
TimerBase := PLibrary(rd^.timerIO^.tr_node.io_Device);<br />
result := RETURN_OK;<br />
end;<br />
end;<br />
end;<br />
<br />
if (result <> RETURN_OK) then<br />
begin<br />
//* in case of an error: cleanup immediately */<br />
FreeTimerDevice(rd);<br />
end;<br />
end;<br />
</source><br />
<br />
And a corresponding function FreeTimerDevice():<br />
<br />
<source lang="pascal"><br />
procedure FreeTimerDevice(rd: PRenderEngineData);<br />
begin<br />
//* close the timer.device */<br />
if Assigned(TimerBase) then<br />
begin<br />
CloseDevice(PIORequest(rd^.timerIO));<br />
TimerBase := nil;<br />
end;<br />
<br />
//* free our timerequest */<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
DeleteExtIO(PIORequest(rd^.timerIO));<br />
rd^.timerIO := nil;<br />
end;<br />
<br />
//* free our message port */<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
DeletePort(rd^.timerPort);<br />
rd^.timerPort := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
An additional short explanation for UNIT_MICROHZ used above: Timer.device offers several different modes, which mainly differ in their used resolution and accuracy. The mode we use here is characteristic for its high resolution. However it is also quite inaccurate: If you use it to count seconds then you'll notice that it will deviate from a price clock after a few minutes. However, this shortcoming is not important for our purpose.<br />
<br />
Anyhow, we call these functions immediately after the creation of our RenderEngineData and directly before its release respectively. Because we will send our timerequests asynchronously they are therefor not available for us at timer.device operations. Therefor we do not use our freshly created timerrequest but use it as a template only in order to retrieve the actual used request. For now we only need one for our timer, which we will also add to the RenderEngineData structure:<br />
<br />
<source lang="pascal"><br />
tickRequest : Ttimerequest;<br />
</source><br />
<br />
We initialize this field by simply assigning the contents of TimerIO to it:<br />
<br />
<source lang="pascal"><br />
rd^.tickRequest := rd^.timerIO^;<br />
</source><br />
<br />
Finally our RunEngine() that now looks like this:<br />
<br />
<source lang="pascal"><br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
//* allocate the memory for our runtime data and initialize it with zeros */<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
result := InitTimerDevice(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
//* setup our tick request */<br />
rd^.tickRequest := rd^.timerIO^;<br />
rd^.tickRequest.tr_node.io_Command := TR_ADDREQUEST;<br />
<br />
//* now let's open our window */<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
FreeTimerDevice(rd);<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
Inside Mainloop(), because we cannot leave our loop before a request is answered by timer.device, we have to remember whether or not the tickRequest is just waiting or not. We do this by using a simple boolean:<br />
<br />
<source lang="pascal"><br />
tickRequestPending : Boolean;<br />
</source><br />
<br />
We also need to expand our wait mask and include the TimerPort:<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the timer port */<br />
tickSig := 1 shl rd^.timerPort^.mp_SigBit;<br />
<br />
//* combine them to our final waitmask */<br />
signals := winSig or tickSig or SIGBREAKF_CTRL_C;<br />
</source><br />
<br />
We don't have to set DoRender to true, we'll do that later when we receive our ticks.<br />
<br />
Immediately before our main loop we send the first tick-request:<br />
<br />
<source lang="pascal"><br />
{* <br />
we start with a no-time request so we receive a tick immediately<br />
(we have to set 2 micros because of a bug in timer.device for 1.3) <br />
*}<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 2;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
tickRequestPending := TRUE;<br />
</source><br />
<br />
Instead of using SetSignal() we are now waiting properly for the arrival of window messages or timers.device ticks:<br />
<br />
<source lang="pascal"><br />
sig := Wait(signals);<br />
</source><br />
<br />
When we receive a tick signal we send it immediately so that we can be signaled about a 1/25 second later:<br />
<br />
<source lang="pascal"><br />
if (sig and tickSig) <> 0 then<br />
begin<br />
//* our tickRequest signalled us, let's remove it from the replyport */<br />
WaitIO(PIORequest(@rd^.tickRequest));<br />
<br />
if (rd^.run) then<br />
begin<br />
//* if we are running then we immediately request another tick... */<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 1000000 div 25;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
rd^.doRender := TRUE;<br />
end<br />
else<br />
begin<br />
//* ... if not we acknowledge that our tickRequest returned */<br />
tickRequestPending := FALSE;<br />
end;<br />
end;<br />
</source><br />
<br />
Only if we want to leave our main loop we set TickRequestPending to False instead, because we can now safely close the timer.device again.<br />
<br />
We also check whether we want to leave the loop but still have a tick-request pending in which case it must be canceled.:<br />
<br />
<source lang="pascal"><br />
if (not(rd^.run) and tickRequestPending) then<br />
begin<br />
//* We want to leave, but there is still a tick request pending? Let's abort it */<br />
AbortIO(PIORequest(@rd^.tickRequest));<br />
end;<br />
</source><br />
<br />
Now, if we compile and execute engine5.pas, we'll immediately see that our animation is now much more stable and smoother.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== More on timing ==<br />
<br />
Our current implementation has now reached the point where it will provide you with a maximum number of desired frames per second and that any other CPU time can be used by the system. However, the opposite has not yet been taken into account sufficiently: if we are unable to reach the desired frame rate, then our animation will also run slower as it simply advances a fixed amount of frames. This is undesired behavior: the animation will of course continue to stutter in the absence of a frame rate but if for example our rectangle makes a quarter rotation per second then it should always stay that way, whether we run with 50fps or only with 5fps. To accomplish this we still have to consider the factor time when drawing. We also retrieve this from timer.device by sending a corresponding request. Because our first request is already managed by timer.device and waits there until answered we have to create a second request. For this purpose we will fill rd^.timerIO() analogue to the initial first request. We also need to keep track of the time of our last frame. So we're going to expand our RenderEngineData with two entries:<br />
<br />
<source lang="pascal"><br />
getTimeRequest : Ttimerequest;<br />
lastRenderTime : Ttimeval;<br />
</source><br />
<br />
In RunEngine (), we'll initialize them:<br />
<br />
<source lang="pascal"><br />
//* setup our getTime request... */<br />
rd^.getTimeRequest := rd^.timerIO^;<br />
<br />
//* ... get the current time... */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* ... and initialize our lastRenderTime */<br />
rd^.lastRenderTime := rd^.getTimeRequest.tr_time;<br />
</source><br />
<br />
Now we have to rewrite our RenderBackbuffer() function: So far we have stubbornly increased our rectangular coordinates by a 1/32 of the output size per frame but now, instead, we want our coordinates to be incremented every four seconds by the output size. In order to make this as precise as possible we do not declare the current position as a word but as a floating point number.<br />
<br />
While FLOAT is a predefined type for c, it is not for Free Pascal so for consistency we declared a new FLOAT type first:<br />
<br />
<source lang="pascal"><br />
Type<br />
FLOAT = single;<br />
</source><br />
<br />
And define currentStep to be of type FLOAT:<br />
<br />
<source lang="pascal"><br />
currentStep : FLOAT;<br />
</source><br />
<br />
CurrentStep will now save the current position as a value between 0.0 and 1.0, where 0.0 will stand for the minimum coordinate and 1.0 for the maximum coordinate.<br />
<br />
To calculate this value, we need to determine the elapsed time per call to RenderBackbuffer() since the last frame. We do this by retrieving the current time and subtract the time of our last frame:<br />
<br />
<source lang="pascal"><br />
diff : Ttimeval;<br />
<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* get the time passed since our last render call */<br />
diff := rd^.getTimeRequest.tr_time;<br />
SubTime(@diff, @rd^.lastRenderTime);<br />
</source><br />
<br />
Diff now contains the difference in timeval format. This is a bit clumsy for our purposes, we'd rather have it as a floating point number in seconds: <br />
<br />
<source lang="pascal"><br />
secondsPassed : FLOAT;<br />
micros : ULONG;<br />
</source><br />
<br />
Therefor we also need to divide the complete number of microseconds in diff by 1000000.0:<br />
<br />
<source lang="pascal"><br />
if (diff.tv_secs <> 0) then<br />
begin<br />
micros := diff.tv_secs * 1000000;<br />
end<br />
else<br />
begin<br />
micros := 0;<br />
end;<br />
micros := micros + diff.tv_micro;<br />
secondsPassed := FLOAT(micros) / 1000000.0;<br />
</source><br />
<br />
Now we just need to increase currentStep by a quarter of secondsPassed so that we can reach our maximum value of 1.0 every four seconds. Once we have achieved this, we must deduct the absolute value from it. Because in practice we only expect an absolute value of 1.0, we do this with a simple subtraction. The while loop serves only as a safety net, just in case we somehow manage to get a value over 2.0; Normally we will only go through them once:<br />
<br />
<source lang="pascal"><br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
<br />
while (rd^.currentStep >= 1.0) do<br />
begin<br />
rd^.currentStep := rd^.currentStep - 1.0;<br />
end;<br />
</source><br />
<br />
Then we just have to insert the product from our maximum position and currentStep as current position:<br />
<br />
<source lang="pascal"><br />
pos.x := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.x)));<br />
pos.y := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.y)));<br />
</source><br />
<br />
If we start the executable then we can see a smooth rotating rectangle. Now if we would increase the system load (for example, by starting Engine6 several times) then we see that the animation starts to become choppy, but the rectangle rotates at the same speed.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Refactoring ==<br />
<br />
Actually, we're done now: We have an animation that is controlled by timer.device and which does not get disturbed even under full load. However, the different functions are very entangled. We want to separate those so that we are able to use the engine regardless of what we want to (re-)render. We also want to add a few smaller tweaks.<br />
<br />
So we want to isolate the renderer from our current code. This consists mainly of code for the drawing operations in RenderBackbuffer(), but also lastRenderTime and its logic belongs to it. Our renderer should also be able to determine the desired frame rate.<br />
<br />
The above makes it clear that our renderer at least consist of three functions:<br />
<br />
* InitRenderer() to create it and set the frame rate<br />
* RenderFrame() to draw a frame and<br />
* DestroyRenderer() to destroy the renderer again.<br />
<br />
We also want to make sure that both InitRenderer() and RenderFrame() can fail, so we use our integer return value that's proven to work. At DestroyRenderer() Nothing can go wrong (we cannot do any meaningful error handling here anyway), so this procedure remains:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(): integer;<br />
function RenderFrame(): integer;<br />
procedure DestroyRenderer();<br />
</source><br />
<br />
Naturally the renderer must also be able to store its own data during its lifetime. Therefor we enable this functionality and store the information inside the init function by passing it as a var pointer to the userdata and retrieve this information at RenderFrame() and DestroyRenderer() by passing the userdata as parameter:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer): integer;<br />
function RenderFrame(userData: pointer: integer;<br />
procedure DestroyRenderer(userData: pointer);<br />
</source><br />
<br />
It is useful to define a structure for the renderer data, which is then initialized in InitRenderer():<br />
<br />
<source lang="pascal"><br />
type<br />
PRendererData = ^TRendererData;<br />
TRendererData = <br />
record<br />
end;<br />
<br />
function InitRenderer(var userdata: pointer): integer;<br />
begin<br />
userData := AllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source> <br />
<br />
... and accordingly freed in DestroyRenderer():<br />
<br />
<source lang="pascal"><br />
procedure DestroyRenderer(userData: pointer);<br />
begin<br />
ExecFreeMem(userData, sizeof(TRendererData));<br />
end;<br />
</source><br />
<br />
In this renderer structure we copy our data from RenderEngineData too:<br />
<br />
<source lang="pascal"><br />
TRendererData = record<br />
lastRenderTime : Ttimeval;<br />
currentStep : FLOAT;<br />
end;<br />
</source><br />
<br />
To initialize this structure, we also need the current system time. And because we also want to set the framerate, our InitRenderer() looks like this:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer; const sysTime: Ptimeval; refreshRate: Ptimeval): integer;<br />
var<br />
rd : PRendererData;<br />
begin<br />
//* allocate our user data */<br />
userData := ExecAllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
//* set our lastRenderTime to now */<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
//* we would like to get a refresh rate of 25 frames per second */<br />
refreshRate^.tv_secs := 0;<br />
refreshRate^.tv_micro := 1000000 div 25;<br />
<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source><br />
<br />
In RenderFrame we use our previous used drawing operations. We are also tinkering with an auxiliary function to convert the difference between the two timeval structures in seconds as a float:<br />
<br />
<source lang="pascal"><br />
function DiffInSeconds(const early: Ptimeval; const late: Ptimeval): FLOAT;<br />
var<br />
diff : Ttimeval;<br />
micros : ULONG;<br />
begin<br />
diff := late^;<br />
SubTime(@diff, Ptimeval(early));<br />
<br />
if (diff.tv_secs <> 0)<br />
then micros := diff.tv_secs * 1000000<br />
else micros := 0;<br />
micros := micros + diff.tv_micro;<br />
<br />
result := FLOAT(micros) / 1000000.0;<br />
end;<br />
</source><br />
<br />
RenderFrame() looks a bit more tidy:<br />
<br />
check this function, it is the wrong code as it was taken from engine7.pas while the workshop is still preparing things<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; const sysTime: Ptimeval): integer;<br />
var<br />
secondsPassed : FLOAT;<br />
pos : TPoint;<br />
maxPos : TPoint;<br />
rd : PRendererData;<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
secondsPassed := DiffInSeconds(@rd^.lastRenderTime, sysTime);<br />
<br />
rd^.maxPos.x := renderTargetSize^.x - 1;<br />
rd^.maxPos.y := renderTargetSize^.y - 1;<br />
<br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
while (currentStep >= 1.0) do<br />
begin<br />
currentStep := currentStep - 1.0;<br />
end;<br />
<br />
//* now compute our new position */<br />
pos.x := SmallInt(trunc(rd^.currentStep * maxPosX));<br />
pos.y := SmallInt(trunc(rd^.currentStep * maxPosY));<br />
<br />
//* clear our bitmap */<br />
SetRast(renderTarget, 0);<br />
<br />
//* draw our rectangle */<br />
SetAPen(renderTarget, 1);<br />
GfxMove(renderTarget, 0 , LongInt(pos.y) );<br />
Draw(renderTarget , LongInt(maxPos.x - pos.x), 0 );<br />
Draw(renderTarget , LongInt(maxPos.x) , LongInt(maxPos.y - pos.y) );<br />
Draw(renderTarget , LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(renderTarget , 0 , LongInt(pos.y) );<br />
<br />
//* remember our render time */<br />
<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
Now we just have to make sure that these functions are called in the engine at the appropriate places. Because we also have to retrieve the current system time on multiple occasions, we write a separate function for it:<br />
<br />
<source lang="pascal"><br />
procedure UpdateTime(rd: PRenderEngineData);<br />
begin<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
end;<br />
</source><br />
<br />
If necessary, we read the system time from our getTimeRequest.<br />
<br />
In our RenderEngineData we also keep track of the desired refresh time and the UserData pointer for the renderer:<br />
<br />
<source lang="pascal"><br />
refreshRate : Ttimeval;<br />
userData : pointer;<br />
</source><br />
<br />
And we place the InitRenderer() call in RunEngine() immediately before opening our window when jumping into MainLoop():<br />
<br />
<source lang="pascal"><br />
//* get the current time... */<br />
UpdateTime(rd);<br />
<br />
//* ... and initialize our Renderer */<br />
result := InitRenderer(rd^.userData,<br />
@rd^.getTimeRequest.tr_time,<br />
@rd^.refreshRate);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
[...] //* open window and do MainLoop */<br />
<br />
DestroyRenderer(rd^.userData);<br />
end;<br />
</source><br />
<br />
RenderFrame() is then then simply called in RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize,<br />
@rd^.getTimeRequest.tr_time<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Directly before the call to RenderFrame() we obtain the current time and pass it to RenderFrame().<br />
<br />
This means that we have completely refactored our renderer logic into three functions. In a real program you could now <br />
conveniently place the remaining engine into a separate unit. As part of this workshop, however, we do not want to have to deal with setting up a project or makefile at this time (red: Free Pascal does not require makefiles and/or projects setup (that is, if you do not wish to do so), so it's very easy to store the engine into a separate unit, as long as the compiler is able to locate this unit (which is no problem if a unit is located at the same location as where the main program file is stored).<br />
<br />
Instead, there is still one more thing that is bothering us: it may very well be that a call to RenderFrame determines that it does not actually has to render anything because the contents of the frame hasn't changed since the last call. However, we need to tell whether the last frame is still present in the backbuffer (e.g. because in the meantime the back buffer had to be recreated), otherwise the frame always have to be redrawn. To accomplish this, we expand RenderFrame with two parameters:<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; renderTargetDirty: boolean; const sysTime: Ptimeval; var updateDone: Boolean): integer;<br />
</source><br />
<br />
So RenderTargetDirty lets the renderer know whether the last frame is still present in the renderTarget. UpdateDone informs the caller whether or not the renderer actually drew a frame in renderTarget.<br />
<br />
To determine whether the backbuffer always has to be redrawn or if the previous frame is still intact, our PrepareBackBuffer function can be used. Therefor we also need to expand this function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData; var backBufferDirty: boolean): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
[Allocate new bitmap code snippet...]<br />
<br />
backBufferDirty := TRUE;<br />
end<br />
else<br />
begin<br />
backBufferDirty := FALSE;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
So we set BackBufferDirty to true as soon as we had to create our bitmap again.<br />
<br />
Now we put these two function together in RenderBackBuffer() and return the DoRepaint of RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData; var doRepaint: boolean): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize, backBufferDirty,<br />
@rd^.getTimeRequest.tr_time,<br />
doRepaint<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Now all we have to do is update the rendercall in MainLoop():<br />
<br />
<source lang="pascal"><br />
var<br />
doRepaint: boolean;<br />
<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd, doRepaint);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, set repaint if required */<br />
if not(rd^.doRepaint) then<br />
begin<br />
rd^.doRepaint := doRepaint;<br />
end;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
end;<br />
</source><br />
<br />
It should be noted that we do not overwrite an already set RD^.DoRepaint with a false value.<br />
<br />
<br />
We have now reached our first goal: we have a window in which we can draw using double-buffering and were we can even change the frame rate which can accurately be controlled by timer.device.<br />
<br />
Engine7.pas is compiled with this call:<br />
<br />
<source lang="pascal"><br />
fpc engine7.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=875Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-24T21:53:28Z<p>Molly: typo te -> the</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
[[File:HighSpeed Pascal 1.10.jpg|thumb|right|175px|HighSpeed Pascal 1.10]]<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
[[File:FPClogogif.gif|thumb|right|175px|Free Pascal]]<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have the word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
[...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Our image learns to walk ==<br />
<br />
Now we want to animate our image by drawing only one iteration of our loop in RenderBackbuffer() per frame. To accomplish this we store the current counter in our RenderEngineData structure.<br />
<br />
<source lang="pascal"><br />
currentStep : integer;<br />
</source><br />
<br />
The RenderBackbuffer() function is modified so that it only uses the current value for four lines per call, and then increments the value by one:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* setup our maximum coordinates and our step width */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
//* compute our current coordinates */<br />
pos.x := rd^.currentStep * lineStep.x;<br />
pos.y := rd^.currentStep * lineStep.y;<br />
<br />
//* increase our step for the next frame */<br />
rd^.currentStep := rd^.currentStep + 1;<br />
if (rd^.currentStep >= stepCount) then<br />
begin<br />
rd^.currentStep := 0;<br />
end;<br />
<br />
//* now draw our line pattern */<br />
SetAPen(rastPort, 1);<br />
GfxMove(rastPort, 0, SLONG(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0 );<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(rastPort, 0 , LongInt(pos.y) );<br />
end;<br />
end;<br />
</source><br />
<br />
As soon as currentStep becomes greater or equal to StepCount, we will have to reset the value back to 0 again.<br />
<br />
The only thing left is to make sure that our RenderBackbuffer () is called regularly. In order to accomplish this we ignore the DoRender flag inside our loop and simply always draw in this situation.<br />
<br />
The Wait() is replaced by a setsignal() and allows us to query if a message from the windows was received but without the need to wait for such a message to arrive.<br />
<br />
<source lang="pascal"><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
signals : ULONG;<br />
<br />
sig : ULONG;<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* combine it with the CTRL-C signal */<br />
signals := winSig or SIGBREAKF_CTRL_C;<br />
<br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
sig := SetSignal(0, signals);<br />
<br />
if (sig and winSig) <> 0 then<br />
begin<br />
//* our window signaled us, so let's harvest all its messages in a loop... */<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
<br />
if (sig and SIGBREAKF_CTRL_C) <> 0 then<br />
begin<br />
//* we leave on CTRL-C */<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
result := rd^.returnCode;<br />
end;<br />
</source><br />
<br />
Here is the source engine4.pas, that can be compiled again with<br />
<br />
<source lang="pascal"><br />
fpc engine4.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== What's timer.device got to do with it ==<br />
<br />
As expected, our window now shows an animation. However, in practice this implementation is far from ideal: On the one hand the speed of our animation directly dependents on the computing and graphical performance of the Amiga on which it runs and, on the other hand it will consume all the processor time that is available. So we need a different solution here. We could simply add a call to Delay() which at the least would sort out the problem with consuming CPU speed. However, if we want to display an animation with a single image every 2 seconds, it would cause the window to be blocked during those two seconds and as a result will respond very sluggish when clicking on the close button or other user actions. We would also need to take the time it takes to render and display into account and for that a simple Delay() is not suitable because of its 1/50-second resolution. We need another timer and timer.device provides one that is perfectly suited for our purposes.<br />
<br />
Like any device, timer.device is also controlled by exec functions OpenDevice()/CloseDevice() and SendIO()/WaitIO()/DoIO()/AbortIO(). Additionally timer.device has a small set of functions that are called in a similar way as functions from a library. These functions are declared inside unit timer and require an initialized base pointer as for any library.<br />
<br />
Note that unit timer already provides a variable named TimerBase for this purpose.<br />
<br />
As it should be for any device, also timer.device is controlled by sending IORequests. These IORequests are generated by us and not by the device itself and in order to send these back and forth we also need a MsgPort. Timer.device also defines its own IORequest structure which is a structure named timerequest and is located in unit timer. In addition to the IORequest, there is also a structure TTimeval defined which supports timing with a resolution of microseconds (1/1000000 seconds).<br />
<br />
Therefor we're going to expand our RenderEngineData structure, this time with a MsgPort and a timerequest:<br />
<br />
<source lang="pascal"><br />
timerPort : PMsgPort;<br />
timerIO : Ptimerequest;<br />
</source><br />
<br />
We also need to make sure the following two units are in our uses clause:<br />
<br />
<source lang="pascal"><br />
Uses<br />
Exec, Timer;<br />
</source><br />
<br />
We create our own function to open timer.device:<br />
<br />
<source lang="pascal"><br />
function InitTimerDevice(rd: PRenderEngineData): integer;<br />
begin<br />
//* we do not return success until we've opened the timer.device */<br />
result := RETURN_FAIL;<br />
<br />
//* create a message port through which we will communicate with the timer.device */<br />
rd^.timerPort := CreatePort(nil, 0);<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
//* create a timerequest which we will we pass between the timer.device and ourself */<br />
rd^.timerIO := Ptimerequest(CreateExtIO(rd^.timerPort, sizeof(Ttimerequest)));<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
//* open the timer.device */<br />
if (OpenDevice(TIMERNAME, UNIT_MICROHZ, PIORequest(rd^.timerIO), 0) = 0) then<br />
begin<br />
//* Success: let's set the TimerBase so we can call timer.device's functions */<br />
TimerBase := PLibrary(rd^.timerIO^.tr_node.io_Device);<br />
result := RETURN_OK;<br />
end;<br />
end;<br />
end;<br />
<br />
if (result <> RETURN_OK) then<br />
begin<br />
//* in case of an error: cleanup immediately */<br />
FreeTimerDevice(rd);<br />
end;<br />
end;<br />
</source><br />
<br />
And a corresponding function FreeTimerDevice():<br />
<br />
<source lang="pascal"><br />
procedure FreeTimerDevice(rd: PRenderEngineData);<br />
begin<br />
//* close the timer.device */<br />
if Assigned(TimerBase) then<br />
begin<br />
CloseDevice(PIORequest(rd^.timerIO));<br />
TimerBase := nil;<br />
end;<br />
<br />
//* free our timerequest */<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
DeleteExtIO(PIORequest(rd^.timerIO));<br />
rd^.timerIO := nil;<br />
end;<br />
<br />
//* free our message port */<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
DeletePort(rd^.timerPort);<br />
rd^.timerPort := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
An additional short explanation for UNIT_MICROHZ used above: Timer.device offers several different modes, which mainly differ in their used resolution and accuracy. The mode we use here is characteristic for its high resolution. However it is also quite inaccurate: If you use it to count seconds then you'll notice that it will deviate from a price clock after a few minutes. However, this shortcoming is not important for our purpose.<br />
<br />
Anyhow, we call these functions immediately after the creation of our RenderEngineData and directly before its release respectively. Because we will send our timerequests asynchronously they are therefor not available for us at timer.device operations. Therefor we do not use our freshly created timerrequest but use it as a template only in order to retrieve the actual used request. For now we only need one for our timer, which we will also add to the RenderEngineData structure:<br />
<br />
<source lang="pascal"><br />
tickRequest : Ttimerequest;<br />
</source><br />
<br />
We initialize this field by simply assigning the contents of TimerIO to it:<br />
<br />
<source lang="pascal"><br />
rd^.tickRequest := rd^.timerIO^;<br />
</source><br />
<br />
Finally our RunEngine() that now looks like this:<br />
<br />
<source lang="pascal"><br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
//* allocate the memory for our runtime data and initialize it with zeros */<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
result := InitTimerDevice(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
//* setup our tick request */<br />
rd^.tickRequest := rd^.timerIO^;<br />
rd^.tickRequest.tr_node.io_Command := TR_ADDREQUEST;<br />
<br />
//* now let's open our window */<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
FreeTimerDevice(rd);<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
Inside Mainloop(), because we cannot leave our loop before a request is answered by timer.device, we have to remember whether or not the tickRequest is just waiting or not. We do this by using a simple boolean:<br />
<br />
<source lang="pascal"><br />
tickRequestPending : Boolean;<br />
</source><br />
<br />
We also need to expand our wait mask and include the TimerPort:<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the timer port */<br />
tickSig := 1 shl rd^.timerPort^.mp_SigBit;<br />
<br />
//* combine them to our final waitmask */<br />
signals := winSig or tickSig or SIGBREAKF_CTRL_C;<br />
</source><br />
<br />
We don't have to set DoRender to true, we'll do that later when we receive our ticks.<br />
<br />
Immediately before our main loop we send the first tick-request:<br />
<br />
<source lang="pascal"><br />
{* <br />
we start with a no-time request so we receive a tick immediately<br />
(we have to set 2 micros because of a bug in timer.device for 1.3) <br />
*}<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 2;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
tickRequestPending := TRUE;<br />
</source><br />
<br />
Instead of using setsignal () we are now waiting properly for the arrival of window messages or timers.device ticks:<br />
<br />
<source lang="pascal"><br />
sig := Wait(signals);<br />
</source><br />
<br />
When we receive a tick signal we send it immediately so that we can be signaled about a 1/25 second later:<br />
<br />
<source lang="pascal"><br />
if (sig and tickSig) <> 0 then<br />
begin<br />
//* our tickRequest signalled us, let's remove it from the replyport */<br />
WaitIO(PIORequest(@rd^.tickRequest));<br />
<br />
if (rd^.run) then<br />
begin<br />
//* if we are running then we immediately request another tick... */<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 1000000 div 25;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
rd^.doRender := TRUE;<br />
end<br />
else<br />
begin<br />
//* ... if not we acknowledge that our tickRequest returned */<br />
tickRequestPending := FALSE;<br />
end;<br />
end;<br />
</source><br />
<br />
Only if we want to leave our main loop we set TickRequestPending to False instead, because we can now safely close the timer.device again.<br />
<br />
We also check whether we want to leave the loop but still have a tick-request pending in which case it must be canceled.:<br />
<br />
<source lang="pascal"><br />
if (not(rd^.run) and tickRequestPending) then<br />
begin<br />
//* We want to leave, but there is still a tick request pending? Let's abort it */<br />
AbortIO(PIORequest(@rd^.tickRequest));<br />
end;<br />
</source><br />
<br />
Now, if we compile and execute engine5.pas, we'll immediately see that our animation is now much more stable and smoother.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== More on timing ==<br />
<br />
Our current implementation has now reached the point where it will provide you with a maximum number of desired frames per second and that any other CPU time can be used by the system. However, the opposite has not yet been taken into account sufficiently: if we are unable to reach the desired frame rate, then our animation will also run slower as it simply advances a fixed amount of frames. This is undesired behavior: the animation will of course continue to stutter in the absence of a frame rate but if for example our rectangle makes a quarter rotation per second then it should always stay that way, whether we run with 50fps or only with 5fps. To accomplish this we still have to consider the factor time when drawing. We also retrieve this from timer.device by sending a corresponding request. Because our first request is already managed by timer.device and waits there until answered we have to create a second request. For this purpose we will fill rd^.timerIO() analogue to the initial first request. We also need to keep track of the time of our last frame. So we're going to expand our RenderEngineData with two entries:<br />
<br />
<source lang="pascal"><br />
getTimeRequest : Ttimerequest;<br />
lastRenderTime : Ttimeval;<br />
</source><br />
<br />
In RunEngine (), we'll initialize them:<br />
<br />
<source lang="pascal"><br />
//* setup our getTime request... */<br />
rd^.getTimeRequest := rd^.timerIO^;<br />
<br />
//* ... get the current time... */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* ... and initialize our lastRenderTime */<br />
rd^.lastRenderTime := rd^.getTimeRequest.tr_time;<br />
</source><br />
<br />
Now we have to rewrite our RenderBackbuffer() function: So far we have stubbornly increased our rectangular coordinates by a 1/32 of the output size per frame but now, instead, we want our coordinates to be incremented every four seconds by the output size. In order to make this as precise as possible we do not declare the current position as a word but as a floating point number.<br />
<br />
While FLOAT is a predefined type for c, it is not for Free Pascal so for consistency we declared a new FLOAT type first:<br />
<br />
<source lang="pascal"><br />
Type<br />
FLOAT = single;<br />
</source><br />
<br />
And define currentStep to be of type FLOAT:<br />
<br />
<source lang="pascal"><br />
currentStep : FLOAT;<br />
</source><br />
<br />
CurrentStep will now save the current position as a value between 0.0 and 1.0, where 0.0 will stand for the minimum coordinate and 1.0 for the maximum coordinate.<br />
<br />
To calculate this value, we need to determine the elapsed time per call to RenderBackbuffer() since the last frame. We do this by retrieving the current time and subtract the time of our last frame:<br />
<br />
<source lang="pascal"><br />
diff : Ttimeval;<br />
<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* get the time passed since our last render call */<br />
diff := rd^.getTimeRequest.tr_time;<br />
SubTime(@diff, @rd^.lastRenderTime);<br />
</source><br />
<br />
Diff now contains the difference in timeval format. This is a bit clumsy for our purposes, we'd rather have it as a floating point number in seconds: <br />
<br />
<source lang="pascal"><br />
secondsPassed : FLOAT;<br />
micros : ULONG;<br />
</source><br />
<br />
Therefor we also need to divide the complete number of microseconds in diff by 1000000.0:<br />
<br />
<source lang="pascal"><br />
if (diff.tv_secs <> 0) then<br />
begin<br />
micros := diff.tv_secs * 1000000;<br />
end<br />
else<br />
begin<br />
micros := 0;<br />
end;<br />
micros := micros + diff.tv_micro;<br />
secondsPassed := FLOAT(micros) / 1000000.0;<br />
</source><br />
<br />
Now we just need to increase currentStep by a quarter of secondsPassed so that we can reach our maximum value of 1.0 every four seconds. Once we have achieved this, we must deduct the absolute value from it. Because in practice we only expect an absolute value of 1.0, we do this with a simple subtraction. The while loop serves only as a safety net, just in case we somehow manage to get a value over 2.0; Normally we will only go through them once:<br />
<br />
<source lang="pascal"><br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
<br />
while (rd^.currentStep >= 1.0) do<br />
begin<br />
rd^.currentStep := rd^.currentStep - 1.0;<br />
end;<br />
</source><br />
<br />
Then we just have to insert the product from our maximum position and currentStep as current position:<br />
<br />
<source lang="pascal"><br />
pos.x := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.x)));<br />
pos.y := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.y)));<br />
</source><br />
<br />
If we start the executable then we can see a smooth rotating rectangle. Now if we would increase the system load (for example, by starting Engine6 several times) then we see that the animation starts to become choppy, but the rectangle rotates at the same speed.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Refactoring ==<br />
<br />
Actually, we're done now: We have an animation that is controlled by timer.device and which does not get disturbed even under full load. However, the different functions are very entangled. We want to separate those so that we are able to use the engine regardless of what we want to (re-)render. We also want to add a few smaller tweaks.<br />
<br />
So we want to isolate the renderer from our current code. This consists mainly of code for the drawing operations in RenderBackbuffer(), but also lastRenderTime and its logic belongs to it. Our renderer should also be able to determine the desired frame rate.<br />
<br />
The above makes it clear that our renderer at least consist of three functions:<br />
<br />
* InitRenderer() to create it and set the frame rate<br />
* RenderFrame() to draw a frame and<br />
* DestroyRenderer() to destroy the renderer again.<br />
<br />
We also want to make sure that both InitRenderer() and RenderFrame() can fail, so we use our integer return value that's proven to work. At DestroyRenderer() Nothing can go wrong (we cannot do any meaningful error handling here anyway), so this procedure remains:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(): integer;<br />
function RenderFrame(): integer;<br />
procedure DestroyRenderer();<br />
</source><br />
<br />
Naturally the renderer must also be able to store its own data during its lifetime. Therefor we enable this functionality and store the information inside the init function by passing it as a var pointer to the userdata and retrieve this information at RenderFrame() and DestroyRenderer() by passing the userdata as parameter:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer): integer;<br />
function RenderFrame(userData: pointer: integer;<br />
procedure DestroyRenderer(userData: pointer);<br />
</source><br />
<br />
It is useful to define a structure for the renderer data, which is then initialized in InitRenderer():<br />
<br />
<source lang="pascal"><br />
type<br />
PRendererData = ^TRendererData;<br />
TRendererData = <br />
record<br />
end;<br />
<br />
function InitRenderer(var userdata: pointer): integer;<br />
begin<br />
userData := AllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source> <br />
<br />
... and accordingly freed in DestroyRenderer():<br />
<br />
<source lang="pascal"><br />
procedure DestroyRenderer(userData: pointer);<br />
begin<br />
ExecFreeMem(userData, sizeof(TRendererData));<br />
end;<br />
</source><br />
<br />
In this renderer structure we copy our data from RenderEngineData too:<br />
<br />
<source lang="pascal"><br />
TRendererData = record<br />
lastRenderTime : Ttimeval;<br />
currentStep : FLOAT;<br />
end;<br />
</source><br />
<br />
To initialize this structure, we also need the current system time. And because we also want to set the framerate, our InitRenderer() looks like this:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer; const sysTime: Ptimeval; refreshRate: Ptimeval): integer;<br />
var<br />
rd : PRendererData;<br />
begin<br />
//* allocate our user data */<br />
userData := ExecAllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
//* set our lastRenderTime to now */<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
//* we would like to get a refresh rate of 25 frames per second */<br />
refreshRate^.tv_secs := 0;<br />
refreshRate^.tv_micro := 1000000 div 25;<br />
<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source><br />
<br />
In RenderFrame we use our previous used drawing operations. We are also tinkering with an auxiliary function to convert the difference between the two timeval structures in seconds as a float:<br />
<br />
<source lang="pascal"><br />
function DiffInSeconds(const early: Ptimeval; const late: Ptimeval): FLOAT;<br />
var<br />
diff : Ttimeval;<br />
micros : ULONG;<br />
begin<br />
diff := late^;<br />
SubTime(@diff, Ptimeval(early));<br />
<br />
if (diff.tv_secs <> 0)<br />
then micros := diff.tv_secs * 1000000<br />
else micros := 0;<br />
micros := micros + diff.tv_micro;<br />
<br />
result := FLOAT(micros) / 1000000.0;<br />
end;<br />
</source><br />
<br />
RenderFrame() looks a bit more tidy:<br />
<br />
check this function, it is the wrong code as it was taken from engine7.pas while the workshop is still preparing things<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; const sysTime: Ptimeval): integer;<br />
var<br />
secondsPassed : FLOAT;<br />
pos : TPoint;<br />
maxPos : TPoint;<br />
rd : PRendererData;<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
secondsPassed := DiffInSeconds(@rd^.lastRenderTime, sysTime);<br />
<br />
rd^.maxPos.x := renderTargetSize^.x - 1;<br />
rd^.maxPos.y := renderTargetSize^.y - 1;<br />
<br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
while (currentStep >= 1.0) do<br />
begin<br />
currentStep := currentStep - 1.0;<br />
end;<br />
<br />
//* now compute our new position */<br />
pos.x := SmallInt(trunc(rd^.currentStep * maxPosX));<br />
pos.y := SmallInt(trunc(rd^.currentStep * maxPosY));<br />
<br />
//* clear our bitmap */<br />
SetRast(renderTarget, 0);<br />
<br />
//* draw our rectangle */<br />
SetAPen(renderTarget, 1);<br />
GfxMove(renderTarget, 0 , LongInt(pos.y) );<br />
Draw(renderTarget , LongInt(maxPos.x - pos.x), 0 );<br />
Draw(renderTarget , LongInt(maxPos.x) , LongInt(maxPos.y - pos.y) );<br />
Draw(renderTarget , LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(renderTarget , 0 , LongInt(pos.y) );<br />
<br />
//* remember our render time */<br />
<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
Now we just have to make sure that these functions are called in the engine at the appropriate places. Because we also have to retrieve the current system time on multiple occasions, we write a separate function for it:<br />
<br />
<source lang="pascal"><br />
procedure UpdateTime(rd: PRenderEngineData);<br />
begin<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
end;<br />
</source><br />
<br />
If necessary, we read the system time from our getTimeRequest.<br />
<br />
In our RenderEngineData we also keep track of the desired refresh time and the UserData pointer for the renderer:<br />
<br />
<source lang="pascal"><br />
refreshRate : Ttimeval;<br />
userData : pointer;<br />
</source><br />
<br />
And we place the InitRenderer() call in RunEngine() immediately before opening our window when jumping into MainLoop():<br />
<br />
<source lang="pascal"><br />
//* get the current time... */<br />
UpdateTime(rd);<br />
<br />
//* ... and initialize our Renderer */<br />
result := InitRenderer(rd^.userData,<br />
@rd^.getTimeRequest.tr_time,<br />
@rd^.refreshRate);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
[...] //* open window and do MainLoop */<br />
<br />
DestroyRenderer(rd^.userData);<br />
end;<br />
</source><br />
<br />
RenderFrame() is then then simply called in RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize,<br />
@rd^.getTimeRequest.tr_time<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Directly before the call to RenderFrame() we obtain the current time and pass it to RenderFrame().<br />
<br />
This means that we have completely refactored our renderer logic into three functions. In a real program you could now <br />
conveniently place the remaining engine into a separate unit. As part of this workshop, however, we do not want to have to deal with setting up a project or makefile at this time (red: Free Pascal does not require makefiles and/or projects setup (that is, if you do not wish to do so), so it's very easy to store the engine into a separate unit, as long as the compiler is able to locate this unit (which is no problem if a unit is located at the same location as where the main program file is stored).<br />
<br />
Instead, there is still one more thing that is bothering us: it may very well be that a call to RenderFrame determines that it does not actually has to render anything because the contents of the frame hasn't changed since the last call. However, we need to tell whether the last frame is still present in the backbuffer (e.g. because in the meantime the back buffer had to be recreated), otherwise the frame always have to be redrawn. To accomplish this, we expand RenderFrame with two parameters:<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; renderTargetDirty: boolean; const sysTime: Ptimeval; var updateDone: Boolean): integer;<br />
</source><br />
<br />
So RenderTargetDirty lets the renderer know whether the last frame is still present in the renderTarget. UpdateDone informs the caller whether or not the renderer actually drew a frame in renderTarget.<br />
<br />
To determine whether the backbuffer always has to be redrawn or if the previous frame is still intact, our PrepareBackBuffer function can be used. Therefor we also need to expand this function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData; var backBufferDirty: boolean): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
[Allocate new bitmap code snippet...]<br />
<br />
backBufferDirty := TRUE;<br />
end<br />
else<br />
begin<br />
backBufferDirty := FALSE;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
So we set BackBufferDirty to true as soon as we had to create our bitmap again.<br />
<br />
Now we put these two function together in RenderBackBuffer() and return the DoRepaint of RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData; var doRepaint: boolean): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize, backBufferDirty,<br />
@rd^.getTimeRequest.tr_time,<br />
doRepaint<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Now all we have to do is update the rendercall in MainLoop():<br />
<br />
<source lang="pascal"><br />
var<br />
doRepaint: boolean;<br />
<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd, doRepaint);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, set repaint if required */<br />
if not(rd^.doRepaint) then<br />
begin<br />
rd^.doRepaint := doRepaint;<br />
end;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
end;<br />
</source><br />
<br />
It should be noted that we do not overwrite an already set RD^.DoRepaint with a false value.<br />
<br />
<br />
We have now reached our first goal: we have a window in which we can draw using double-buffering and were we can even change the frame rate which can accurately be controlled by timer.device.<br />
<br />
Engine7.pas is compiled with this call:<br />
<br />
<source lang="pascal"><br />
fpc engine7.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=874Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-24T21:49:09Z<p>Molly: typo immediatly -> immediately</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
[[File:HighSpeed Pascal 1.10.jpg|thumb|right|175px|HighSpeed Pascal 1.10]]<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
[[File:FPClogogif.gif|thumb|right|175px|Free Pascal]]<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
[...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Our image learns to walk ==<br />
<br />
Now we want to animate our image by drawing only one iteration of our loop in RenderBackbuffer() per frame. To accomplish this we store the current counter in our RenderEngineData structure.<br />
<br />
<source lang="pascal"><br />
currentStep : integer;<br />
</source><br />
<br />
The RenderBackbuffer() function is modified so that it only uses the current value for four lines per call, and then increments the value by one:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* setup our maximum coordinates and our step width */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
//* compute our current coordinates */<br />
pos.x := rd^.currentStep * lineStep.x;<br />
pos.y := rd^.currentStep * lineStep.y;<br />
<br />
//* increase our step for the next frame */<br />
rd^.currentStep := rd^.currentStep + 1;<br />
if (rd^.currentStep >= stepCount) then<br />
begin<br />
rd^.currentStep := 0;<br />
end;<br />
<br />
//* now draw our line pattern */<br />
SetAPen(rastPort, 1);<br />
GfxMove(rastPort, 0, SLONG(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0 );<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(rastPort, 0 , LongInt(pos.y) );<br />
end;<br />
end;<br />
</source><br />
<br />
As soon as currentStep becomes greater or equal to StepCount, we will have to reset the value back to 0 again.<br />
<br />
The only thing left is to make sure that our RenderBackbuffer () is called regularly. In order to accomplish this we ignore the DoRender flag inside our loop and simply always draw in this situation.<br />
<br />
The Wait() is replaced by a setsignal() and allows us to query if a message from the windows was received but without the need to wait for such a message to arrive.<br />
<br />
<source lang="pascal"><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
signals : ULONG;<br />
<br />
sig : ULONG;<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* combine it with the CTRL-C signal */<br />
signals := winSig or SIGBREAKF_CTRL_C;<br />
<br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
sig := SetSignal(0, signals);<br />
<br />
if (sig and winSig) <> 0 then<br />
begin<br />
//* our window signaled us, so let's harvest all its messages in a loop... */<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
<br />
if (sig and SIGBREAKF_CTRL_C) <> 0 then<br />
begin<br />
//* we leave on CTRL-C */<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
result := rd^.returnCode;<br />
end;<br />
</source><br />
<br />
Here is the source engine4.pas, that can be compiled again with<br />
<br />
<source lang="pascal"><br />
fpc engine4.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== What's timer.device got to do with it ==<br />
<br />
As expected, our window now shows an animation. However, in practice this implementation is far from ideal: On the one hand the speed of our animation directly dependents on the computing and graphical performance of the Amiga on which it runs and, on the other hand it will consume all the processor time that is available. So we need a different solution here. We could simply add a call to Delay() which at the least would sort out the problem with consuming CPU speed. However, if we want to display an animation with a single image every 2 seconds, it would cause the window to be blocked during those two seconds and as a result will respond very sluggish when clicking on the close button or other user actions. We would also need to take the time it takes to render and display into account and for that a simple Delay() is not suitable because of its 1/50-second resolution. We need another timer and timer.device provides one that is perfectly suited for our purposes.<br />
<br />
Like any device, timer.device is also controlled by exec functions OpenDevice()/CloseDevice() and SendIO()/WaitIO()/DoIO()/AbortIO(). Additionally timer.device has a small set of functions that are called in a similar way as functions from a library. These functions are declared inside unit timer and require an initialized base pointer as for any library.<br />
<br />
Note that unit timer already provides a variable named TimerBase for this purpose.<br />
<br />
As it should be for any device, also timer.device is controlled by sending IORequests. These IORequests are generated by us and not by the device itself and in order to send these back and forth we also need a MsgPort. Timer.device also defines its own IORequest structure which is a structure named timerequest and is located in unit timer. In addition to the IORequest, there is also a structure TTimeval defined which supports timing with a resolution of microseconds (1/1000000 seconds).<br />
<br />
Therefor we're going to expand our RenderEngineData structure, this time with a MsgPort and a timerequest:<br />
<br />
<source lang="pascal"><br />
timerPort : PMsgPort;<br />
timerIO : Ptimerequest;<br />
</source><br />
<br />
We also need to make sure the following two units are in our uses clause:<br />
<br />
<source lang="pascal"><br />
Uses<br />
Exec, Timer;<br />
</source><br />
<br />
We create our own function to open timer.device:<br />
<br />
<source lang="pascal"><br />
function InitTimerDevice(rd: PRenderEngineData): integer;<br />
begin<br />
//* we do not return success until we've opened the timer.device */<br />
result := RETURN_FAIL;<br />
<br />
//* create a message port through which we will communicate with the timer.device */<br />
rd^.timerPort := CreatePort(nil, 0);<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
//* create a timerequest which we will we pass between the timer.device and ourself */<br />
rd^.timerIO := Ptimerequest(CreateExtIO(rd^.timerPort, sizeof(Ttimerequest)));<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
//* open the timer.device */<br />
if (OpenDevice(TIMERNAME, UNIT_MICROHZ, PIORequest(rd^.timerIO), 0) = 0) then<br />
begin<br />
//* Success: let's set the TimerBase so we can call timer.device's functions */<br />
TimerBase := PLibrary(rd^.timerIO^.tr_node.io_Device);<br />
result := RETURN_OK;<br />
end;<br />
end;<br />
end;<br />
<br />
if (result <> RETURN_OK) then<br />
begin<br />
//* in case of an error: cleanup immediately */<br />
FreeTimerDevice(rd);<br />
end;<br />
end;<br />
</source><br />
<br />
And a corresponding function FreeTimerDevice():<br />
<br />
<source lang="pascal"><br />
procedure FreeTimerDevice(rd: PRenderEngineData);<br />
begin<br />
//* close the timer.device */<br />
if Assigned(TimerBase) then<br />
begin<br />
CloseDevice(PIORequest(rd^.timerIO));<br />
TimerBase := nil;<br />
end;<br />
<br />
//* free our timerequest */<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
DeleteExtIO(PIORequest(rd^.timerIO));<br />
rd^.timerIO := nil;<br />
end;<br />
<br />
//* free our message port */<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
DeletePort(rd^.timerPort);<br />
rd^.timerPort := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
An additional short explanation for UNIT_MICROHZ used above: Timer.device offers several different modes, which mainly differ in their used resolution and accuracy. The mode we use here is characteristic for its high resolution. However it is also quite inaccurate: If you use it to count seconds then you'll notice that it will deviate from a price clock after a few minutes. However, this shortcoming is not important for our purpose.<br />
<br />
Anyhow, we call these functions immediately after the creation of our RenderEngineData and directly before its release respectively. Because we will send our timerequests asynchronously they are therefor not available for us at timer.device operations. Therefor we do not use our freshly created timerrequest but use it as a template only in order to retrieve the actual used request. For now we only need one for our timer, which we will also add to the RenderEngineData structure:<br />
<br />
<source lang="pascal"><br />
tickRequest : Ttimerequest;<br />
</source><br />
<br />
We initialize this field by simply assigning the contents of TimerIO to it:<br />
<br />
<source lang="pascal"><br />
rd^.tickRequest := rd^.timerIO^;<br />
</source><br />
<br />
Finally our RunEngine() that now looks like this:<br />
<br />
<source lang="pascal"><br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
//* allocate the memory for our runtime data and initialize it with zeros */<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
result := InitTimerDevice(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
//* setup our tick request */<br />
rd^.tickRequest := rd^.timerIO^;<br />
rd^.tickRequest.tr_node.io_Command := TR_ADDREQUEST;<br />
<br />
//* now let's open our window */<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
FreeTimerDevice(rd);<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
Inside Mainloop(), because we cannot leave our loop before a request is answered by timer.device, we have to remember whether or not the tickRequest is just waiting or not. We do this by using a simple boolean:<br />
<br />
<source lang="pascal"><br />
tickRequestPending : Boolean;<br />
</source><br />
<br />
We also need to expand our wait mask and include the TimerPort:<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the timer port */<br />
tickSig := 1 shl rd^.timerPort^.mp_SigBit;<br />
<br />
//* combine them to our final waitmask */<br />
signals := winSig or tickSig or SIGBREAKF_CTRL_C;<br />
</source><br />
<br />
We don't have to set DoRender to true, we'll do that later when we receive our ticks.<br />
<br />
Immediately before our main loop we send the first tick-request:<br />
<br />
<source lang="pascal"><br />
{* <br />
we start with a no-time request so we receive a tick immediately<br />
(we have to set 2 micros because of a bug in timer.device for 1.3) <br />
*}<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 2;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
tickRequestPending := TRUE;<br />
</source><br />
<br />
Instead of using setsignal () we are now waiting properly for the arrival of window messages or timers.device ticks:<br />
<br />
<source lang="pascal"><br />
sig := Wait(signals);<br />
</source><br />
<br />
When we receive a tick signal we send it immediately so that we can be signaled about a 1/25 second later:<br />
<br />
<source lang="pascal"><br />
if (sig and tickSig) <> 0 then<br />
begin<br />
//* our tickRequest signalled us, let's remove it from the replyport */<br />
WaitIO(PIORequest(@rd^.tickRequest));<br />
<br />
if (rd^.run) then<br />
begin<br />
//* if we are running then we immediately request another tick... */<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 1000000 div 25;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
rd^.doRender := TRUE;<br />
end<br />
else<br />
begin<br />
//* ... if not we acknowledge that our tickRequest returned */<br />
tickRequestPending := FALSE;<br />
end;<br />
end;<br />
</source><br />
<br />
Only if we want to leave our main loop we set TickRequestPending to False instead, because we can now safely close the timer.device again.<br />
<br />
We also check whether we want to leave the loop but still have a tick-request pending in which case it must be canceled.:<br />
<br />
<source lang="pascal"><br />
if (not(rd^.run) and tickRequestPending) then<br />
begin<br />
//* We want to leave, but there is still a tick request pending? Let's abort it */<br />
AbortIO(PIORequest(@rd^.tickRequest));<br />
end;<br />
</source><br />
<br />
Now, if we compile and execute engine5.pas, we'll immediately see that our animation is now much more stable and smoother.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== More on timing ==<br />
<br />
Our current implementation has now reached the point where it will provide you with a maximum number of desired frames per second and that any other CPU time can be used by the system. However, the opposite has not yet been taken into account sufficiently: if we are unable to reach the desired frame rate, then our animation will also run slower as it simply advances a fixed amount of frames. This is undesired behavior: the animation will of course continue to stutter in the absence of a frame rate but if for example our rectangle makes a quarter rotation per second then it should always stay that way, whether we run with 50fps or only with 5fps. To accomplish this we still have to consider the factor time when drawing. We also retrieve this from timer.device by sending a corresponding request. Because our first request is already managed by timer.device and waits there until answered we have to create a second request. For this purpose we will fill rd^.timerIO() analogue to the initial first request. We also need to keep track of the time of our last frame. So we're going to expand our RenderEngineData with two entries:<br />
<br />
<source lang="pascal"><br />
getTimeRequest : Ttimerequest;<br />
lastRenderTime : Ttimeval;<br />
</source><br />
<br />
In RunEngine (), we'll initialize them:<br />
<br />
<source lang="pascal"><br />
//* setup our getTime request... */<br />
rd^.getTimeRequest := rd^.timerIO^;<br />
<br />
//* ... get the current time... */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* ... and initialize our lastRenderTime */<br />
rd^.lastRenderTime := rd^.getTimeRequest.tr_time;<br />
</source><br />
<br />
Now we have to rewrite our RenderBackbuffer() function: So far we have stubbornly increased our rectangular coordinates by a 1/32 of the output size per frame but now, instead, we want our coordinates to be incremented every four seconds by the output size. In order to make this as precise as possible we do not declare the current position as a word but as a floating point number.<br />
<br />
While FLOAT is a predefined type for c, it is not for Free Pascal so for consistency we declared a new FLOAT type first:<br />
<br />
<source lang="pascal"><br />
Type<br />
FLOAT = single;<br />
</source><br />
<br />
And define currentStep to be of type FLOAT:<br />
<br />
<source lang="pascal"><br />
currentStep : FLOAT;<br />
</source><br />
<br />
CurrentStep will now save the current position as a value between 0.0 and 1.0, where 0.0 will stand for the minimum coordinate and 1.0 for the maximum coordinate.<br />
<br />
To calculate this value, we need to determine the elapsed time per call to RenderBackbuffer() since the last frame. We do this by retrieving the current time and subtract the time of our last frame:<br />
<br />
<source lang="pascal"><br />
diff : Ttimeval;<br />
<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* get the time passed since our last render call */<br />
diff := rd^.getTimeRequest.tr_time;<br />
SubTime(@diff, @rd^.lastRenderTime);<br />
</source><br />
<br />
Diff now contains the difference in timeval format. This is a bit clumsy for our purposes, we'd rather have it as a floating point number in seconds: <br />
<br />
<source lang="pascal"><br />
secondsPassed : FLOAT;<br />
micros : ULONG;<br />
</source><br />
<br />
Therefor we also need to divide the complete number of microseconds in diff by 1000000.0:<br />
<br />
<source lang="pascal"><br />
if (diff.tv_secs <> 0) then<br />
begin<br />
micros := diff.tv_secs * 1000000;<br />
end<br />
else<br />
begin<br />
micros := 0;<br />
end;<br />
micros := micros + diff.tv_micro;<br />
secondsPassed := FLOAT(micros) / 1000000.0;<br />
</source><br />
<br />
Now we just need to increase currentStep by a quarter of secondsPassed so that we can reach our maximum value of 1.0 every four seconds. Once we have achieved this, we must deduct the absolute value from it. Because in practice we only expect an absolute value of 1.0, we do this with a simple subtraction. The while loop serves only as a safety net, just in case we somehow manage to get a value over 2.0; Normally we will only go through them once:<br />
<br />
<source lang="pascal"><br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
<br />
while (rd^.currentStep >= 1.0) do<br />
begin<br />
rd^.currentStep := rd^.currentStep - 1.0;<br />
end;<br />
</source><br />
<br />
Then we just have to insert the product from our maximum position and currentStep as current position:<br />
<br />
<source lang="pascal"><br />
pos.x := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.x)));<br />
pos.y := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.y)));<br />
</source><br />
<br />
If we start the executable then we can see a smooth rotating rectangle. Now if we would increase the system load (for example, by starting Engine6 several times) then we see that the animation starts to become choppy, but the rectangle rotates at the same speed.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Refactoring ==<br />
<br />
Actually, we're done now: We have an animation that is controlled by timer.device and which does not get disturbed even under full load. However, the different functions are very entangled. We want to separate those so that we are able to use the engine regardless of what we want to (re-)render. We also want to add a few smaller tweaks.<br />
<br />
So we want to isolate the renderer from our current code. This consists mainly of code for the drawing operations in RenderBackbuffer(), but also lastRenderTime and its logic belongs to it. Our renderer should also be able to determine the desired frame rate.<br />
<br />
The above makes it clear that our renderer at least consist of three functions:<br />
<br />
* InitRenderer() to create it and set the frame rate<br />
* RenderFrame() to draw a frame and<br />
* DestroyRenderer() to destroy the renderer again.<br />
<br />
We also want to make sure that both InitRenderer() and RenderFrame() can fail, so we use our integer return value that's proven to work. At DestroyRenderer() Nothing can go wrong (we cannot do any meaningful error handling here anyway), so this procedure remains:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(): integer;<br />
function RenderFrame(): integer;<br />
procedure DestroyRenderer();<br />
</source><br />
<br />
Naturally the renderer must also be able to store its own data during its lifetime. Therefor we enable this functionality and store the information inside the init function by passing it as a var pointer to the userdata and retrieve this information at RenderFrame() and DestroyRenderer() by passing the userdata as parameter:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer): integer;<br />
function RenderFrame(userData: pointer: integer;<br />
procedure DestroyRenderer(userData: pointer);<br />
</source><br />
<br />
It is useful to define a structure for the renderer data, which is then initialized in InitRenderer():<br />
<br />
<source lang="pascal"><br />
type<br />
PRendererData = ^TRendererData;<br />
TRendererData = <br />
record<br />
end;<br />
<br />
function InitRenderer(var userdata: pointer): integer;<br />
begin<br />
userData := AllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source> <br />
<br />
... and accordingly freed in DestroyRenderer():<br />
<br />
<source lang="pascal"><br />
procedure DestroyRenderer(userData: pointer);<br />
begin<br />
ExecFreeMem(userData, sizeof(TRendererData));<br />
end;<br />
</source><br />
<br />
In this renderer structure we copy our data from RenderEngineData too:<br />
<br />
<source lang="pascal"><br />
TRendererData = record<br />
lastRenderTime : Ttimeval;<br />
currentStep : FLOAT;<br />
end;<br />
</source><br />
<br />
To initialize this structure, we also need the current system time. And because we also want to set the framerate, our InitRenderer() looks like this:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer; const sysTime: Ptimeval; refreshRate: Ptimeval): integer;<br />
var<br />
rd : PRendererData;<br />
begin<br />
//* allocate our user data */<br />
userData := ExecAllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
//* set our lastRenderTime to now */<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
//* we would like to get a refresh rate of 25 frames per second */<br />
refreshRate^.tv_secs := 0;<br />
refreshRate^.tv_micro := 1000000 div 25;<br />
<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source><br />
<br />
In RenderFrame we use our previous used drawing operations. We are also tinkering with an auxiliary function to convert the difference between the two timeval structures in seconds as a float:<br />
<br />
<source lang="pascal"><br />
function DiffInSeconds(const early: Ptimeval; const late: Ptimeval): FLOAT;<br />
var<br />
diff : Ttimeval;<br />
micros : ULONG;<br />
begin<br />
diff := late^;<br />
SubTime(@diff, Ptimeval(early));<br />
<br />
if (diff.tv_secs <> 0)<br />
then micros := diff.tv_secs * 1000000<br />
else micros := 0;<br />
micros := micros + diff.tv_micro;<br />
<br />
result := FLOAT(micros) / 1000000.0;<br />
end;<br />
</source><br />
<br />
RenderFrame() looks a bit more tidy:<br />
<br />
check this function, it is the wrong code as it was taken from engine7.pas while the workshop is still preparing things<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; const sysTime: Ptimeval): integer;<br />
var<br />
secondsPassed : FLOAT;<br />
pos : TPoint;<br />
maxPos : TPoint;<br />
rd : PRendererData;<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
secondsPassed := DiffInSeconds(@rd^.lastRenderTime, sysTime);<br />
<br />
rd^.maxPos.x := renderTargetSize^.x - 1;<br />
rd^.maxPos.y := renderTargetSize^.y - 1;<br />
<br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
while (currentStep >= 1.0) do<br />
begin<br />
currentStep := currentStep - 1.0;<br />
end;<br />
<br />
//* now compute our new position */<br />
pos.x := SmallInt(trunc(rd^.currentStep * maxPosX));<br />
pos.y := SmallInt(trunc(rd^.currentStep * maxPosY));<br />
<br />
//* clear our bitmap */<br />
SetRast(renderTarget, 0);<br />
<br />
//* draw our rectangle */<br />
SetAPen(renderTarget, 1);<br />
GfxMove(renderTarget, 0 , LongInt(pos.y) );<br />
Draw(renderTarget , LongInt(maxPos.x - pos.x), 0 );<br />
Draw(renderTarget , LongInt(maxPos.x) , LongInt(maxPos.y - pos.y) );<br />
Draw(renderTarget , LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(renderTarget , 0 , LongInt(pos.y) );<br />
<br />
//* remember our render time */<br />
<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
Now we just have to make sure that these functions are called in the engine at the appropriate places. Because we also have to retrieve the current system time on multiple occasions, we write a separate function for it:<br />
<br />
<source lang="pascal"><br />
procedure UpdateTime(rd: PRenderEngineData);<br />
begin<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
end;<br />
</source><br />
<br />
If necessary, we read the system time from our getTimeRequest.<br />
<br />
In our RenderEngineData we also keep track of the desired refresh time and the UserData pointer for the renderer:<br />
<br />
<source lang="pascal"><br />
refreshRate : Ttimeval;<br />
userData : pointer;<br />
</source><br />
<br />
And we place the InitRenderer() call in RunEngine() immediately before opening our window when jumping into MainLoop():<br />
<br />
<source lang="pascal"><br />
//* get the current time... */<br />
UpdateTime(rd);<br />
<br />
//* ... and initialize our Renderer */<br />
result := InitRenderer(rd^.userData,<br />
@rd^.getTimeRequest.tr_time,<br />
@rd^.refreshRate);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
[...] //* open window and do MainLoop */<br />
<br />
DestroyRenderer(rd^.userData);<br />
end;<br />
</source><br />
<br />
RenderFrame() is then then simply called in RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize,<br />
@rd^.getTimeRequest.tr_time<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Directly before the call to RenderFrame() we obtain the current time and pass it to RenderFrame().<br />
<br />
This means that we have completely refactored our renderer logic into three functions. In a real program you could now <br />
conveniently place the remaining engine into a separate unit. As part of this workshop, however, we do not want to have to deal with setting up a project or makefile at this time (red: Free Pascal does not require makefiles and/or projects setup (that is, if you do not wish to do so), so it's very easy to store the engine into a separate unit, as long as the compiler is able to locate this unit (which is no problem if a unit is located at the same location as where the main program file is stored).<br />
<br />
Instead, there is still one more thing that is bothering us: it may very well be that a call to RenderFrame determines that it does not actually has to render anything because the contents of the frame hasn't changed since the last call. However, we need to tell whether the last frame is still present in the backbuffer (e.g. because in the meantime the back buffer had to be recreated), otherwise the frame always have to be redrawn. To accomplish this, we expand RenderFrame with two parameters:<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; renderTargetDirty: boolean; const sysTime: Ptimeval; var updateDone: Boolean): integer;<br />
</source><br />
<br />
So RenderTargetDirty lets the renderer know whether the last frame is still present in the renderTarget. UpdateDone informs the caller whether or not the renderer actually drew a frame in renderTarget.<br />
<br />
To determine whether the backbuffer always has to be redrawn or if the previous frame is still intact, our PrepareBackBuffer function can be used. Therefor we also need to expand this function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData; var backBufferDirty: boolean): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
[Allocate new bitmap code snippet...]<br />
<br />
backBufferDirty := TRUE;<br />
end<br />
else<br />
begin<br />
backBufferDirty := FALSE;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
So we set BackBufferDirty to true as soon as we had to create our bitmap again.<br />
<br />
Now we put these two function together in RenderBackBuffer() and return the DoRepaint of RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData; var doRepaint: boolean): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize, backBufferDirty,<br />
@rd^.getTimeRequest.tr_time,<br />
doRepaint<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Now all we have to do is update the rendercall in MainLoop():<br />
<br />
<source lang="pascal"><br />
var<br />
doRepaint: boolean;<br />
<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd, doRepaint);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, set repaint if required */<br />
if not(rd^.doRepaint) then<br />
begin<br />
rd^.doRepaint := doRepaint;<br />
end;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
end;<br />
</source><br />
<br />
It should be noted that we do not overwrite an already set RD^.DoRepaint with a false value.<br />
<br />
<br />
We have now reached our first goal: we have a window in which we can draw using double-buffering and were we can even change the frame rate which can accurately be controlled by timer.device.<br />
<br />
Engine7.pas is compiled with this call:<br />
<br />
<source lang="pascal"><br />
fpc engine7.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=873Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-24T21:14:47Z<p>Molly: /* Free Pascal */ Add picture</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
[[File:HighSpeed Pascal 1.10.jpg|thumb|right|175px|HighSpeed Pascal 1.10]]<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
[[File:FPClogogif.gif|thumb|right|175px|Free Pascal]]<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
[...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Our image learns to walk ==<br />
<br />
Now we want to animate our image by drawing only one iteration of our loop in RenderBackbuffer() per frame. To accomplish this we store the current counter in our RenderEngineData structure.<br />
<br />
<source lang="pascal"><br />
currentStep : integer;<br />
</source><br />
<br />
The RenderBackbuffer() function is modified so that it only uses the current value for four lines per call, and then increments the value by one:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* setup our maximum coordinates and our step width */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
//* compute our current coordinates */<br />
pos.x := rd^.currentStep * lineStep.x;<br />
pos.y := rd^.currentStep * lineStep.y;<br />
<br />
//* increase our step for the next frame */<br />
rd^.currentStep := rd^.currentStep + 1;<br />
if (rd^.currentStep >= stepCount) then<br />
begin<br />
rd^.currentStep := 0;<br />
end;<br />
<br />
//* now draw our line pattern */<br />
SetAPen(rastPort, 1);<br />
GfxMove(rastPort, 0, SLONG(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0 );<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(rastPort, 0 , LongInt(pos.y) );<br />
end;<br />
end;<br />
</source><br />
<br />
As soon as currentStep becomes greater or equal to StepCount, we will have to reset the value back to 0 again.<br />
<br />
The only thing left is to make sure that our RenderBackbuffer () is called regularly. In order to accomplish this we ignore the DoRender flag inside our loop and simply always draw in this situation.<br />
<br />
The Wait() is replaced by a setsignal() and allows us to query if a message from the windows was received but without the need to wait for such a message to arrive.<br />
<br />
<source lang="pascal"><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
signals : ULONG;<br />
<br />
sig : ULONG;<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* combine it with the CTRL-C signal */<br />
signals := winSig or SIGBREAKF_CTRL_C;<br />
<br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
sig := SetSignal(0, signals);<br />
<br />
if (sig and winSig) <> 0 then<br />
begin<br />
//* our window signaled us, so let's harvest all its messages in a loop... */<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
<br />
if (sig and SIGBREAKF_CTRL_C) <> 0 then<br />
begin<br />
//* we leave on CTRL-C */<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
result := rd^.returnCode;<br />
end;<br />
</source><br />
<br />
Here is the source engine4.pas, that can be compiled again with<br />
<br />
<source lang="pascal"><br />
fpc engine4.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== What's timer.device got to do with it ==<br />
<br />
As expected, our window now shows an animation. However, in practice this implementation is far from ideal: On the one hand the speed of our animation directly dependents on the computing and graphical performance of the Amiga on which it runs and, on the other hand it will consume all the processor time that is available. So we need a different solution here. We could simply add a call to Delay() which at the least would sort out the problem with consuming CPU speed. However, if we want to display an animation with a single image every 2 seconds, it would cause the window to be blocked during those two seconds and as a result will respond very sluggish when clicking on the close button or other user actions. We would also need to take the time it takes to render and display into account and for that a simple Delay() is not suitable because of its 1/50-second resolution. We need another timer and timer.device provides one that is perfectly suited for our purposes.<br />
<br />
Like any device, timer.device is also controlled by exec functions OpenDevice()/CloseDevice() and SendIO()/WaitIO()/DoIO()/AbortIO(). Additionally timer.device has a small set of functions that are called in a similar way as functions from a library. These functions are declared inside unit timer and require an initialized base pointer as for any library.<br />
<br />
Note that unit timer already provides a variable named TimerBase for this purpose.<br />
<br />
As it should be for any device, also timer.device is controlled by sending IORequests. These IORequests are generated by us and not by the device itself and in order to send these back and forth we also need a MsgPort. Timer.device also defines its own IORequest structure which is a structure named timerequest and is located in unit timer. In addition to the IORequest, there is also a structure TTimeval defined which supports timing with a resolution of microseconds (1/1000000 seconds).<br />
<br />
Therefor we're going to expand our RenderEngineData structure, this time with a MsgPort and a timerequest:<br />
<br />
<source lang="pascal"><br />
timerPort : PMsgPort;<br />
timerIO : Ptimerequest;<br />
</source><br />
<br />
We also need to make sure the following two units are in our uses clause:<br />
<br />
<source lang="pascal"><br />
Uses<br />
Exec, Timer;<br />
</source><br />
<br />
We create our own function to open timer.device:<br />
<br />
<source lang="pascal"><br />
function InitTimerDevice(rd: PRenderEngineData): integer;<br />
begin<br />
//* we do not return success until we've opened the timer.device */<br />
result := RETURN_FAIL;<br />
<br />
//* create a message port through which we will communicate with the timer.device */<br />
rd^.timerPort := CreatePort(nil, 0);<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
//* create a timerequest which we will we pass between the timer.device and ourself */<br />
rd^.timerIO := Ptimerequest(CreateExtIO(rd^.timerPort, sizeof(Ttimerequest)));<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
//* open the timer.device */<br />
if (OpenDevice(TIMERNAME, UNIT_MICROHZ, PIORequest(rd^.timerIO), 0) = 0) then<br />
begin<br />
//* Success: let's set the TimerBase so we can call timer.device's functions */<br />
TimerBase := PLibrary(rd^.timerIO^.tr_node.io_Device);<br />
result := RETURN_OK;<br />
end;<br />
end;<br />
end;<br />
<br />
if (result <> RETURN_OK) then<br />
begin<br />
//* in case of an error: cleanup immediatly */<br />
FreeTimerDevice(rd);<br />
end;<br />
end;<br />
</source><br />
<br />
And a corresponding function FreeTimerDevice():<br />
<br />
<source lang="pascal"><br />
procedure FreeTimerDevice(rd: PRenderEngineData);<br />
begin<br />
//* close the timer.device */<br />
if Assigned(TimerBase) then<br />
begin<br />
CloseDevice(PIORequest(rd^.timerIO));<br />
TimerBase := nil;<br />
end;<br />
<br />
//* free our timerequest */<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
DeleteExtIO(PIORequest(rd^.timerIO));<br />
rd^.timerIO := nil;<br />
end;<br />
<br />
//* free our message port */<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
DeletePort(rd^.timerPort);<br />
rd^.timerPort := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
An additional short explanation for UNIT_MICROHZ used above: Timer.device offers several different modes, which mainly differ in their used resolution and accuracy. The mode we use here is characteristic for its high resolution. However it is also quite inaccurate: If you use it to count seconds then you'll notice that it will deviate from a price clock after a few minutes. However, this shortcoming is not important for our purpose.<br />
<br />
Anyhow, we call these functions immediately after the creation of our RenderEngineData and directly before its release respectively. Because we will send our timerequests asynchronously they are therefor not available for us at timer.device operations. Therefor we do not use our freshly created timerrequest but use it as a template only in order to retrieve the actual used request. For now we only need one for our timer, which we will also add to the RenderEngineData structure:<br />
<br />
<source lang="pascal"><br />
tickRequest : Ttimerequest;<br />
</source><br />
<br />
We initialize this field by simply assigning the contents of TimerIO to it:<br />
<br />
<source lang="pascal"><br />
rd^.tickRequest := rd^.timerIO^;<br />
</source><br />
<br />
Finally our RunEngine() that now looks like this:<br />
<br />
<source lang="pascal"><br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
//* allocate the memory for our runtime data and initialize it with zeros */<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
result := InitTimerDevice(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
//* setup our tick request */<br />
rd^.tickRequest := rd^.timerIO^;<br />
rd^.tickRequest.tr_node.io_Command := TR_ADDREQUEST;<br />
<br />
//* now let's open our window */<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
FreeTimerDevice(rd);<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
Inside Mainloop(), because we cannot leave our loop before a request is answered by timer.device, we have to remember whether or not the tickRequest is just waiting or not. We do this by using a simple boolean:<br />
<br />
<source lang="pascal"><br />
tickRequestPending : Boolean;<br />
</source><br />
<br />
We also need to expand our wait mask and include the TimerPort:<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the timer port */<br />
tickSig := 1 shl rd^.timerPort^.mp_SigBit;<br />
<br />
//* combine them to our final waitmask */<br />
signals := winSig or tickSig or SIGBREAKF_CTRL_C;<br />
</source><br />
<br />
We don't have to set DoRender to true, we'll do that later when we receive our ticks.<br />
<br />
Immediately before our main loop we send the first tick-request:<br />
<br />
<source lang="pascal"><br />
{* <br />
we start with a no-time request so we receive a tick immediately<br />
(we have to set 2 micros because of a bug in timer.device for 1.3) <br />
*}<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 2;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
tickRequestPending := TRUE;<br />
</source><br />
<br />
Instead of using setsignal () we are now waiting properly for the arrival of window messages or timers.device ticks:<br />
<br />
<source lang="pascal"><br />
sig := Wait(signals);<br />
</source><br />
<br />
When we receive a tick signal we send it immediately so that we can be signaled about a 1/25 second later:<br />
<br />
<source lang="pascal"><br />
if (sig and tickSig) <> 0 then<br />
begin<br />
//* our tickRequest signalled us, let's remove it from the replyport */<br />
WaitIO(PIORequest(@rd^.tickRequest));<br />
<br />
if (rd^.run) then<br />
begin<br />
//* if we are running then we immediately request another tick... */<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 1000000 div 25;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
rd^.doRender := TRUE;<br />
end<br />
else<br />
begin<br />
//* ... if not we acknowledge that our tickRequest returned */<br />
tickRequestPending := FALSE;<br />
end;<br />
end;<br />
</source><br />
<br />
Only if we want to leave our main loop we set TickRequestPending to False instead, because we can now safely close the timer.device again.<br />
<br />
We also check whether we want to leave the loop but still have a tick-request pending in which case it must be canceled.:<br />
<br />
<source lang="pascal"><br />
if (not(rd^.run) and tickRequestPending) then<br />
begin<br />
//* We want to leave, but there is still a tick request pending? Let's abort it */<br />
AbortIO(PIORequest(@rd^.tickRequest));<br />
end;<br />
</source><br />
<br />
Now, if we compile and execute engine5.pas, we'll immediately see that our animation is now much more stable and smoother.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== More on timing ==<br />
<br />
Our current implementation has now reached the point where it will provide you with a maximum number of desired frames per second and that any other CPU time can be used by the system. However, the opposite has not yet been taken into account sufficiently: if we are unable to reach the desired frame rate, then our animation will also run slower as it simply advances a fixed amount of frames. This is undesired behavior: the animation will of course continue to stutter in the absence of a frame rate but if for example our rectangle makes a quarter rotation per second then it should always stay that way, whether we run with 50fps or only with 5fps. To accomplish this we still have to consider the factor time when drawing. We also retrieve this from timer.device by sending a corresponding request. Because our first request is already managed by timer.device and waits there until answered we have to create a second request. For this purpose we will fill rd^.timerIO() analogue to the initial first request. We also need to keep track of the time of our last frame. So we're going to expand our RenderEngineData with two entries:<br />
<br />
<source lang="pascal"><br />
getTimeRequest : Ttimerequest;<br />
lastRenderTime : Ttimeval;<br />
</source><br />
<br />
In RunEngine (), we'll initialize them:<br />
<br />
<source lang="pascal"><br />
//* setup our getTime request... */<br />
rd^.getTimeRequest := rd^.timerIO^;<br />
<br />
//* ... get the current time... */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* ... and initialize our lastRenderTime */<br />
rd^.lastRenderTime := rd^.getTimeRequest.tr_time;<br />
</source><br />
<br />
Now we have to rewrite our RenderBackbuffer() function: So far we have stubbornly increased our rectangular coordinates by a 1/32 of the output size per frame but now, instead, we want our coordinates to be incremented every four seconds by the output size. In order to make this as precise as possible we do not declare the current position as a word but as a floating point number.<br />
<br />
While FLOAT is a predefined type for c, it is not for Free Pascal so for consistency we declared a new FLOAT type first:<br />
<br />
<source lang="pascal"><br />
Type<br />
FLOAT = single;<br />
</source><br />
<br />
And define currentStep to be of type FLOAT:<br />
<br />
<source lang="pascal"><br />
currentStep : FLOAT;<br />
</source><br />
<br />
CurrentStep will now save the current position as a value between 0.0 and 1.0, where 0.0 will stand for the minimum coordinate and 1.0 for the maximum coordinate.<br />
<br />
To calculate this value, we need to determine the elapsed time per call to RenderBackbuffer() since the last frame. We do this by retrieving the current time and subtract the time of our last frame:<br />
<br />
<source lang="pascal"><br />
diff : Ttimeval;<br />
<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* get the time passed since our last render call */<br />
diff := rd^.getTimeRequest.tr_time;<br />
SubTime(@diff, @rd^.lastRenderTime);<br />
</source><br />
<br />
Diff now contains the difference in timeval format. This is a bit clumsy for our purposes, we'd rather have it as a floating point number in seconds: <br />
<br />
<source lang="pascal"><br />
secondsPassed : FLOAT;<br />
micros : ULONG;<br />
</source><br />
<br />
Therefor we also need to divide the complete number of microseconds in diff by 1000000.0:<br />
<br />
<source lang="pascal"><br />
if (diff.tv_secs <> 0) then<br />
begin<br />
micros := diff.tv_secs * 1000000;<br />
end<br />
else<br />
begin<br />
micros := 0;<br />
end;<br />
micros := micros + diff.tv_micro;<br />
secondsPassed := FLOAT(micros) / 1000000.0;<br />
</source><br />
<br />
Now we just need to increase currentStep by a quarter of secondsPassed so that we can reach our maximum value of 1.0 every four seconds. Once we have achieved this, we must deduct the absolute value from it. Because in practice we only expect an absolute value of 1.0, we do this with a simple subtraction. The while loop serves only as a safety net, just in case we somehow manage to get a value over 2.0; Normally we will only go through them once:<br />
<br />
<source lang="pascal"><br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
<br />
while (rd^.currentStep >= 1.0) do<br />
begin<br />
rd^.currentStep := rd^.currentStep - 1.0;<br />
end;<br />
</source><br />
<br />
Then we just have to insert the product from our maximum position and currentStep as current position:<br />
<br />
<source lang="pascal"><br />
pos.x := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.x)));<br />
pos.y := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.y)));<br />
</source><br />
<br />
If we start the executable then we can see a smooth rotating rectangle. Now if we would increase the system load (for example, by starting Engine6 several times) then we see that the animation starts to become choppy, but the rectangle rotates at the same speed.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Refactoring ==<br />
<br />
Actually, we're done now: We have an animation that is controlled by timer.device and which does not get disturbed even under full load. However, the different functions are very entangled. We want to separate those so that we are able to use the engine regardless of what we want to (re-)render. We also want to add a few smaller tweaks.<br />
<br />
So we want to isolate the renderer from our current code. This consists mainly of code for the drawing operations in RenderBackbuffer(), but also lastRenderTime and its logic belongs to it. Our renderer should also be able to determine the desired frame rate.<br />
<br />
The above makes it clear that our renderer at least consist of three functions:<br />
<br />
* InitRenderer() to create it and set the frame rate<br />
* RenderFrame() to draw a frame and<br />
* DestroyRenderer() to destroy the renderer again.<br />
<br />
We also want to make sure that both InitRenderer() and RenderFrame() can fail, so we use our integer return value that's proven to work. At DestroyRenderer() Nothing can go wrong (we cannot do any meaningful error handling here anyway), so this procedure remains:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(): integer;<br />
function RenderFrame(): integer;<br />
procedure DestroyRenderer();<br />
</source><br />
<br />
Naturally the renderer must also be able to store its own data during its lifetime. Therefor we enable this functionality and store the information inside the init function by passing it as a var pointer to the userdata and retrieve this information at RenderFrame() and DestroyRenderer() by passing the userdata as parameter:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer): integer;<br />
function RenderFrame(userData: pointer: integer;<br />
procedure DestroyRenderer(userData: pointer);<br />
</source><br />
<br />
It is useful to define a structure for the renderer data, which is then initialized in InitRenderer():<br />
<br />
<source lang="pascal"><br />
type<br />
PRendererData = ^TRendererData;<br />
TRendererData = <br />
record<br />
end;<br />
<br />
function InitRenderer(var userdata: pointer): integer;<br />
begin<br />
userData := AllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source> <br />
<br />
... and accordingly freed in DestroyRenderer():<br />
<br />
<source lang="pascal"><br />
procedure DestroyRenderer(userData: pointer);<br />
begin<br />
ExecFreeMem(userData, sizeof(TRendererData));<br />
end;<br />
</source><br />
<br />
In this renderer structure we copy our data from RenderEngineData too:<br />
<br />
<source lang="pascal"><br />
TRendererData = record<br />
lastRenderTime : Ttimeval;<br />
currentStep : FLOAT;<br />
end;<br />
</source><br />
<br />
To initialize this structure, we also need the current system time. And because we also want to set the framerate, our InitRenderer() looks like this:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer; const sysTime: Ptimeval; refreshRate: Ptimeval): integer;<br />
var<br />
rd : PRendererData;<br />
begin<br />
//* allocate our user data */<br />
userData := ExecAllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
//* set our lastRenderTime to now */<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
//* we would like to get a refresh rate of 25 frames per second */<br />
refreshRate^.tv_secs := 0;<br />
refreshRate^.tv_micro := 1000000 div 25;<br />
<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source><br />
<br />
In RenderFrame we use our previous used drawing operations. We are also tinkering with an auxiliary function to convert the difference between the two timeval structures in seconds as a float:<br />
<br />
<source lang="pascal"><br />
function DiffInSeconds(const early: Ptimeval; const late: Ptimeval): FLOAT;<br />
var<br />
diff : Ttimeval;<br />
micros : ULONG;<br />
begin<br />
diff := late^;<br />
SubTime(@diff, Ptimeval(early));<br />
<br />
if (diff.tv_secs <> 0)<br />
then micros := diff.tv_secs * 1000000<br />
else micros := 0;<br />
micros := micros + diff.tv_micro;<br />
<br />
result := FLOAT(micros) / 1000000.0;<br />
end;<br />
</source><br />
<br />
RenderFrame() looks a bit more tidy:<br />
<br />
check this function, it is the wrong code as it was taken from engine7.pas while the workshop is still preparing things<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; const sysTime: Ptimeval): integer;<br />
var<br />
secondsPassed : FLOAT;<br />
pos : TPoint;<br />
maxPos : TPoint;<br />
rd : PRendererData;<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
secondsPassed := DiffInSeconds(@rd^.lastRenderTime, sysTime);<br />
<br />
rd^.maxPos.x := renderTargetSize^.x - 1;<br />
rd^.maxPos.y := renderTargetSize^.y - 1;<br />
<br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
while (currentStep >= 1.0) do<br />
begin<br />
currentStep := currentStep - 1.0;<br />
end;<br />
<br />
//* now compute our new position */<br />
pos.x := SmallInt(trunc(rd^.currentStep * maxPosX));<br />
pos.y := SmallInt(trunc(rd^.currentStep * maxPosY));<br />
<br />
//* clear our bitmap */<br />
SetRast(renderTarget, 0);<br />
<br />
//* draw our rectangle */<br />
SetAPen(renderTarget, 1);<br />
GfxMove(renderTarget, 0 , LongInt(pos.y) );<br />
Draw(renderTarget , LongInt(maxPos.x - pos.x), 0 );<br />
Draw(renderTarget , LongInt(maxPos.x) , LongInt(maxPos.y - pos.y) );<br />
Draw(renderTarget , LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(renderTarget , 0 , LongInt(pos.y) );<br />
<br />
//* remember our render time */<br />
<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
Now we just have to make sure that these functions are called in the engine at the appropriate places. Because we also have to retrieve the current system time on multiple occasions, we write a separate function for it:<br />
<br />
<source lang="pascal"><br />
procedure UpdateTime(rd: PRenderEngineData);<br />
begin<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
end;<br />
</source><br />
<br />
If necessary, we read the system time from our getTimeRequest.<br />
<br />
In our RenderEngineData we also keep track of the desired refresh time and the UserData pointer for the renderer:<br />
<br />
<source lang="pascal"><br />
refreshRate : Ttimeval;<br />
userData : pointer;<br />
</source><br />
<br />
And we place the InitRenderer() call in RunEngine() immediately before opening our window when jumping into MainLoop():<br />
<br />
<source lang="pascal"><br />
//* get the current time... */<br />
UpdateTime(rd);<br />
<br />
//* ... and initialize our Renderer */<br />
result := InitRenderer(rd^.userData,<br />
@rd^.getTimeRequest.tr_time,<br />
@rd^.refreshRate);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
[...] //* open window and do MainLoop */<br />
<br />
DestroyRenderer(rd^.userData);<br />
end;<br />
</source><br />
<br />
RenderFrame() is then then simply called in RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize,<br />
@rd^.getTimeRequest.tr_time<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Directly before the call to RenderFrame() we obtain the current time and pass it to RenderFrame().<br />
<br />
This means that we have completely refactored our renderer logic into three functions. In a real program you could now <br />
conveniently place the remaining engine into a separate unit. As part of this workshop, however, we do not want to have to deal with setting up a project or makefile at this time (red: Free Pascal does not require makefiles and/or projects setup (that is, if you do not wish to do so), so it's very easy to store the engine into a separate unit, as long as the compiler is able to locate this unit (which is no problem if a unit is located at the same location as where the main program file is stored).<br />
<br />
Instead, there is still one more thing that is bothering us: it may very well be that a call to RenderFrame determines that it does not actually has to render anything because the contents of the frame hasn't changed since the last call. However, we need to tell whether the last frame is still present in the backbuffer (e.g. because in the meantime the back buffer had to be recreated), otherwise the frame always have to be redrawn. To accomplish this, we expand RenderFrame with two parameters:<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; renderTargetDirty: boolean; const sysTime: Ptimeval; var updateDone: Boolean): integer;<br />
</source><br />
<br />
So RenderTargetDirty lets the renderer know whether the last frame is still present in the renderTarget. UpdateDone informs the caller whether or not the renderer actually drew a frame in renderTarget.<br />
<br />
To determine whether the backbuffer always has to be redrawn or if the previous frame is still intact, our PrepareBackBuffer function can be used. Therefor we also need to expand this function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData; var backBufferDirty: boolean): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
[Allocate new bitmap code snippet...]<br />
<br />
backBufferDirty := TRUE;<br />
end<br />
else<br />
begin<br />
backBufferDirty := FALSE;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
So we set BackBufferDirty to true as soon as we had to create our bitmap again.<br />
<br />
Now we put these two function together in RenderBackBuffer() and return the DoRepaint of RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData; var doRepaint: boolean): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize, backBufferDirty,<br />
@rd^.getTimeRequest.tr_time,<br />
doRepaint<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Now all we have to do is update the rendercall in MainLoop():<br />
<br />
<source lang="pascal"><br />
var<br />
doRepaint: boolean;<br />
<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd, doRepaint);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, set repaint if required */<br />
if not(rd^.doRepaint) then<br />
begin<br />
rd^.doRepaint := doRepaint;<br />
end;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
end;<br />
</source><br />
<br />
It should be noted that we do not overwrite an already set RD^.DoRepaint with a false value.<br />
<br />
<br />
We have now reached our first goal: we have a window in which we can draw using double-buffering and were we can even change the frame rate which can accurately be controlled by timer.device.<br />
<br />
Engine7.pas is compiled with this call:<br />
<br />
<source lang="pascal"><br />
fpc engine7.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=872Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-24T21:12:23Z<p>Molly: /* HighSpeed Pascal */ Add picture</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
[[File:HighSpeed Pascal 1.10.jpg|thumb|right|175px|HighSpeed Pascal 1.10]]<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
[...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Our image learns to walk ==<br />
<br />
Now we want to animate our image by drawing only one iteration of our loop in RenderBackbuffer() per frame. To accomplish this we store the current counter in our RenderEngineData structure.<br />
<br />
<source lang="pascal"><br />
currentStep : integer;<br />
</source><br />
<br />
The RenderBackbuffer() function is modified so that it only uses the current value for four lines per call, and then increments the value by one:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* setup our maximum coordinates and our step width */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
//* compute our current coordinates */<br />
pos.x := rd^.currentStep * lineStep.x;<br />
pos.y := rd^.currentStep * lineStep.y;<br />
<br />
//* increase our step for the next frame */<br />
rd^.currentStep := rd^.currentStep + 1;<br />
if (rd^.currentStep >= stepCount) then<br />
begin<br />
rd^.currentStep := 0;<br />
end;<br />
<br />
//* now draw our line pattern */<br />
SetAPen(rastPort, 1);<br />
GfxMove(rastPort, 0, SLONG(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0 );<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(rastPort, 0 , LongInt(pos.y) );<br />
end;<br />
end;<br />
</source><br />
<br />
As soon as currentStep becomes greater or equal to StepCount, we will have to reset the value back to 0 again.<br />
<br />
The only thing left is to make sure that our RenderBackbuffer () is called regularly. In order to accomplish this we ignore the DoRender flag inside our loop and simply always draw in this situation.<br />
<br />
The Wait() is replaced by a setsignal() and allows us to query if a message from the windows was received but without the need to wait for such a message to arrive.<br />
<br />
<source lang="pascal"><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
signals : ULONG;<br />
<br />
sig : ULONG;<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* combine it with the CTRL-C signal */<br />
signals := winSig or SIGBREAKF_CTRL_C;<br />
<br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
sig := SetSignal(0, signals);<br />
<br />
if (sig and winSig) <> 0 then<br />
begin<br />
//* our window signaled us, so let's harvest all its messages in a loop... */<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
<br />
if (sig and SIGBREAKF_CTRL_C) <> 0 then<br />
begin<br />
//* we leave on CTRL-C */<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
result := rd^.returnCode;<br />
end;<br />
</source><br />
<br />
Here is the source engine4.pas, that can be compiled again with<br />
<br />
<source lang="pascal"><br />
fpc engine4.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== What's timer.device got to do with it ==<br />
<br />
As expected, our window now shows an animation. However, in practice this implementation is far from ideal: On the one hand the speed of our animation directly dependents on the computing and graphical performance of the Amiga on which it runs and, on the other hand it will consume all the processor time that is available. So we need a different solution here. We could simply add a call to Delay() which at the least would sort out the problem with consuming CPU speed. However, if we want to display an animation with a single image every 2 seconds, it would cause the window to be blocked during those two seconds and as a result will respond very sluggish when clicking on the close button or other user actions. We would also need to take the time it takes to render and display into account and for that a simple Delay() is not suitable because of its 1/50-second resolution. We need another timer and timer.device provides one that is perfectly suited for our purposes.<br />
<br />
Like any device, timer.device is also controlled by exec functions OpenDevice()/CloseDevice() and SendIO()/WaitIO()/DoIO()/AbortIO(). Additionally timer.device has a small set of functions that are called in a similar way as functions from a library. These functions are declared inside unit timer and require an initialized base pointer as for any library.<br />
<br />
Note that unit timer already provides a variable named TimerBase for this purpose.<br />
<br />
As it should be for any device, also timer.device is controlled by sending IORequests. These IORequests are generated by us and not by the device itself and in order to send these back and forth we also need a MsgPort. Timer.device also defines its own IORequest structure which is a structure named timerequest and is located in unit timer. In addition to the IORequest, there is also a structure TTimeval defined which supports timing with a resolution of microseconds (1/1000000 seconds).<br />
<br />
Therefor we're going to expand our RenderEngineData structure, this time with a MsgPort and a timerequest:<br />
<br />
<source lang="pascal"><br />
timerPort : PMsgPort;<br />
timerIO : Ptimerequest;<br />
</source><br />
<br />
We also need to make sure the following two units are in our uses clause:<br />
<br />
<source lang="pascal"><br />
Uses<br />
Exec, Timer;<br />
</source><br />
<br />
We create our own function to open timer.device:<br />
<br />
<source lang="pascal"><br />
function InitTimerDevice(rd: PRenderEngineData): integer;<br />
begin<br />
//* we do not return success until we've opened the timer.device */<br />
result := RETURN_FAIL;<br />
<br />
//* create a message port through which we will communicate with the timer.device */<br />
rd^.timerPort := CreatePort(nil, 0);<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
//* create a timerequest which we will we pass between the timer.device and ourself */<br />
rd^.timerIO := Ptimerequest(CreateExtIO(rd^.timerPort, sizeof(Ttimerequest)));<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
//* open the timer.device */<br />
if (OpenDevice(TIMERNAME, UNIT_MICROHZ, PIORequest(rd^.timerIO), 0) = 0) then<br />
begin<br />
//* Success: let's set the TimerBase so we can call timer.device's functions */<br />
TimerBase := PLibrary(rd^.timerIO^.tr_node.io_Device);<br />
result := RETURN_OK;<br />
end;<br />
end;<br />
end;<br />
<br />
if (result <> RETURN_OK) then<br />
begin<br />
//* in case of an error: cleanup immediatly */<br />
FreeTimerDevice(rd);<br />
end;<br />
end;<br />
</source><br />
<br />
And a corresponding function FreeTimerDevice():<br />
<br />
<source lang="pascal"><br />
procedure FreeTimerDevice(rd: PRenderEngineData);<br />
begin<br />
//* close the timer.device */<br />
if Assigned(TimerBase) then<br />
begin<br />
CloseDevice(PIORequest(rd^.timerIO));<br />
TimerBase := nil;<br />
end;<br />
<br />
//* free our timerequest */<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
DeleteExtIO(PIORequest(rd^.timerIO));<br />
rd^.timerIO := nil;<br />
end;<br />
<br />
//* free our message port */<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
DeletePort(rd^.timerPort);<br />
rd^.timerPort := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
An additional short explanation for UNIT_MICROHZ used above: Timer.device offers several different modes, which mainly differ in their used resolution and accuracy. The mode we use here is characteristic for its high resolution. However it is also quite inaccurate: If you use it to count seconds then you'll notice that it will deviate from a price clock after a few minutes. However, this shortcoming is not important for our purpose.<br />
<br />
Anyhow, we call these functions immediately after the creation of our RenderEngineData and directly before its release respectively. Because we will send our timerequests asynchronously they are therefor not available for us at timer.device operations. Therefor we do not use our freshly created timerrequest but use it as a template only in order to retrieve the actual used request. For now we only need one for our timer, which we will also add to the RenderEngineData structure:<br />
<br />
<source lang="pascal"><br />
tickRequest : Ttimerequest;<br />
</source><br />
<br />
We initialize this field by simply assigning the contents of TimerIO to it:<br />
<br />
<source lang="pascal"><br />
rd^.tickRequest := rd^.timerIO^;<br />
</source><br />
<br />
Finally our RunEngine() that now looks like this:<br />
<br />
<source lang="pascal"><br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
//* allocate the memory for our runtime data and initialize it with zeros */<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
result := InitTimerDevice(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
//* setup our tick request */<br />
rd^.tickRequest := rd^.timerIO^;<br />
rd^.tickRequest.tr_node.io_Command := TR_ADDREQUEST;<br />
<br />
//* now let's open our window */<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
FreeTimerDevice(rd);<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
Inside Mainloop(), because we cannot leave our loop before a request is answered by timer.device, we have to remember whether or not the tickRequest is just waiting or not. We do this by using a simple boolean:<br />
<br />
<source lang="pascal"><br />
tickRequestPending : Boolean;<br />
</source><br />
<br />
We also need to expand our wait mask and include the TimerPort:<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the timer port */<br />
tickSig := 1 shl rd^.timerPort^.mp_SigBit;<br />
<br />
//* combine them to our final waitmask */<br />
signals := winSig or tickSig or SIGBREAKF_CTRL_C;<br />
</source><br />
<br />
We don't have to set DoRender to true, we'll do that later when we receive our ticks.<br />
<br />
Immediately before our main loop we send the first tick-request:<br />
<br />
<source lang="pascal"><br />
{* <br />
we start with a no-time request so we receive a tick immediately<br />
(we have to set 2 micros because of a bug in timer.device for 1.3) <br />
*}<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 2;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
tickRequestPending := TRUE;<br />
</source><br />
<br />
Instead of using setsignal () we are now waiting properly for the arrival of window messages or timers.device ticks:<br />
<br />
<source lang="pascal"><br />
sig := Wait(signals);<br />
</source><br />
<br />
When we receive a tick signal we send it immediately so that we can be signaled about a 1/25 second later:<br />
<br />
<source lang="pascal"><br />
if (sig and tickSig) <> 0 then<br />
begin<br />
//* our tickRequest signalled us, let's remove it from the replyport */<br />
WaitIO(PIORequest(@rd^.tickRequest));<br />
<br />
if (rd^.run) then<br />
begin<br />
//* if we are running then we immediately request another tick... */<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 1000000 div 25;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
rd^.doRender := TRUE;<br />
end<br />
else<br />
begin<br />
//* ... if not we acknowledge that our tickRequest returned */<br />
tickRequestPending := FALSE;<br />
end;<br />
end;<br />
</source><br />
<br />
Only if we want to leave our main loop we set TickRequestPending to False instead, because we can now safely close the timer.device again.<br />
<br />
We also check whether we want to leave the loop but still have a tick-request pending in which case it must be canceled.:<br />
<br />
<source lang="pascal"><br />
if (not(rd^.run) and tickRequestPending) then<br />
begin<br />
//* We want to leave, but there is still a tick request pending? Let's abort it */<br />
AbortIO(PIORequest(@rd^.tickRequest));<br />
end;<br />
</source><br />
<br />
Now, if we compile and execute engine5.pas, we'll immediately see that our animation is now much more stable and smoother.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== More on timing ==<br />
<br />
Our current implementation has now reached the point where it will provide you with a maximum number of desired frames per second and that any other CPU time can be used by the system. However, the opposite has not yet been taken into account sufficiently: if we are unable to reach the desired frame rate, then our animation will also run slower as it simply advances a fixed amount of frames. This is undesired behavior: the animation will of course continue to stutter in the absence of a frame rate but if for example our rectangle makes a quarter rotation per second then it should always stay that way, whether we run with 50fps or only with 5fps. To accomplish this we still have to consider the factor time when drawing. We also retrieve this from timer.device by sending a corresponding request. Because our first request is already managed by timer.device and waits there until answered we have to create a second request. For this purpose we will fill rd^.timerIO() analogue to the initial first request. We also need to keep track of the time of our last frame. So we're going to expand our RenderEngineData with two entries:<br />
<br />
<source lang="pascal"><br />
getTimeRequest : Ttimerequest;<br />
lastRenderTime : Ttimeval;<br />
</source><br />
<br />
In RunEngine (), we'll initialize them:<br />
<br />
<source lang="pascal"><br />
//* setup our getTime request... */<br />
rd^.getTimeRequest := rd^.timerIO^;<br />
<br />
//* ... get the current time... */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* ... and initialize our lastRenderTime */<br />
rd^.lastRenderTime := rd^.getTimeRequest.tr_time;<br />
</source><br />
<br />
Now we have to rewrite our RenderBackbuffer() function: So far we have stubbornly increased our rectangular coordinates by a 1/32 of the output size per frame but now, instead, we want our coordinates to be incremented every four seconds by the output size. In order to make this as precise as possible we do not declare the current position as a word but as a floating point number.<br />
<br />
While FLOAT is a predefined type for c, it is not for Free Pascal so for consistency we declared a new FLOAT type first:<br />
<br />
<source lang="pascal"><br />
Type<br />
FLOAT = single;<br />
</source><br />
<br />
And define currentStep to be of type FLOAT:<br />
<br />
<source lang="pascal"><br />
currentStep : FLOAT;<br />
</source><br />
<br />
CurrentStep will now save the current position as a value between 0.0 and 1.0, where 0.0 will stand for the minimum coordinate and 1.0 for the maximum coordinate.<br />
<br />
To calculate this value, we need to determine the elapsed time per call to RenderBackbuffer() since the last frame. We do this by retrieving the current time and subtract the time of our last frame:<br />
<br />
<source lang="pascal"><br />
diff : Ttimeval;<br />
<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* get the time passed since our last render call */<br />
diff := rd^.getTimeRequest.tr_time;<br />
SubTime(@diff, @rd^.lastRenderTime);<br />
</source><br />
<br />
Diff now contains the difference in timeval format. This is a bit clumsy for our purposes, we'd rather have it as a floating point number in seconds: <br />
<br />
<source lang="pascal"><br />
secondsPassed : FLOAT;<br />
micros : ULONG;<br />
</source><br />
<br />
Therefor we also need to divide the complete number of microseconds in diff by 1000000.0:<br />
<br />
<source lang="pascal"><br />
if (diff.tv_secs <> 0) then<br />
begin<br />
micros := diff.tv_secs * 1000000;<br />
end<br />
else<br />
begin<br />
micros := 0;<br />
end;<br />
micros := micros + diff.tv_micro;<br />
secondsPassed := FLOAT(micros) / 1000000.0;<br />
</source><br />
<br />
Now we just need to increase currentStep by a quarter of secondsPassed so that we can reach our maximum value of 1.0 every four seconds. Once we have achieved this, we must deduct the absolute value from it. Because in practice we only expect an absolute value of 1.0, we do this with a simple subtraction. The while loop serves only as a safety net, just in case we somehow manage to get a value over 2.0; Normally we will only go through them once:<br />
<br />
<source lang="pascal"><br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
<br />
while (rd^.currentStep >= 1.0) do<br />
begin<br />
rd^.currentStep := rd^.currentStep - 1.0;<br />
end;<br />
</source><br />
<br />
Then we just have to insert the product from our maximum position and currentStep as current position:<br />
<br />
<source lang="pascal"><br />
pos.x := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.x)));<br />
pos.y := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.y)));<br />
</source><br />
<br />
If we start the executable then we can see a smooth rotating rectangle. Now if we would increase the system load (for example, by starting Engine6 several times) then we see that the animation starts to become choppy, but the rectangle rotates at the same speed.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Refactoring ==<br />
<br />
Actually, we're done now: We have an animation that is controlled by timer.device and which does not get disturbed even under full load. However, the different functions are very entangled. We want to separate those so that we are able to use the engine regardless of what we want to (re-)render. We also want to add a few smaller tweaks.<br />
<br />
So we want to isolate the renderer from our current code. This consists mainly of code for the drawing operations in RenderBackbuffer(), but also lastRenderTime and its logic belongs to it. Our renderer should also be able to determine the desired frame rate.<br />
<br />
The above makes it clear that our renderer at least consist of three functions:<br />
<br />
* InitRenderer() to create it and set the frame rate<br />
* RenderFrame() to draw a frame and<br />
* DestroyRenderer() to destroy the renderer again.<br />
<br />
We also want to make sure that both InitRenderer() and RenderFrame() can fail, so we use our integer return value that's proven to work. At DestroyRenderer() Nothing can go wrong (we cannot do any meaningful error handling here anyway), so this procedure remains:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(): integer;<br />
function RenderFrame(): integer;<br />
procedure DestroyRenderer();<br />
</source><br />
<br />
Naturally the renderer must also be able to store its own data during its lifetime. Therefor we enable this functionality and store the information inside the init function by passing it as a var pointer to the userdata and retrieve this information at RenderFrame() and DestroyRenderer() by passing the userdata as parameter:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer): integer;<br />
function RenderFrame(userData: pointer: integer;<br />
procedure DestroyRenderer(userData: pointer);<br />
</source><br />
<br />
It is useful to define a structure for the renderer data, which is then initialized in InitRenderer():<br />
<br />
<source lang="pascal"><br />
type<br />
PRendererData = ^TRendererData;<br />
TRendererData = <br />
record<br />
end;<br />
<br />
function InitRenderer(var userdata: pointer): integer;<br />
begin<br />
userData := AllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source> <br />
<br />
... and accordingly freed in DestroyRenderer():<br />
<br />
<source lang="pascal"><br />
procedure DestroyRenderer(userData: pointer);<br />
begin<br />
ExecFreeMem(userData, sizeof(TRendererData));<br />
end;<br />
</source><br />
<br />
In this renderer structure we copy our data from RenderEngineData too:<br />
<br />
<source lang="pascal"><br />
TRendererData = record<br />
lastRenderTime : Ttimeval;<br />
currentStep : FLOAT;<br />
end;<br />
</source><br />
<br />
To initialize this structure, we also need the current system time. And because we also want to set the framerate, our InitRenderer() looks like this:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer; const sysTime: Ptimeval; refreshRate: Ptimeval): integer;<br />
var<br />
rd : PRendererData;<br />
begin<br />
//* allocate our user data */<br />
userData := ExecAllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
//* set our lastRenderTime to now */<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
//* we would like to get a refresh rate of 25 frames per second */<br />
refreshRate^.tv_secs := 0;<br />
refreshRate^.tv_micro := 1000000 div 25;<br />
<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source><br />
<br />
In RenderFrame we use our previous used drawing operations. We are also tinkering with an auxiliary function to convert the difference between the two timeval structures in seconds as a float:<br />
<br />
<source lang="pascal"><br />
function DiffInSeconds(const early: Ptimeval; const late: Ptimeval): FLOAT;<br />
var<br />
diff : Ttimeval;<br />
micros : ULONG;<br />
begin<br />
diff := late^;<br />
SubTime(@diff, Ptimeval(early));<br />
<br />
if (diff.tv_secs <> 0)<br />
then micros := diff.tv_secs * 1000000<br />
else micros := 0;<br />
micros := micros + diff.tv_micro;<br />
<br />
result := FLOAT(micros) / 1000000.0;<br />
end;<br />
</source><br />
<br />
RenderFrame() looks a bit more tidy:<br />
<br />
check this function, it is the wrong code as it was taken from engine7.pas while the workshop is still preparing things<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; const sysTime: Ptimeval): integer;<br />
var<br />
secondsPassed : FLOAT;<br />
pos : TPoint;<br />
maxPos : TPoint;<br />
rd : PRendererData;<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
secondsPassed := DiffInSeconds(@rd^.lastRenderTime, sysTime);<br />
<br />
rd^.maxPos.x := renderTargetSize^.x - 1;<br />
rd^.maxPos.y := renderTargetSize^.y - 1;<br />
<br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
while (currentStep >= 1.0) do<br />
begin<br />
currentStep := currentStep - 1.0;<br />
end;<br />
<br />
//* now compute our new position */<br />
pos.x := SmallInt(trunc(rd^.currentStep * maxPosX));<br />
pos.y := SmallInt(trunc(rd^.currentStep * maxPosY));<br />
<br />
//* clear our bitmap */<br />
SetRast(renderTarget, 0);<br />
<br />
//* draw our rectangle */<br />
SetAPen(renderTarget, 1);<br />
GfxMove(renderTarget, 0 , LongInt(pos.y) );<br />
Draw(renderTarget , LongInt(maxPos.x - pos.x), 0 );<br />
Draw(renderTarget , LongInt(maxPos.x) , LongInt(maxPos.y - pos.y) );<br />
Draw(renderTarget , LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(renderTarget , 0 , LongInt(pos.y) );<br />
<br />
//* remember our render time */<br />
<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
Now we just have to make sure that these functions are called in the engine at the appropriate places. Because we also have to retrieve the current system time on multiple occasions, we write a separate function for it:<br />
<br />
<source lang="pascal"><br />
procedure UpdateTime(rd: PRenderEngineData);<br />
begin<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
end;<br />
</source><br />
<br />
If necessary, we read the system time from our getTimeRequest.<br />
<br />
In our RenderEngineData we also keep track of the desired refresh time and the UserData pointer for the renderer:<br />
<br />
<source lang="pascal"><br />
refreshRate : Ttimeval;<br />
userData : pointer;<br />
</source><br />
<br />
And we place the InitRenderer() call in RunEngine() immediately before opening our window when jumping into MainLoop():<br />
<br />
<source lang="pascal"><br />
//* get the current time... */<br />
UpdateTime(rd);<br />
<br />
//* ... and initialize our Renderer */<br />
result := InitRenderer(rd^.userData,<br />
@rd^.getTimeRequest.tr_time,<br />
@rd^.refreshRate);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
[...] //* open window and do MainLoop */<br />
<br />
DestroyRenderer(rd^.userData);<br />
end;<br />
</source><br />
<br />
RenderFrame() is then then simply called in RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize,<br />
@rd^.getTimeRequest.tr_time<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Directly before the call to RenderFrame() we obtain the current time and pass it to RenderFrame().<br />
<br />
This means that we have completely refactored our renderer logic into three functions. In a real program you could now <br />
conveniently place the remaining engine into a separate unit. As part of this workshop, however, we do not want to have to deal with setting up a project or makefile at this time (red: Free Pascal does not require makefiles and/or projects setup (that is, if you do not wish to do so), so it's very easy to store the engine into a separate unit, as long as the compiler is able to locate this unit (which is no problem if a unit is located at the same location as where the main program file is stored).<br />
<br />
Instead, there is still one more thing that is bothering us: it may very well be that a call to RenderFrame determines that it does not actually has to render anything because the contents of the frame hasn't changed since the last call. However, we need to tell whether the last frame is still present in the backbuffer (e.g. because in the meantime the back buffer had to be recreated), otherwise the frame always have to be redrawn. To accomplish this, we expand RenderFrame with two parameters:<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; renderTargetDirty: boolean; const sysTime: Ptimeval; var updateDone: Boolean): integer;<br />
</source><br />
<br />
So RenderTargetDirty lets the renderer know whether the last frame is still present in the renderTarget. UpdateDone informs the caller whether or not the renderer actually drew a frame in renderTarget.<br />
<br />
To determine whether the backbuffer always has to be redrawn or if the previous frame is still intact, our PrepareBackBuffer function can be used. Therefor we also need to expand this function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData; var backBufferDirty: boolean): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
[Allocate new bitmap code snippet...]<br />
<br />
backBufferDirty := TRUE;<br />
end<br />
else<br />
begin<br />
backBufferDirty := FALSE;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
So we set BackBufferDirty to true as soon as we had to create our bitmap again.<br />
<br />
Now we put these two function together in RenderBackBuffer() and return the DoRepaint of RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData; var doRepaint: boolean): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize, backBufferDirty,<br />
@rd^.getTimeRequest.tr_time,<br />
doRepaint<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Now all we have to do is update the rendercall in MainLoop():<br />
<br />
<source lang="pascal"><br />
var<br />
doRepaint: boolean;<br />
<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd, doRepaint);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, set repaint if required */<br />
if not(rd^.doRepaint) then<br />
begin<br />
rd^.doRepaint := doRepaint;<br />
end;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
end;<br />
</source><br />
<br />
It should be noted that we do not overwrite an already set RD^.DoRepaint with a false value.<br />
<br />
<br />
We have now reached our first goal: we have a window in which we can draw using double-buffering and were we can even change the frame rate which can accurately be controlled by timer.device.<br />
<br />
Engine7.pas is compiled with this call:<br />
<br />
<source lang="pascal"><br />
fpc engine7.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=871Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-24T20:48:48Z<p>Molly: Undo revision 870 by Molly (talk)</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
[...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Our image learns to walk ==<br />
<br />
Now we want to animate our image by drawing only one iteration of our loop in RenderBackbuffer() per frame. To accomplish this we store the current counter in our RenderEngineData structure.<br />
<br />
<source lang="pascal"><br />
currentStep : integer;<br />
</source><br />
<br />
The RenderBackbuffer() function is modified so that it only uses the current value for four lines per call, and then increments the value by one:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* setup our maximum coordinates and our step width */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
//* compute our current coordinates */<br />
pos.x := rd^.currentStep * lineStep.x;<br />
pos.y := rd^.currentStep * lineStep.y;<br />
<br />
//* increase our step for the next frame */<br />
rd^.currentStep := rd^.currentStep + 1;<br />
if (rd^.currentStep >= stepCount) then<br />
begin<br />
rd^.currentStep := 0;<br />
end;<br />
<br />
//* now draw our line pattern */<br />
SetAPen(rastPort, 1);<br />
GfxMove(rastPort, 0, SLONG(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0 );<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(rastPort, 0 , LongInt(pos.y) );<br />
end;<br />
end;<br />
</source><br />
<br />
As soon as currentStep becomes greater or equal to StepCount, we will have to reset the value back to 0 again.<br />
<br />
The only thing left is to make sure that our RenderBackbuffer () is called regularly. In order to accomplish this we ignore the DoRender flag inside our loop and simply always draw in this situation.<br />
<br />
The Wait() is replaced by a setsignal() and allows us to query if a message from the windows was received but without the need to wait for such a message to arrive.<br />
<br />
<source lang="pascal"><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
signals : ULONG;<br />
<br />
sig : ULONG;<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* combine it with the CTRL-C signal */<br />
signals := winSig or SIGBREAKF_CTRL_C;<br />
<br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
sig := SetSignal(0, signals);<br />
<br />
if (sig and winSig) <> 0 then<br />
begin<br />
//* our window signaled us, so let's harvest all its messages in a loop... */<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
<br />
if (sig and SIGBREAKF_CTRL_C) <> 0 then<br />
begin<br />
//* we leave on CTRL-C */<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
result := rd^.returnCode;<br />
end;<br />
</source><br />
<br />
Here is the source engine4.pas, that can be compiled again with<br />
<br />
<source lang="pascal"><br />
fpc engine4.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== What's timer.device got to do with it ==<br />
<br />
As expected, our window now shows an animation. However, in practice this implementation is far from ideal: On the one hand the speed of our animation directly dependents on the computing and graphical performance of the Amiga on which it runs and, on the other hand it will consume all the processor time that is available. So we need a different solution here. We could simply add a call to Delay() which at the least would sort out the problem with consuming CPU speed. However, if we want to display an animation with a single image every 2 seconds, it would cause the window to be blocked during those two seconds and as a result will respond very sluggish when clicking on the close button or other user actions. We would also need to take the time it takes to render and display into account and for that a simple Delay() is not suitable because of its 1/50-second resolution. We need another timer and timer.device provides one that is perfectly suited for our purposes.<br />
<br />
Like any device, timer.device is also controlled by exec functions OpenDevice()/CloseDevice() and SendIO()/WaitIO()/DoIO()/AbortIO(). Additionally timer.device has a small set of functions that are called in a similar way as functions from a library. These functions are declared inside unit timer and require an initialized base pointer as for any library.<br />
<br />
Note that unit timer already provides a variable named TimerBase for this purpose.<br />
<br />
As it should be for any device, also timer.device is controlled by sending IORequests. These IORequests are generated by us and not by the device itself and in order to send these back and forth we also need a MsgPort. Timer.device also defines its own IORequest structure which is a structure named timerequest and is located in unit timer. In addition to the IORequest, there is also a structure TTimeval defined which supports timing with a resolution of microseconds (1/1000000 seconds).<br />
<br />
Therefor we're going to expand our RenderEngineData structure, this time with a MsgPort and a timerequest:<br />
<br />
<source lang="pascal"><br />
timerPort : PMsgPort;<br />
timerIO : Ptimerequest;<br />
</source><br />
<br />
We also need to make sure the following two units are in our uses clause:<br />
<br />
<source lang="pascal"><br />
Uses<br />
Exec, Timer;<br />
</source><br />
<br />
We create our own function to open timer.device:<br />
<br />
<source lang="pascal"><br />
function InitTimerDevice(rd: PRenderEngineData): integer;<br />
begin<br />
//* we do not return success until we've opened the timer.device */<br />
result := RETURN_FAIL;<br />
<br />
//* create a message port through which we will communicate with the timer.device */<br />
rd^.timerPort := CreatePort(nil, 0);<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
//* create a timerequest which we will we pass between the timer.device and ourself */<br />
rd^.timerIO := Ptimerequest(CreateExtIO(rd^.timerPort, sizeof(Ttimerequest)));<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
//* open the timer.device */<br />
if (OpenDevice(TIMERNAME, UNIT_MICROHZ, PIORequest(rd^.timerIO), 0) = 0) then<br />
begin<br />
//* Success: let's set the TimerBase so we can call timer.device's functions */<br />
TimerBase := PLibrary(rd^.timerIO^.tr_node.io_Device);<br />
result := RETURN_OK;<br />
end;<br />
end;<br />
end;<br />
<br />
if (result <> RETURN_OK) then<br />
begin<br />
//* in case of an error: cleanup immediatly */<br />
FreeTimerDevice(rd);<br />
end;<br />
end;<br />
</source><br />
<br />
And a corresponding function FreeTimerDevice():<br />
<br />
<source lang="pascal"><br />
procedure FreeTimerDevice(rd: PRenderEngineData);<br />
begin<br />
//* close the timer.device */<br />
if Assigned(TimerBase) then<br />
begin<br />
CloseDevice(PIORequest(rd^.timerIO));<br />
TimerBase := nil;<br />
end;<br />
<br />
//* free our timerequest */<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
DeleteExtIO(PIORequest(rd^.timerIO));<br />
rd^.timerIO := nil;<br />
end;<br />
<br />
//* free our message port */<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
DeletePort(rd^.timerPort);<br />
rd^.timerPort := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
An additional short explanation for UNIT_MICROHZ used above: Timer.device offers several different modes, which mainly differ in their used resolution and accuracy. The mode we use here is characteristic for its high resolution. However it is also quite inaccurate: If you use it to count seconds then you'll notice that it will deviate from a price clock after a few minutes. However, this shortcoming is not important for our purpose.<br />
<br />
Anyhow, we call these functions immediately after the creation of our RenderEngineData and directly before its release respectively. Because we will send our timerequests asynchronously they are therefor not available for us at timer.device operations. Therefor we do not use our freshly created timerrequest but use it as a template only in order to retrieve the actual used request. For now we only need one for our timer, which we will also add to the RenderEngineData structure:<br />
<br />
<source lang="pascal"><br />
tickRequest : Ttimerequest;<br />
</source><br />
<br />
We initialize this field by simply assigning the contents of TimerIO to it:<br />
<br />
<source lang="pascal"><br />
rd^.tickRequest := rd^.timerIO^;<br />
</source><br />
<br />
Finally our RunEngine() that now looks like this:<br />
<br />
<source lang="pascal"><br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
//* allocate the memory for our runtime data and initialize it with zeros */<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
result := InitTimerDevice(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
//* setup our tick request */<br />
rd^.tickRequest := rd^.timerIO^;<br />
rd^.tickRequest.tr_node.io_Command := TR_ADDREQUEST;<br />
<br />
//* now let's open our window */<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
FreeTimerDevice(rd);<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
Inside Mainloop(), because we cannot leave our loop before a request is answered by timer.device, we have to remember whether or not the tickRequest is just waiting or not. We do this by using a simple boolean:<br />
<br />
<source lang="pascal"><br />
tickRequestPending : Boolean;<br />
</source><br />
<br />
We also need to expand our wait mask and include the TimerPort:<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the timer port */<br />
tickSig := 1 shl rd^.timerPort^.mp_SigBit;<br />
<br />
//* combine them to our final waitmask */<br />
signals := winSig or tickSig or SIGBREAKF_CTRL_C;<br />
</source><br />
<br />
We don't have to set DoRender to true, we'll do that later when we receive our ticks.<br />
<br />
Immediately before our main loop we send the first tick-request:<br />
<br />
<source lang="pascal"><br />
{* <br />
we start with a no-time request so we receive a tick immediately<br />
(we have to set 2 micros because of a bug in timer.device for 1.3) <br />
*}<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 2;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
tickRequestPending := TRUE;<br />
</source><br />
<br />
Instead of using setsignal () we are now waiting properly for the arrival of window messages or timers.device ticks:<br />
<br />
<source lang="pascal"><br />
sig := Wait(signals);<br />
</source><br />
<br />
When we receive a tick signal we send it immediately so that we can be signaled about a 1/25 second later:<br />
<br />
<source lang="pascal"><br />
if (sig and tickSig) <> 0 then<br />
begin<br />
//* our tickRequest signalled us, let's remove it from the replyport */<br />
WaitIO(PIORequest(@rd^.tickRequest));<br />
<br />
if (rd^.run) then<br />
begin<br />
//* if we are running then we immediately request another tick... */<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 1000000 div 25;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
rd^.doRender := TRUE;<br />
end<br />
else<br />
begin<br />
//* ... if not we acknowledge that our tickRequest returned */<br />
tickRequestPending := FALSE;<br />
end;<br />
end;<br />
</source><br />
<br />
Only if we want to leave our main loop we set TickRequestPending to False instead, because we can now safely close the timer.device again.<br />
<br />
We also check whether we want to leave the loop but still have a tick-request pending in which case it must be canceled.:<br />
<br />
<source lang="pascal"><br />
if (not(rd^.run) and tickRequestPending) then<br />
begin<br />
//* We want to leave, but there is still a tick request pending? Let's abort it */<br />
AbortIO(PIORequest(@rd^.tickRequest));<br />
end;<br />
</source><br />
<br />
Now, if we compile and execute engine5.pas, we'll immediately see that our animation is now much more stable and smoother.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== More on timing ==<br />
<br />
Our current implementation has now reached the point where it will provide you with a maximum number of desired frames per second and that any other CPU time can be used by the system. However, the opposite has not yet been taken into account sufficiently: if we are unable to reach the desired frame rate, then our animation will also run slower as it simply advances a fixed amount of frames. This is undesired behavior: the animation will of course continue to stutter in the absence of a frame rate but if for example our rectangle makes a quarter rotation per second then it should always stay that way, whether we run with 50fps or only with 5fps. To accomplish this we still have to consider the factor time when drawing. We also retrieve this from timer.device by sending a corresponding request. Because our first request is already managed by timer.device and waits there until answered we have to create a second request. For this purpose we will fill rd^.timerIO() analogue to the initial first request. We also need to keep track of the time of our last frame. So we're going to expand our RenderEngineData with two entries:<br />
<br />
<source lang="pascal"><br />
getTimeRequest : Ttimerequest;<br />
lastRenderTime : Ttimeval;<br />
</source><br />
<br />
In RunEngine (), we'll initialize them:<br />
<br />
<source lang="pascal"><br />
//* setup our getTime request... */<br />
rd^.getTimeRequest := rd^.timerIO^;<br />
<br />
//* ... get the current time... */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* ... and initialize our lastRenderTime */<br />
rd^.lastRenderTime := rd^.getTimeRequest.tr_time;<br />
</source><br />
<br />
Now we have to rewrite our RenderBackbuffer() function: So far we have stubbornly increased our rectangular coordinates by a 1/32 of the output size per frame but now, instead, we want our coordinates to be incremented every four seconds by the output size. In order to make this as precise as possible we do not declare the current position as a word but as a floating point number.<br />
<br />
While FLOAT is a predefined type for c, it is not for Free Pascal so for consistency we declared a new FLOAT type first:<br />
<br />
<source lang="pascal"><br />
Type<br />
FLOAT = single;<br />
</source><br />
<br />
And define currentStep to be of type FLOAT:<br />
<br />
<source lang="pascal"><br />
currentStep : FLOAT;<br />
</source><br />
<br />
CurrentStep will now save the current position as a value between 0.0 and 1.0, where 0.0 will stand for the minimum coordinate and 1.0 for the maximum coordinate.<br />
<br />
To calculate this value, we need to determine the elapsed time per call to RenderBackbuffer() since the last frame. We do this by retrieving the current time and subtract the time of our last frame:<br />
<br />
<source lang="pascal"><br />
diff : Ttimeval;<br />
<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* get the time passed since our last render call */<br />
diff := rd^.getTimeRequest.tr_time;<br />
SubTime(@diff, @rd^.lastRenderTime);<br />
</source><br />
<br />
Diff now contains the difference in timeval format. This is a bit clumsy for our purposes, we'd rather have it as a floating point number in seconds: <br />
<br />
<source lang="pascal"><br />
secondsPassed : FLOAT;<br />
micros : ULONG;<br />
</source><br />
<br />
Therefor we also need to divide the complete number of microseconds in diff by 1000000.0:<br />
<br />
<source lang="pascal"><br />
if (diff.tv_secs <> 0) then<br />
begin<br />
micros := diff.tv_secs * 1000000;<br />
end<br />
else<br />
begin<br />
micros := 0;<br />
end;<br />
micros := micros + diff.tv_micro;<br />
secondsPassed := FLOAT(micros) / 1000000.0;<br />
</source><br />
<br />
Now we just need to increase currentStep by a quarter of secondsPassed so that we can reach our maximum value of 1.0 every four seconds. Once we have achieved this, we must deduct the absolute value from it. Because in practice we only expect an absolute value of 1.0, we do this with a simple subtraction. The while loop serves only as a safety net, just in case we somehow manage to get a value over 2.0; Normally we will only go through them once:<br />
<br />
<source lang="pascal"><br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
<br />
while (rd^.currentStep >= 1.0) do<br />
begin<br />
rd^.currentStep := rd^.currentStep - 1.0;<br />
end;<br />
</source><br />
<br />
Then we just have to insert the product from our maximum position and currentStep as current position:<br />
<br />
<source lang="pascal"><br />
pos.x := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.x)));<br />
pos.y := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.y)));<br />
</source><br />
<br />
If we start the executable then we can see a smooth rotating rectangle. Now if we would increase the system load (for example, by starting Engine6 several times) then we see that the animation starts to become choppy, but the rectangle rotates at the same speed.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Refactoring ==<br />
<br />
Actually, we're done now: We have an animation that is controlled by timer.device and which does not get disturbed even under full load. However, the different functions are very entangled. We want to separate those so that we are able to use the engine regardless of what we want to (re-)render. We also want to add a few smaller tweaks.<br />
<br />
So we want to isolate the renderer from our current code. This consists mainly of code for the drawing operations in RenderBackbuffer(), but also lastRenderTime and its logic belongs to it. Our renderer should also be able to determine the desired frame rate.<br />
<br />
The above makes it clear that our renderer at least consist of three functions:<br />
<br />
* InitRenderer() to create it and set the frame rate<br />
* RenderFrame() to draw a frame and<br />
* DestroyRenderer() to destroy the renderer again.<br />
<br />
We also want to make sure that both InitRenderer() and RenderFrame() can fail, so we use our integer return value that's proven to work. At DestroyRenderer() Nothing can go wrong (we cannot do any meaningful error handling here anyway), so this procedure remains:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(): integer;<br />
function RenderFrame(): integer;<br />
procedure DestroyRenderer();<br />
</source><br />
<br />
Naturally the renderer must also be able to store its own data during its lifetime. Therefor we enable this functionality and store the information inside the init function by passing it as a var pointer to the userdata and retrieve this information at RenderFrame() and DestroyRenderer() by passing the userdata as parameter:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer): integer;<br />
function RenderFrame(userData: pointer: integer;<br />
procedure DestroyRenderer(userData: pointer);<br />
</source><br />
<br />
It is useful to define a structure for the renderer data, which is then initialized in InitRenderer():<br />
<br />
<source lang="pascal"><br />
type<br />
PRendererData = ^TRendererData;<br />
TRendererData = <br />
record<br />
end;<br />
<br />
function InitRenderer(var userdata: pointer): integer;<br />
begin<br />
userData := AllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source> <br />
<br />
... and accordingly freed in DestroyRenderer():<br />
<br />
<source lang="pascal"><br />
procedure DestroyRenderer(userData: pointer);<br />
begin<br />
ExecFreeMem(userData, sizeof(TRendererData));<br />
end;<br />
</source><br />
<br />
In this renderer structure we copy our data from RenderEngineData too:<br />
<br />
<source lang="pascal"><br />
TRendererData = record<br />
lastRenderTime : Ttimeval;<br />
currentStep : FLOAT;<br />
end;<br />
</source><br />
<br />
To initialize this structure, we also need the current system time. And because we also want to set the framerate, our InitRenderer() looks like this:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer; const sysTime: Ptimeval; refreshRate: Ptimeval): integer;<br />
var<br />
rd : PRendererData;<br />
begin<br />
//* allocate our user data */<br />
userData := ExecAllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
//* set our lastRenderTime to now */<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
//* we would like to get a refresh rate of 25 frames per second */<br />
refreshRate^.tv_secs := 0;<br />
refreshRate^.tv_micro := 1000000 div 25;<br />
<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source><br />
<br />
In RenderFrame we use our previous used drawing operations. We are also tinkering with an auxiliary function to convert the difference between the two timeval structures in seconds as a float:<br />
<br />
<source lang="pascal"><br />
function DiffInSeconds(const early: Ptimeval; const late: Ptimeval): FLOAT;<br />
var<br />
diff : Ttimeval;<br />
micros : ULONG;<br />
begin<br />
diff := late^;<br />
SubTime(@diff, Ptimeval(early));<br />
<br />
if (diff.tv_secs <> 0)<br />
then micros := diff.tv_secs * 1000000<br />
else micros := 0;<br />
micros := micros + diff.tv_micro;<br />
<br />
result := FLOAT(micros) / 1000000.0;<br />
end;<br />
</source><br />
<br />
RenderFrame() looks a bit more tidy:<br />
<br />
check this function, it is the wrong code as it was taken from engine7.pas while the workshop is still preparing things<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; const sysTime: Ptimeval): integer;<br />
var<br />
secondsPassed : FLOAT;<br />
pos : TPoint;<br />
maxPos : TPoint;<br />
rd : PRendererData;<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
secondsPassed := DiffInSeconds(@rd^.lastRenderTime, sysTime);<br />
<br />
rd^.maxPos.x := renderTargetSize^.x - 1;<br />
rd^.maxPos.y := renderTargetSize^.y - 1;<br />
<br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
while (currentStep >= 1.0) do<br />
begin<br />
currentStep := currentStep - 1.0;<br />
end;<br />
<br />
//* now compute our new position */<br />
pos.x := SmallInt(trunc(rd^.currentStep * maxPosX));<br />
pos.y := SmallInt(trunc(rd^.currentStep * maxPosY));<br />
<br />
//* clear our bitmap */<br />
SetRast(renderTarget, 0);<br />
<br />
//* draw our rectangle */<br />
SetAPen(renderTarget, 1);<br />
GfxMove(renderTarget, 0 , LongInt(pos.y) );<br />
Draw(renderTarget , LongInt(maxPos.x - pos.x), 0 );<br />
Draw(renderTarget , LongInt(maxPos.x) , LongInt(maxPos.y - pos.y) );<br />
Draw(renderTarget , LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(renderTarget , 0 , LongInt(pos.y) );<br />
<br />
//* remember our render time */<br />
<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
Now we just have to make sure that these functions are called in the engine at the appropriate places. Because we also have to retrieve the current system time on multiple occasions, we write a separate function for it:<br />
<br />
<source lang="pascal"><br />
procedure UpdateTime(rd: PRenderEngineData);<br />
begin<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
end;<br />
</source><br />
<br />
If necessary, we read the system time from our getTimeRequest.<br />
<br />
In our RenderEngineData we also keep track of the desired refresh time and the UserData pointer for the renderer:<br />
<br />
<source lang="pascal"><br />
refreshRate : Ttimeval;<br />
userData : pointer;<br />
</source><br />
<br />
And we place the InitRenderer() call in RunEngine() immediately before opening our window when jumping into MainLoop():<br />
<br />
<source lang="pascal"><br />
//* get the current time... */<br />
UpdateTime(rd);<br />
<br />
//* ... and initialize our Renderer */<br />
result := InitRenderer(rd^.userData,<br />
@rd^.getTimeRequest.tr_time,<br />
@rd^.refreshRate);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
[...] //* open window and do MainLoop */<br />
<br />
DestroyRenderer(rd^.userData);<br />
end;<br />
</source><br />
<br />
RenderFrame() is then then simply called in RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize,<br />
@rd^.getTimeRequest.tr_time<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Directly before the call to RenderFrame() we obtain the current time and pass it to RenderFrame().<br />
<br />
This means that we have completely refactored our renderer logic into three functions. In a real program you could now <br />
conveniently place the remaining engine into a separate unit. As part of this workshop, however, we do not want to have to deal with setting up a project or makefile at this time (red: Free Pascal does not require makefiles and/or projects setup (that is, if you do not wish to do so), so it's very easy to store the engine into a separate unit, as long as the compiler is able to locate this unit (which is no problem if a unit is located at the same location as where the main program file is stored).<br />
<br />
Instead, there is still one more thing that is bothering us: it may very well be that a call to RenderFrame determines that it does not actually has to render anything because the contents of the frame hasn't changed since the last call. However, we need to tell whether the last frame is still present in the backbuffer (e.g. because in the meantime the back buffer had to be recreated), otherwise the frame always have to be redrawn. To accomplish this, we expand RenderFrame with two parameters:<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; renderTargetDirty: boolean; const sysTime: Ptimeval; var updateDone: Boolean): integer;<br />
</source><br />
<br />
So RenderTargetDirty lets the renderer know whether the last frame is still present in the renderTarget. UpdateDone informs the caller whether or not the renderer actually drew a frame in renderTarget.<br />
<br />
To determine whether the backbuffer always has to be redrawn or if the previous frame is still intact, our PrepareBackBuffer function can be used. Therefor we also need to expand this function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData; var backBufferDirty: boolean): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
[Allocate new bitmap code snippet...]<br />
<br />
backBufferDirty := TRUE;<br />
end<br />
else<br />
begin<br />
backBufferDirty := FALSE;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
So we set BackBufferDirty to true as soon as we had to create our bitmap again.<br />
<br />
Now we put these two function together in RenderBackBuffer() and return the DoRepaint of RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData; var doRepaint: boolean): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize, backBufferDirty,<br />
@rd^.getTimeRequest.tr_time,<br />
doRepaint<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Now all we have to do is update the rendercall in MainLoop():<br />
<br />
<source lang="pascal"><br />
var<br />
doRepaint: boolean;<br />
<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd, doRepaint);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, set repaint if required */<br />
if not(rd^.doRepaint) then<br />
begin<br />
rd^.doRepaint := doRepaint;<br />
end;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
end;<br />
</source><br />
<br />
It should be noted that we do not overwrite an already set RD^.DoRepaint with a false value.<br />
<br />
<br />
We have now reached our first goal: we have a window in which we can draw using double-buffering and were we can even change the frame rate which can accurately be controlled by timer.device.<br />
<br />
Engine7.pas is compiled with this call:<br />
<br />
<source lang="pascal"><br />
fpc engine7.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=870Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-24T20:45:51Z<p>Molly: </p>
<hr />
<div>[[Category:Workshops]]<br />
{{TOC limit}}<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
[...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Our image learns to walk ==<br />
<br />
Now we want to animate our image by drawing only one iteration of our loop in RenderBackbuffer() per frame. To accomplish this we store the current counter in our RenderEngineData structure.<br />
<br />
<source lang="pascal"><br />
currentStep : integer;<br />
</source><br />
<br />
The RenderBackbuffer() function is modified so that it only uses the current value for four lines per call, and then increments the value by one:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* setup our maximum coordinates and our step width */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
//* compute our current coordinates */<br />
pos.x := rd^.currentStep * lineStep.x;<br />
pos.y := rd^.currentStep * lineStep.y;<br />
<br />
//* increase our step for the next frame */<br />
rd^.currentStep := rd^.currentStep + 1;<br />
if (rd^.currentStep >= stepCount) then<br />
begin<br />
rd^.currentStep := 0;<br />
end;<br />
<br />
//* now draw our line pattern */<br />
SetAPen(rastPort, 1);<br />
GfxMove(rastPort, 0, SLONG(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0 );<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(rastPort, 0 , LongInt(pos.y) );<br />
end;<br />
end;<br />
</source><br />
<br />
As soon as currentStep becomes greater or equal to StepCount, we will have to reset the value back to 0 again.<br />
<br />
The only thing left is to make sure that our RenderBackbuffer () is called regularly. In order to accomplish this we ignore the DoRender flag inside our loop and simply always draw in this situation.<br />
<br />
The Wait() is replaced by a setsignal() and allows us to query if a message from the windows was received but without the need to wait for such a message to arrive.<br />
<br />
<source lang="pascal"><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
signals : ULONG;<br />
<br />
sig : ULONG;<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* combine it with the CTRL-C signal */<br />
signals := winSig or SIGBREAKF_CTRL_C;<br />
<br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
sig := SetSignal(0, signals);<br />
<br />
if (sig and winSig) <> 0 then<br />
begin<br />
//* our window signaled us, so let's harvest all its messages in a loop... */<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
<br />
if (sig and SIGBREAKF_CTRL_C) <> 0 then<br />
begin<br />
//* we leave on CTRL-C */<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
result := rd^.returnCode;<br />
end;<br />
</source><br />
<br />
Here is the source engine4.pas, that can be compiled again with<br />
<br />
<source lang="pascal"><br />
fpc engine4.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== What's timer.device got to do with it ==<br />
<br />
As expected, our window now shows an animation. However, in practice this implementation is far from ideal: On the one hand the speed of our animation directly dependents on the computing and graphical performance of the Amiga on which it runs and, on the other hand it will consume all the processor time that is available. So we need a different solution here. We could simply add a call to Delay() which at the least would sort out the problem with consuming CPU speed. However, if we want to display an animation with a single image every 2 seconds, it would cause the window to be blocked during those two seconds and as a result will respond very sluggish when clicking on the close button or other user actions. We would also need to take the time it takes to render and display into account and for that a simple Delay() is not suitable because of its 1/50-second resolution. We need another timer and timer.device provides one that is perfectly suited for our purposes.<br />
<br />
Like any device, timer.device is also controlled by exec functions OpenDevice()/CloseDevice() and SendIO()/WaitIO()/DoIO()/AbortIO(). Additionally timer.device has a small set of functions that are called in a similar way as functions from a library. These functions are declared inside unit timer and require an initialized base pointer as for any library.<br />
<br />
Note that unit timer already provides a variable named TimerBase for this purpose.<br />
<br />
As it should be for any device, also timer.device is controlled by sending IORequests. These IORequests are generated by us and not by the device itself and in order to send these back and forth we also need a MsgPort. Timer.device also defines its own IORequest structure which is a structure named timerequest and is located in unit timer. In addition to the IORequest, there is also a structure TTimeval defined which supports timing with a resolution of microseconds (1/1000000 seconds).<br />
<br />
Therefor we're going to expand our RenderEngineData structure, this time with a MsgPort and a timerequest:<br />
<br />
<source lang="pascal"><br />
timerPort : PMsgPort;<br />
timerIO : Ptimerequest;<br />
</source><br />
<br />
We also need to make sure the following two units are in our uses clause:<br />
<br />
<source lang="pascal"><br />
Uses<br />
Exec, Timer;<br />
</source><br />
<br />
We create our own function to open timer.device:<br />
<br />
<source lang="pascal"><br />
function InitTimerDevice(rd: PRenderEngineData): integer;<br />
begin<br />
//* we do not return success until we've opened the timer.device */<br />
result := RETURN_FAIL;<br />
<br />
//* create a message port through which we will communicate with the timer.device */<br />
rd^.timerPort := CreatePort(nil, 0);<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
//* create a timerequest which we will we pass between the timer.device and ourself */<br />
rd^.timerIO := Ptimerequest(CreateExtIO(rd^.timerPort, sizeof(Ttimerequest)));<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
//* open the timer.device */<br />
if (OpenDevice(TIMERNAME, UNIT_MICROHZ, PIORequest(rd^.timerIO), 0) = 0) then<br />
begin<br />
//* Success: let's set the TimerBase so we can call timer.device's functions */<br />
TimerBase := PLibrary(rd^.timerIO^.tr_node.io_Device);<br />
result := RETURN_OK;<br />
end;<br />
end;<br />
end;<br />
<br />
if (result <> RETURN_OK) then<br />
begin<br />
//* in case of an error: cleanup immediatly */<br />
FreeTimerDevice(rd);<br />
end;<br />
end;<br />
</source><br />
<br />
And a corresponding function FreeTimerDevice():<br />
<br />
<source lang="pascal"><br />
procedure FreeTimerDevice(rd: PRenderEngineData);<br />
begin<br />
//* close the timer.device */<br />
if Assigned(TimerBase) then<br />
begin<br />
CloseDevice(PIORequest(rd^.timerIO));<br />
TimerBase := nil;<br />
end;<br />
<br />
//* free our timerequest */<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
DeleteExtIO(PIORequest(rd^.timerIO));<br />
rd^.timerIO := nil;<br />
end;<br />
<br />
//* free our message port */<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
DeletePort(rd^.timerPort);<br />
rd^.timerPort := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
An additional short explanation for UNIT_MICROHZ used above: Timer.device offers several different modes, which mainly differ in their used resolution and accuracy. The mode we use here is characteristic for its high resolution. However it is also quite inaccurate: If you use it to count seconds then you'll notice that it will deviate from a price clock after a few minutes. However, this shortcoming is not important for our purpose.<br />
<br />
Anyhow, we call these functions immediately after the creation of our RenderEngineData and directly before its release respectively. Because we will send our timerequests asynchronously they are therefor not available for us at timer.device operations. Therefor we do not use our freshly created timerrequest but use it as a template only in order to retrieve the actual used request. For now we only need one for our timer, which we will also add to the RenderEngineData structure:<br />
<br />
<source lang="pascal"><br />
tickRequest : Ttimerequest;<br />
</source><br />
<br />
We initialize this field by simply assigning the contents of TimerIO to it:<br />
<br />
<source lang="pascal"><br />
rd^.tickRequest := rd^.timerIO^;<br />
</source><br />
<br />
Finally our RunEngine() that now looks like this:<br />
<br />
<source lang="pascal"><br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
//* allocate the memory for our runtime data and initialize it with zeros */<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
result := InitTimerDevice(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
//* setup our tick request */<br />
rd^.tickRequest := rd^.timerIO^;<br />
rd^.tickRequest.tr_node.io_Command := TR_ADDREQUEST;<br />
<br />
//* now let's open our window */<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
FreeTimerDevice(rd);<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
Inside Mainloop(), because we cannot leave our loop before a request is answered by timer.device, we have to remember whether or not the tickRequest is just waiting or not. We do this by using a simple boolean:<br />
<br />
<source lang="pascal"><br />
tickRequestPending : Boolean;<br />
</source><br />
<br />
We also need to expand our wait mask and include the TimerPort:<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the timer port */<br />
tickSig := 1 shl rd^.timerPort^.mp_SigBit;<br />
<br />
//* combine them to our final waitmask */<br />
signals := winSig or tickSig or SIGBREAKF_CTRL_C;<br />
</source><br />
<br />
We don't have to set DoRender to true, we'll do that later when we receive our ticks.<br />
<br />
Immediately before our main loop we send the first tick-request:<br />
<br />
<source lang="pascal"><br />
{* <br />
we start with a no-time request so we receive a tick immediately<br />
(we have to set 2 micros because of a bug in timer.device for 1.3) <br />
*}<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 2;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
tickRequestPending := TRUE;<br />
</source><br />
<br />
Instead of using setsignal () we are now waiting properly for the arrival of window messages or timers.device ticks:<br />
<br />
<source lang="pascal"><br />
sig := Wait(signals);<br />
</source><br />
<br />
When we receive a tick signal we send it immediately so that we can be signaled about a 1/25 second later:<br />
<br />
<source lang="pascal"><br />
if (sig and tickSig) <> 0 then<br />
begin<br />
//* our tickRequest signalled us, let's remove it from the replyport */<br />
WaitIO(PIORequest(@rd^.tickRequest));<br />
<br />
if (rd^.run) then<br />
begin<br />
//* if we are running then we immediately request another tick... */<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 1000000 div 25;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
rd^.doRender := TRUE;<br />
end<br />
else<br />
begin<br />
//* ... if not we acknowledge that our tickRequest returned */<br />
tickRequestPending := FALSE;<br />
end;<br />
end;<br />
</source><br />
<br />
Only if we want to leave our main loop we set TickRequestPending to False instead, because we can now safely close the timer.device again.<br />
<br />
We also check whether we want to leave the loop but still have a tick-request pending in which case it must be canceled.:<br />
<br />
<source lang="pascal"><br />
if (not(rd^.run) and tickRequestPending) then<br />
begin<br />
//* We want to leave, but there is still a tick request pending? Let's abort it */<br />
AbortIO(PIORequest(@rd^.tickRequest));<br />
end;<br />
</source><br />
<br />
Now, if we compile and execute engine5.pas, we'll immediately see that our animation is now much more stable and smoother.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== More on timing ==<br />
<br />
Our current implementation has now reached the point where it will provide you with a maximum number of desired frames per second and that any other CPU time can be used by the system. However, the opposite has not yet been taken into account sufficiently: if we are unable to reach the desired frame rate, then our animation will also run slower as it simply advances a fixed amount of frames. This is undesired behavior: the animation will of course continue to stutter in the absence of a frame rate but if for example our rectangle makes a quarter rotation per second then it should always stay that way, whether we run with 50fps or only with 5fps. To accomplish this we still have to consider the factor time when drawing. We also retrieve this from timer.device by sending a corresponding request. Because our first request is already managed by timer.device and waits there until answered we have to create a second request. For this purpose we will fill rd^.timerIO() analogue to the initial first request. We also need to keep track of the time of our last frame. So we're going to expand our RenderEngineData with two entries:<br />
<br />
<source lang="pascal"><br />
getTimeRequest : Ttimerequest;<br />
lastRenderTime : Ttimeval;<br />
</source><br />
<br />
In RunEngine (), we'll initialize them:<br />
<br />
<source lang="pascal"><br />
//* setup our getTime request... */<br />
rd^.getTimeRequest := rd^.timerIO^;<br />
<br />
//* ... get the current time... */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* ... and initialize our lastRenderTime */<br />
rd^.lastRenderTime := rd^.getTimeRequest.tr_time;<br />
</source><br />
<br />
Now we have to rewrite our RenderBackbuffer() function: So far we have stubbornly increased our rectangular coordinates by a 1/32 of the output size per frame but now, instead, we want our coordinates to be incremented every four seconds by the output size. In order to make this as precise as possible we do not declare the current position as a word but as a floating point number.<br />
<br />
While FLOAT is a predefined type for c, it is not for Free Pascal so for consistency we declared a new FLOAT type first:<br />
<br />
<source lang="pascal"><br />
Type<br />
FLOAT = single;<br />
</source><br />
<br />
And define currentStep to be of type FLOAT:<br />
<br />
<source lang="pascal"><br />
currentStep : FLOAT;<br />
</source><br />
<br />
CurrentStep will now save the current position as a value between 0.0 and 1.0, where 0.0 will stand for the minimum coordinate and 1.0 for the maximum coordinate.<br />
<br />
To calculate this value, we need to determine the elapsed time per call to RenderBackbuffer() since the last frame. We do this by retrieving the current time and subtract the time of our last frame:<br />
<br />
<source lang="pascal"><br />
diff : Ttimeval;<br />
<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* get the time passed since our last render call */<br />
diff := rd^.getTimeRequest.tr_time;<br />
SubTime(@diff, @rd^.lastRenderTime);<br />
</source><br />
<br />
Diff now contains the difference in timeval format. This is a bit clumsy for our purposes, we'd rather have it as a floating point number in seconds: <br />
<br />
<source lang="pascal"><br />
secondsPassed : FLOAT;<br />
micros : ULONG;<br />
</source><br />
<br />
Therefor we also need to divide the complete number of microseconds in diff by 1000000.0:<br />
<br />
<source lang="pascal"><br />
if (diff.tv_secs <> 0) then<br />
begin<br />
micros := diff.tv_secs * 1000000;<br />
end<br />
else<br />
begin<br />
micros := 0;<br />
end;<br />
micros := micros + diff.tv_micro;<br />
secondsPassed := FLOAT(micros) / 1000000.0;<br />
</source><br />
<br />
Now we just need to increase currentStep by a quarter of secondsPassed so that we can reach our maximum value of 1.0 every four seconds. Once we have achieved this, we must deduct the absolute value from it. Because in practice we only expect an absolute value of 1.0, we do this with a simple subtraction. The while loop serves only as a safety net, just in case we somehow manage to get a value over 2.0; Normally we will only go through them once:<br />
<br />
<source lang="pascal"><br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
<br />
while (rd^.currentStep >= 1.0) do<br />
begin<br />
rd^.currentStep := rd^.currentStep - 1.0;<br />
end;<br />
</source><br />
<br />
Then we just have to insert the product from our maximum position and currentStep as current position:<br />
<br />
<source lang="pascal"><br />
pos.x := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.x)));<br />
pos.y := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.y)));<br />
</source><br />
<br />
If we start the executable then we can see a smooth rotating rectangle. Now if we would increase the system load (for example, by starting Engine6 several times) then we see that the animation starts to become choppy, but the rectangle rotates at the same speed.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Refactoring ==<br />
<br />
Actually, we're done now: We have an animation that is controlled by timer.device and which does not get disturbed even under full load. However, the different functions are very entangled. We want to separate those so that we are able to use the engine regardless of what we want to (re-)render. We also want to add a few smaller tweaks.<br />
<br />
So we want to isolate the renderer from our current code. This consists mainly of code for the drawing operations in RenderBackbuffer(), but also lastRenderTime and its logic belongs to it. Our renderer should also be able to determine the desired frame rate.<br />
<br />
The above makes it clear that our renderer at least consist of three functions:<br />
<br />
* InitRenderer() to create it and set the frame rate<br />
* RenderFrame() to draw a frame and<br />
* DestroyRenderer() to destroy the renderer again.<br />
<br />
We also want to make sure that both InitRenderer() and RenderFrame() can fail, so we use our integer return value that's proven to work. At DestroyRenderer() Nothing can go wrong (we cannot do any meaningful error handling here anyway), so this procedure remains:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(): integer;<br />
function RenderFrame(): integer;<br />
procedure DestroyRenderer();<br />
</source><br />
<br />
Naturally the renderer must also be able to store its own data during its lifetime. Therefor we enable this functionality and store the information inside the init function by passing it as a var pointer to the userdata and retrieve this information at RenderFrame() and DestroyRenderer() by passing the userdata as parameter:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer): integer;<br />
function RenderFrame(userData: pointer: integer;<br />
procedure DestroyRenderer(userData: pointer);<br />
</source><br />
<br />
It is useful to define a structure for the renderer data, which is then initialized in InitRenderer():<br />
<br />
<source lang="pascal"><br />
type<br />
PRendererData = ^TRendererData;<br />
TRendererData = <br />
record<br />
end;<br />
<br />
function InitRenderer(var userdata: pointer): integer;<br />
begin<br />
userData := AllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source> <br />
<br />
... and accordingly freed in DestroyRenderer():<br />
<br />
<source lang="pascal"><br />
procedure DestroyRenderer(userData: pointer);<br />
begin<br />
ExecFreeMem(userData, sizeof(TRendererData));<br />
end;<br />
</source><br />
<br />
In this renderer structure we copy our data from RenderEngineData too:<br />
<br />
<source lang="pascal"><br />
TRendererData = record<br />
lastRenderTime : Ttimeval;<br />
currentStep : FLOAT;<br />
end;<br />
</source><br />
<br />
To initialize this structure, we also need the current system time. And because we also want to set the framerate, our InitRenderer() looks like this:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer; const sysTime: Ptimeval; refreshRate: Ptimeval): integer;<br />
var<br />
rd : PRendererData;<br />
begin<br />
//* allocate our user data */<br />
userData := ExecAllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
//* set our lastRenderTime to now */<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
//* we would like to get a refresh rate of 25 frames per second */<br />
refreshRate^.tv_secs := 0;<br />
refreshRate^.tv_micro := 1000000 div 25;<br />
<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source><br />
<br />
In RenderFrame we use our previous used drawing operations. We are also tinkering with an auxiliary function to convert the difference between the two timeval structures in seconds as a float:<br />
<br />
<source lang="pascal"><br />
function DiffInSeconds(const early: Ptimeval; const late: Ptimeval): FLOAT;<br />
var<br />
diff : Ttimeval;<br />
micros : ULONG;<br />
begin<br />
diff := late^;<br />
SubTime(@diff, Ptimeval(early));<br />
<br />
if (diff.tv_secs <> 0)<br />
then micros := diff.tv_secs * 1000000<br />
else micros := 0;<br />
micros := micros + diff.tv_micro;<br />
<br />
result := FLOAT(micros) / 1000000.0;<br />
end;<br />
</source><br />
<br />
RenderFrame() looks a bit more tidy:<br />
<br />
check this function, it is the wrong code as it was taken from engine7.pas while the workshop is still preparing things<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; const sysTime: Ptimeval): integer;<br />
var<br />
secondsPassed : FLOAT;<br />
pos : TPoint;<br />
maxPos : TPoint;<br />
rd : PRendererData;<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
secondsPassed := DiffInSeconds(@rd^.lastRenderTime, sysTime);<br />
<br />
rd^.maxPos.x := renderTargetSize^.x - 1;<br />
rd^.maxPos.y := renderTargetSize^.y - 1;<br />
<br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
while (currentStep >= 1.0) do<br />
begin<br />
currentStep := currentStep - 1.0;<br />
end;<br />
<br />
//* now compute our new position */<br />
pos.x := SmallInt(trunc(rd^.currentStep * maxPosX));<br />
pos.y := SmallInt(trunc(rd^.currentStep * maxPosY));<br />
<br />
//* clear our bitmap */<br />
SetRast(renderTarget, 0);<br />
<br />
//* draw our rectangle */<br />
SetAPen(renderTarget, 1);<br />
GfxMove(renderTarget, 0 , LongInt(pos.y) );<br />
Draw(renderTarget , LongInt(maxPos.x - pos.x), 0 );<br />
Draw(renderTarget , LongInt(maxPos.x) , LongInt(maxPos.y - pos.y) );<br />
Draw(renderTarget , LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(renderTarget , 0 , LongInt(pos.y) );<br />
<br />
//* remember our render time */<br />
<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
Now we just have to make sure that these functions are called in the engine at the appropriate places. Because we also have to retrieve the current system time on multiple occasions, we write a separate function for it:<br />
<br />
<source lang="pascal"><br />
procedure UpdateTime(rd: PRenderEngineData);<br />
begin<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
end;<br />
</source><br />
<br />
If necessary, we read the system time from our getTimeRequest.<br />
<br />
In our RenderEngineData we also keep track of the desired refresh time and the UserData pointer for the renderer:<br />
<br />
<source lang="pascal"><br />
refreshRate : Ttimeval;<br />
userData : pointer;<br />
</source><br />
<br />
And we place the InitRenderer() call in RunEngine() immediately before opening our window when jumping into MainLoop():<br />
<br />
<source lang="pascal"><br />
//* get the current time... */<br />
UpdateTime(rd);<br />
<br />
//* ... and initialize our Renderer */<br />
result := InitRenderer(rd^.userData,<br />
@rd^.getTimeRequest.tr_time,<br />
@rd^.refreshRate);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
[...] //* open window and do MainLoop */<br />
<br />
DestroyRenderer(rd^.userData);<br />
end;<br />
</source><br />
<br />
RenderFrame() is then then simply called in RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize,<br />
@rd^.getTimeRequest.tr_time<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Directly before the call to RenderFrame() we obtain the current time and pass it to RenderFrame().<br />
<br />
This means that we have completely refactored our renderer logic into three functions. In a real program you could now <br />
conveniently place the remaining engine into a separate unit. As part of this workshop, however, we do not want to have to deal with setting up a project or makefile at this time (red: Free Pascal does not require makefiles and/or projects setup (that is, if you do not wish to do so), so it's very easy to store the engine into a separate unit, as long as the compiler is able to locate this unit (which is no problem if a unit is located at the same location as where the main program file is stored).<br />
<br />
Instead, there is still one more thing that is bothering us: it may very well be that a call to RenderFrame determines that it does not actually has to render anything because the contents of the frame hasn't changed since the last call. However, we need to tell whether the last frame is still present in the backbuffer (e.g. because in the meantime the back buffer had to be recreated), otherwise the frame always have to be redrawn. To accomplish this, we expand RenderFrame with two parameters:<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; renderTargetDirty: boolean; const sysTime: Ptimeval; var updateDone: Boolean): integer;<br />
</source><br />
<br />
So RenderTargetDirty lets the renderer know whether the last frame is still present in the renderTarget. UpdateDone informs the caller whether or not the renderer actually drew a frame in renderTarget.<br />
<br />
To determine whether the backbuffer always has to be redrawn or if the previous frame is still intact, our PrepareBackBuffer function can be used. Therefor we also need to expand this function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData; var backBufferDirty: boolean): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
[Allocate new bitmap code snippet...]<br />
<br />
backBufferDirty := TRUE;<br />
end<br />
else<br />
begin<br />
backBufferDirty := FALSE;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
So we set BackBufferDirty to true as soon as we had to create our bitmap again.<br />
<br />
Now we put these two function together in RenderBackBuffer() and return the DoRepaint of RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData; var doRepaint: boolean): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize, backBufferDirty,<br />
@rd^.getTimeRequest.tr_time,<br />
doRepaint<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Now all we have to do is update the rendercall in MainLoop():<br />
<br />
<source lang="pascal"><br />
var<br />
doRepaint: boolean;<br />
<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd, doRepaint);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, set repaint if required */<br />
if not(rd^.doRepaint) then<br />
begin<br />
rd^.doRepaint := doRepaint;<br />
end;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
end;<br />
</source><br />
<br />
It should be noted that we do not overwrite an already set RD^.DoRepaint with a false value.<br />
<br />
<br />
We have now reached our first goal: we have a window in which we can draw using double-buffering and were we can even change the frame rate which can accurately be controlled by timer.device.<br />
<br />
Engine7.pas is compiled with this call:<br />
<br />
<source lang="pascal"><br />
fpc engine7.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=869Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-24T20:13:26Z<p>Molly: remove superfluous header</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
[...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Our image learns to walk ==<br />
<br />
Now we want to animate our image by drawing only one iteration of our loop in RenderBackbuffer() per frame. To accomplish this we store the current counter in our RenderEngineData structure.<br />
<br />
<source lang="pascal"><br />
currentStep : integer;<br />
</source><br />
<br />
The RenderBackbuffer() function is modified so that it only uses the current value for four lines per call, and then increments the value by one:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* setup our maximum coordinates and our step width */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
//* compute our current coordinates */<br />
pos.x := rd^.currentStep * lineStep.x;<br />
pos.y := rd^.currentStep * lineStep.y;<br />
<br />
//* increase our step for the next frame */<br />
rd^.currentStep := rd^.currentStep + 1;<br />
if (rd^.currentStep >= stepCount) then<br />
begin<br />
rd^.currentStep := 0;<br />
end;<br />
<br />
//* now draw our line pattern */<br />
SetAPen(rastPort, 1);<br />
GfxMove(rastPort, 0, SLONG(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0 );<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(rastPort, 0 , LongInt(pos.y) );<br />
end;<br />
end;<br />
</source><br />
<br />
As soon as currentStep becomes greater or equal to StepCount, we will have to reset the value back to 0 again.<br />
<br />
The only thing left is to make sure that our RenderBackbuffer () is called regularly. In order to accomplish this we ignore the DoRender flag inside our loop and simply always draw in this situation.<br />
<br />
The Wait() is replaced by a setsignal() and allows us to query if a message from the windows was received but without the need to wait for such a message to arrive.<br />
<br />
<source lang="pascal"><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
signals : ULONG;<br />
<br />
sig : ULONG;<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* combine it with the CTRL-C signal */<br />
signals := winSig or SIGBREAKF_CTRL_C;<br />
<br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
sig := SetSignal(0, signals);<br />
<br />
if (sig and winSig) <> 0 then<br />
begin<br />
//* our window signaled us, so let's harvest all its messages in a loop... */<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
<br />
if (sig and SIGBREAKF_CTRL_C) <> 0 then<br />
begin<br />
//* we leave on CTRL-C */<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
result := rd^.returnCode;<br />
end;<br />
</source><br />
<br />
Here is the source engine4.pas, that can be compiled again with<br />
<br />
<source lang="pascal"><br />
fpc engine4.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== What's timer.device got to do with it ==<br />
<br />
As expected, our window now shows an animation. However, in practice this implementation is far from ideal: On the one hand the speed of our animation directly dependents on the computing and graphical performance of the Amiga on which it runs and, on the other hand it will consume all the processor time that is available. So we need a different solution here. We could simply add a call to Delay() which at the least would sort out the problem with consuming CPU speed. However, if we want to display an animation with a single image every 2 seconds, it would cause the window to be blocked during those two seconds and as a result will respond very sluggish when clicking on the close button or other user actions. We would also need to take the time it takes to render and display into account and for that a simple Delay() is not suitable because of its 1/50-second resolution. We need another timer and timer.device provides one that is perfectly suited for our purposes.<br />
<br />
Like any device, timer.device is also controlled by exec functions OpenDevice()/CloseDevice() and SendIO()/WaitIO()/DoIO()/AbortIO(). Additionally timer.device has a small set of functions that are called in a similar way as functions from a library. These functions are declared inside unit timer and require an initialized base pointer as for any library.<br />
<br />
Note that unit timer already provides a variable named TimerBase for this purpose.<br />
<br />
As it should be for any device, also timer.device is controlled by sending IORequests. These IORequests are generated by us and not by the device itself and in order to send these back and forth we also need a MsgPort. Timer.device also defines its own IORequest structure which is a structure named timerequest and is located in unit timer. In addition to the IORequest, there is also a structure TTimeval defined which supports timing with a resolution of microseconds (1/1000000 seconds).<br />
<br />
Therefor we're going to expand our RenderEngineData structure, this time with a MsgPort and a timerequest:<br />
<br />
<source lang="pascal"><br />
timerPort : PMsgPort;<br />
timerIO : Ptimerequest;<br />
</source><br />
<br />
We also need to make sure the following two units are in our uses clause:<br />
<br />
<source lang="pascal"><br />
Uses<br />
Exec, Timer;<br />
</source><br />
<br />
We create our own function to open timer.device:<br />
<br />
<source lang="pascal"><br />
function InitTimerDevice(rd: PRenderEngineData): integer;<br />
begin<br />
//* we do not return success until we've opened the timer.device */<br />
result := RETURN_FAIL;<br />
<br />
//* create a message port through which we will communicate with the timer.device */<br />
rd^.timerPort := CreatePort(nil, 0);<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
//* create a timerequest which we will we pass between the timer.device and ourself */<br />
rd^.timerIO := Ptimerequest(CreateExtIO(rd^.timerPort, sizeof(Ttimerequest)));<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
//* open the timer.device */<br />
if (OpenDevice(TIMERNAME, UNIT_MICROHZ, PIORequest(rd^.timerIO), 0) = 0) then<br />
begin<br />
//* Success: let's set the TimerBase so we can call timer.device's functions */<br />
TimerBase := PLibrary(rd^.timerIO^.tr_node.io_Device);<br />
result := RETURN_OK;<br />
end;<br />
end;<br />
end;<br />
<br />
if (result <> RETURN_OK) then<br />
begin<br />
//* in case of an error: cleanup immediatly */<br />
FreeTimerDevice(rd);<br />
end;<br />
end;<br />
</source><br />
<br />
And a corresponding function FreeTimerDevice():<br />
<br />
<source lang="pascal"><br />
procedure FreeTimerDevice(rd: PRenderEngineData);<br />
begin<br />
//* close the timer.device */<br />
if Assigned(TimerBase) then<br />
begin<br />
CloseDevice(PIORequest(rd^.timerIO));<br />
TimerBase := nil;<br />
end;<br />
<br />
//* free our timerequest */<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
DeleteExtIO(PIORequest(rd^.timerIO));<br />
rd^.timerIO := nil;<br />
end;<br />
<br />
//* free our message port */<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
DeletePort(rd^.timerPort);<br />
rd^.timerPort := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
An additional short explanation for UNIT_MICROHZ used above: Timer.device offers several different modes, which mainly differ in their used resolution and accuracy. The mode we use here is characteristic for its high resolution. However it is also quite inaccurate: If you use it to count seconds then you'll notice that it will deviate from a price clock after a few minutes. However, this shortcoming is not important for our purpose.<br />
<br />
Anyhow, we call these functions immediately after the creation of our RenderEngineData and directly before its release respectively. Because we will send our timerequests asynchronously they are therefor not available for us at timer.device operations. Therefor we do not use our freshly created timerrequest but use it as a template only in order to retrieve the actual used request. For now we only need one for our timer, which we will also add to the RenderEngineData structure:<br />
<br />
<source lang="pascal"><br />
tickRequest : Ttimerequest;<br />
</source><br />
<br />
We initialize this field by simply assigning the contents of TimerIO to it:<br />
<br />
<source lang="pascal"><br />
rd^.tickRequest := rd^.timerIO^;<br />
</source><br />
<br />
Finally our RunEngine() that now looks like this:<br />
<br />
<source lang="pascal"><br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
//* allocate the memory for our runtime data and initialize it with zeros */<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
result := InitTimerDevice(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
//* setup our tick request */<br />
rd^.tickRequest := rd^.timerIO^;<br />
rd^.tickRequest.tr_node.io_Command := TR_ADDREQUEST;<br />
<br />
//* now let's open our window */<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
FreeTimerDevice(rd);<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
Inside Mainloop(), because we cannot leave our loop before a request is answered by timer.device, we have to remember whether or not the tickRequest is just waiting or not. We do this by using a simple boolean:<br />
<br />
<source lang="pascal"><br />
tickRequestPending : Boolean;<br />
</source><br />
<br />
We also need to expand our wait mask and include the TimerPort:<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the timer port */<br />
tickSig := 1 shl rd^.timerPort^.mp_SigBit;<br />
<br />
//* combine them to our final waitmask */<br />
signals := winSig or tickSig or SIGBREAKF_CTRL_C;<br />
</source><br />
<br />
We don't have to set DoRender to true, we'll do that later when we receive our ticks.<br />
<br />
Immediately before our main loop we send the first tick-request:<br />
<br />
<source lang="pascal"><br />
{* <br />
we start with a no-time request so we receive a tick immediately<br />
(we have to set 2 micros because of a bug in timer.device for 1.3) <br />
*}<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 2;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
tickRequestPending := TRUE;<br />
</source><br />
<br />
Instead of using setsignal () we are now waiting properly for the arrival of window messages or timers.device ticks:<br />
<br />
<source lang="pascal"><br />
sig := Wait(signals);<br />
</source><br />
<br />
When we receive a tick signal we send it immediately so that we can be signaled about a 1/25 second later:<br />
<br />
<source lang="pascal"><br />
if (sig and tickSig) <> 0 then<br />
begin<br />
//* our tickRequest signalled us, let's remove it from the replyport */<br />
WaitIO(PIORequest(@rd^.tickRequest));<br />
<br />
if (rd^.run) then<br />
begin<br />
//* if we are running then we immediately request another tick... */<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 1000000 div 25;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
rd^.doRender := TRUE;<br />
end<br />
else<br />
begin<br />
//* ... if not we acknowledge that our tickRequest returned */<br />
tickRequestPending := FALSE;<br />
end;<br />
end;<br />
</source><br />
<br />
Only if we want to leave our main loop we set TickRequestPending to False instead, because we can now safely close the timer.device again.<br />
<br />
We also check whether we want to leave the loop but still have a tick-request pending in which case it must be canceled.:<br />
<br />
<source lang="pascal"><br />
if (not(rd^.run) and tickRequestPending) then<br />
begin<br />
//* We want to leave, but there is still a tick request pending? Let's abort it */<br />
AbortIO(PIORequest(@rd^.tickRequest));<br />
end;<br />
</source><br />
<br />
Now, if we compile and execute engine5.pas, we'll immediately see that our animation is now much more stable and smoother.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== More on timing ==<br />
<br />
Our current implementation has now reached the point where it will provide you with a maximum number of desired frames per second and that any other CPU time can be used by the system. However, the opposite has not yet been taken into account sufficiently: if we are unable to reach the desired frame rate, then our animation will also run slower as it simply advances a fixed amount of frames. This is undesired behavior: the animation will of course continue to stutter in the absence of a frame rate but if for example our rectangle makes a quarter rotation per second then it should always stay that way, whether we run with 50fps or only with 5fps. To accomplish this we still have to consider the factor time when drawing. We also retrieve this from timer.device by sending a corresponding request. Because our first request is already managed by timer.device and waits there until answered we have to create a second request. For this purpose we will fill rd^.timerIO() analogue to the initial first request. We also need to keep track of the time of our last frame. So we're going to expand our RenderEngineData with two entries:<br />
<br />
<source lang="pascal"><br />
getTimeRequest : Ttimerequest;<br />
lastRenderTime : Ttimeval;<br />
</source><br />
<br />
In RunEngine (), we'll initialize them:<br />
<br />
<source lang="pascal"><br />
//* setup our getTime request... */<br />
rd^.getTimeRequest := rd^.timerIO^;<br />
<br />
//* ... get the current time... */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* ... and initialize our lastRenderTime */<br />
rd^.lastRenderTime := rd^.getTimeRequest.tr_time;<br />
</source><br />
<br />
Now we have to rewrite our RenderBackbuffer() function: So far we have stubbornly increased our rectangular coordinates by a 1/32 of the output size per frame but now, instead, we want our coordinates to be incremented every four seconds by the output size. In order to make this as precise as possible we do not declare the current position as a word but as a floating point number.<br />
<br />
While FLOAT is a predefined type for c, it is not for Free Pascal so for consistency we declared a new FLOAT type first:<br />
<br />
<source lang="pascal"><br />
Type<br />
FLOAT = single;<br />
</source><br />
<br />
And define currentStep to be of type FLOAT:<br />
<br />
<source lang="pascal"><br />
currentStep : FLOAT;<br />
</source><br />
<br />
CurrentStep will now save the current position as a value between 0.0 and 1.0, where 0.0 will stand for the minimum coordinate and 1.0 for the maximum coordinate.<br />
<br />
To calculate this value, we need to determine the elapsed time per call to RenderBackbuffer() since the last frame. We do this by retrieving the current time and subtract the time of our last frame:<br />
<br />
<source lang="pascal"><br />
diff : Ttimeval;<br />
<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* get the time passed since our last render call */<br />
diff := rd^.getTimeRequest.tr_time;<br />
SubTime(@diff, @rd^.lastRenderTime);<br />
</source><br />
<br />
Diff now contains the difference in timeval format. This is a bit clumsy for our purposes, we'd rather have it as a floating point number in seconds: <br />
<br />
<source lang="pascal"><br />
secondsPassed : FLOAT;<br />
micros : ULONG;<br />
</source><br />
<br />
Therefor we also need to divide the complete number of microseconds in diff by 1000000.0:<br />
<br />
<source lang="pascal"><br />
if (diff.tv_secs <> 0) then<br />
begin<br />
micros := diff.tv_secs * 1000000;<br />
end<br />
else<br />
begin<br />
micros := 0;<br />
end;<br />
micros := micros + diff.tv_micro;<br />
secondsPassed := FLOAT(micros) / 1000000.0;<br />
</source><br />
<br />
Now we just need to increase currentStep by a quarter of secondsPassed so that we can reach our maximum value of 1.0 every four seconds. Once we have achieved this, we must deduct the absolute value from it. Because in practice we only expect an absolute value of 1.0, we do this with a simple subtraction. The while loop serves only as a safety net, just in case we somehow manage to get a value over 2.0; Normally we will only go through them once:<br />
<br />
<source lang="pascal"><br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
<br />
while (rd^.currentStep >= 1.0) do<br />
begin<br />
rd^.currentStep := rd^.currentStep - 1.0;<br />
end;<br />
</source><br />
<br />
Then we just have to insert the product from our maximum position and currentStep as current position:<br />
<br />
<source lang="pascal"><br />
pos.x := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.x)));<br />
pos.y := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.y)));<br />
</source><br />
<br />
If we start the executable then we can see a smooth rotating rectangle. Now if we would increase the system load (for example, by starting Engine6 several times) then we see that the animation starts to become choppy, but the rectangle rotates at the same speed.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Refactoring ==<br />
<br />
Actually, we're done now: We have an animation that is controlled by timer.device and which does not get disturbed even under full load. However, the different functions are very entangled. We want to separate those so that we are able to use the engine regardless of what we want to (re-)render. We also want to add a few smaller tweaks.<br />
<br />
So we want to isolate the renderer from our current code. This consists mainly of code for the drawing operations in RenderBackbuffer(), but also lastRenderTime and its logic belongs to it. Our renderer should also be able to determine the desired frame rate.<br />
<br />
The above makes it clear that our renderer at least consist of three functions:<br />
<br />
* InitRenderer() to create it and set the frame rate<br />
* RenderFrame() to draw a frame and<br />
* DestroyRenderer() to destroy the renderer again.<br />
<br />
We also want to make sure that both InitRenderer() and RenderFrame() can fail, so we use our integer return value that's proven to work. At DestroyRenderer() Nothing can go wrong (we cannot do any meaningful error handling here anyway), so this procedure remains:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(): integer;<br />
function RenderFrame(): integer;<br />
procedure DestroyRenderer();<br />
</source><br />
<br />
Naturally the renderer must also be able to store its own data during its lifetime. Therefor we enable this functionality and store the information inside the init function by passing it as a var pointer to the userdata and retrieve this information at RenderFrame() and DestroyRenderer() by passing the userdata as parameter:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer): integer;<br />
function RenderFrame(userData: pointer: integer;<br />
procedure DestroyRenderer(userData: pointer);<br />
</source><br />
<br />
It is useful to define a structure for the renderer data, which is then initialized in InitRenderer():<br />
<br />
<source lang="pascal"><br />
type<br />
PRendererData = ^TRendererData;<br />
TRendererData = <br />
record<br />
end;<br />
<br />
function InitRenderer(var userdata: pointer): integer;<br />
begin<br />
userData := AllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source> <br />
<br />
... and accordingly freed in DestroyRenderer():<br />
<br />
<source lang="pascal"><br />
procedure DestroyRenderer(userData: pointer);<br />
begin<br />
ExecFreeMem(userData, sizeof(TRendererData));<br />
end;<br />
</source><br />
<br />
In this renderer structure we copy our data from RenderEngineData too:<br />
<br />
<source lang="pascal"><br />
TRendererData = record<br />
lastRenderTime : Ttimeval;<br />
currentStep : FLOAT;<br />
end;<br />
</source><br />
<br />
To initialize this structure, we also need the current system time. And because we also want to set the framerate, our InitRenderer() looks like this:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer; const sysTime: Ptimeval; refreshRate: Ptimeval): integer;<br />
var<br />
rd : PRendererData;<br />
begin<br />
//* allocate our user data */<br />
userData := ExecAllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
//* set our lastRenderTime to now */<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
//* we would like to get a refresh rate of 25 frames per second */<br />
refreshRate^.tv_secs := 0;<br />
refreshRate^.tv_micro := 1000000 div 25;<br />
<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source><br />
<br />
In RenderFrame we use our previous used drawing operations. We are also tinkering with an auxiliary function to convert the difference between the two timeval structures in seconds as a float:<br />
<br />
<source lang="pascal"><br />
function DiffInSeconds(const early: Ptimeval; const late: Ptimeval): FLOAT;<br />
var<br />
diff : Ttimeval;<br />
micros : ULONG;<br />
begin<br />
diff := late^;<br />
SubTime(@diff, Ptimeval(early));<br />
<br />
if (diff.tv_secs <> 0)<br />
then micros := diff.tv_secs * 1000000<br />
else micros := 0;<br />
micros := micros + diff.tv_micro;<br />
<br />
result := FLOAT(micros) / 1000000.0;<br />
end;<br />
</source><br />
<br />
RenderFrame() looks a bit more tidy:<br />
<br />
check this function, it is the wrong code as it was taken from engine7.pas while the workshop is still preparing things<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; const sysTime: Ptimeval): integer;<br />
var<br />
secondsPassed : FLOAT;<br />
pos : TPoint;<br />
maxPos : TPoint;<br />
rd : PRendererData;<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
secondsPassed := DiffInSeconds(@rd^.lastRenderTime, sysTime);<br />
<br />
rd^.maxPos.x := renderTargetSize^.x - 1;<br />
rd^.maxPos.y := renderTargetSize^.y - 1;<br />
<br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
while (currentStep >= 1.0) do<br />
begin<br />
currentStep := currentStep - 1.0;<br />
end;<br />
<br />
//* now compute our new position */<br />
pos.x := SmallInt(trunc(rd^.currentStep * maxPosX));<br />
pos.y := SmallInt(trunc(rd^.currentStep * maxPosY));<br />
<br />
//* clear our bitmap */<br />
SetRast(renderTarget, 0);<br />
<br />
//* draw our rectangle */<br />
SetAPen(renderTarget, 1);<br />
GfxMove(renderTarget, 0 , LongInt(pos.y) );<br />
Draw(renderTarget , LongInt(maxPos.x - pos.x), 0 );<br />
Draw(renderTarget , LongInt(maxPos.x) , LongInt(maxPos.y - pos.y) );<br />
Draw(renderTarget , LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(renderTarget , 0 , LongInt(pos.y) );<br />
<br />
//* remember our render time */<br />
<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
Now we just have to make sure that these functions are called in the engine at the appropriate places. Because we also have to retrieve the current system time on multiple occasions, we write a separate function for it:<br />
<br />
<source lang="pascal"><br />
procedure UpdateTime(rd: PRenderEngineData);<br />
begin<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
end;<br />
</source><br />
<br />
If necessary, we read the system time from our getTimeRequest.<br />
<br />
In our RenderEngineData we also keep track of the desired refresh time and the UserData pointer for the renderer:<br />
<br />
<source lang="pascal"><br />
refreshRate : Ttimeval;<br />
userData : pointer;<br />
</source><br />
<br />
And we place the InitRenderer() call in RunEngine() immediately before opening our window when jumping into MainLoop():<br />
<br />
<source lang="pascal"><br />
//* get the current time... */<br />
UpdateTime(rd);<br />
<br />
//* ... and initialize our Renderer */<br />
result := InitRenderer(rd^.userData,<br />
@rd^.getTimeRequest.tr_time,<br />
@rd^.refreshRate);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
[...] //* open window and do MainLoop */<br />
<br />
DestroyRenderer(rd^.userData);<br />
end;<br />
</source><br />
<br />
RenderFrame() is then then simply called in RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize,<br />
@rd^.getTimeRequest.tr_time<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Directly before the call to RenderFrame() we obtain the current time and pass it to RenderFrame().<br />
<br />
This means that we have completely refactored our renderer logic into three functions. In a real program you could now <br />
conveniently place the remaining engine into a separate unit. As part of this workshop, however, we do not want to have to deal with setting up a project or makefile at this time (red: Free Pascal does not require makefiles and/or projects setup (that is, if you do not wish to do so), so it's very easy to store the engine into a separate unit, as long as the compiler is able to locate this unit (which is no problem if a unit is located at the same location as where the main program file is stored).<br />
<br />
Instead, there is still one more thing that is bothering us: it may very well be that a call to RenderFrame determines that it does not actually has to render anything because the contents of the frame hasn't changed since the last call. However, we need to tell whether the last frame is still present in the backbuffer (e.g. because in the meantime the back buffer had to be recreated), otherwise the frame always have to be redrawn. To accomplish this, we expand RenderFrame with two parameters:<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; renderTargetDirty: boolean; const sysTime: Ptimeval; var updateDone: Boolean): integer;<br />
</source><br />
<br />
So RenderTargetDirty lets the renderer know whether the last frame is still present in the renderTarget. UpdateDone informs the caller whether or not the renderer actually drew a frame in renderTarget.<br />
<br />
To determine whether the backbuffer always has to be redrawn or if the previous frame is still intact, our PrepareBackBuffer function can be used. Therefor we also need to expand this function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData; var backBufferDirty: boolean): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
[Allocate new bitmap code snippet...]<br />
<br />
backBufferDirty := TRUE;<br />
end<br />
else<br />
begin<br />
backBufferDirty := FALSE;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
So we set BackBufferDirty to true as soon as we had to create our bitmap again.<br />
<br />
Now we put these two function together in RenderBackBuffer() and return the DoRepaint of RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData; var doRepaint: boolean): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize, backBufferDirty,<br />
@rd^.getTimeRequest.tr_time,<br />
doRepaint<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Now all we have to do is update the rendercall in MainLoop():<br />
<br />
<source lang="pascal"><br />
var<br />
doRepaint: boolean;<br />
<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd, doRepaint);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, set repaint if required */<br />
if not(rd^.doRepaint) then<br />
begin<br />
rd^.doRepaint := doRepaint;<br />
end;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
end;<br />
</source><br />
<br />
It should be noted that we do not overwrite an already set RD^.DoRepaint with a false value.<br />
<br />
<br />
We have now reached our first goal: we have a window in which we can draw using double-buffering and were we can even change the frame rate which can accurately be controlled by timer.device.<br />
<br />
Engine7.pas is compiled with this call:<br />
<br />
<source lang="pascal"><br />
fpc engine7.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=868Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-24T20:12:15Z<p>Molly: /* Heading text */ Add content for chapter: Refactoring</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
[...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Our image learns to walk ==<br />
<br />
Now we want to animate our image by drawing only one iteration of our loop in RenderBackbuffer() per frame. To accomplish this we store the current counter in our RenderEngineData structure.<br />
<br />
<source lang="pascal"><br />
currentStep : integer;<br />
</source><br />
<br />
The RenderBackbuffer() function is modified so that it only uses the current value for four lines per call, and then increments the value by one:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* setup our maximum coordinates and our step width */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
//* compute our current coordinates */<br />
pos.x := rd^.currentStep * lineStep.x;<br />
pos.y := rd^.currentStep * lineStep.y;<br />
<br />
//* increase our step for the next frame */<br />
rd^.currentStep := rd^.currentStep + 1;<br />
if (rd^.currentStep >= stepCount) then<br />
begin<br />
rd^.currentStep := 0;<br />
end;<br />
<br />
//* now draw our line pattern */<br />
SetAPen(rastPort, 1);<br />
GfxMove(rastPort, 0, SLONG(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0 );<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(rastPort, 0 , LongInt(pos.y) );<br />
end;<br />
end;<br />
</source><br />
<br />
As soon as currentStep becomes greater or equal to StepCount, we will have to reset the value back to 0 again.<br />
<br />
The only thing left is to make sure that our RenderBackbuffer () is called regularly. In order to accomplish this we ignore the DoRender flag inside our loop and simply always draw in this situation.<br />
<br />
The Wait() is replaced by a setsignal() and allows us to query if a message from the windows was received but without the need to wait for such a message to arrive.<br />
<br />
<source lang="pascal"><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
signals : ULONG;<br />
<br />
sig : ULONG;<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* combine it with the CTRL-C signal */<br />
signals := winSig or SIGBREAKF_CTRL_C;<br />
<br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
sig := SetSignal(0, signals);<br />
<br />
if (sig and winSig) <> 0 then<br />
begin<br />
//* our window signaled us, so let's harvest all its messages in a loop... */<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
<br />
if (sig and SIGBREAKF_CTRL_C) <> 0 then<br />
begin<br />
//* we leave on CTRL-C */<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
result := rd^.returnCode;<br />
end;<br />
</source><br />
<br />
Here is the source engine4.pas, that can be compiled again with<br />
<br />
<source lang="pascal"><br />
fpc engine4.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== What's timer.device got to do with it ==<br />
<br />
As expected, our window now shows an animation. However, in practice this implementation is far from ideal: On the one hand the speed of our animation directly dependents on the computing and graphical performance of the Amiga on which it runs and, on the other hand it will consume all the processor time that is available. So we need a different solution here. We could simply add a call to Delay() which at the least would sort out the problem with consuming CPU speed. However, if we want to display an animation with a single image every 2 seconds, it would cause the window to be blocked during those two seconds and as a result will respond very sluggish when clicking on the close button or other user actions. We would also need to take the time it takes to render and display into account and for that a simple Delay() is not suitable because of its 1/50-second resolution. We need another timer and timer.device provides one that is perfectly suited for our purposes.<br />
<br />
Like any device, timer.device is also controlled by exec functions OpenDevice()/CloseDevice() and SendIO()/WaitIO()/DoIO()/AbortIO(). Additionally timer.device has a small set of functions that are called in a similar way as functions from a library. These functions are declared inside unit timer and require an initialized base pointer as for any library.<br />
<br />
Note that unit timer already provides a variable named TimerBase for this purpose.<br />
<br />
As it should be for any device, also timer.device is controlled by sending IORequests. These IORequests are generated by us and not by the device itself and in order to send these back and forth we also need a MsgPort. Timer.device also defines its own IORequest structure which is a structure named timerequest and is located in unit timer. In addition to the IORequest, there is also a structure TTimeval defined which supports timing with a resolution of microseconds (1/1000000 seconds).<br />
<br />
Therefor we're going to expand our RenderEngineData structure, this time with a MsgPort and a timerequest:<br />
<br />
<source lang="pascal"><br />
timerPort : PMsgPort;<br />
timerIO : Ptimerequest;<br />
</source><br />
<br />
We also need to make sure the following two units are in our uses clause:<br />
<br />
<source lang="pascal"><br />
Uses<br />
Exec, Timer;<br />
</source><br />
<br />
We create our own function to open timer.device:<br />
<br />
<source lang="pascal"><br />
function InitTimerDevice(rd: PRenderEngineData): integer;<br />
begin<br />
//* we do not return success until we've opened the timer.device */<br />
result := RETURN_FAIL;<br />
<br />
//* create a message port through which we will communicate with the timer.device */<br />
rd^.timerPort := CreatePort(nil, 0);<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
//* create a timerequest which we will we pass between the timer.device and ourself */<br />
rd^.timerIO := Ptimerequest(CreateExtIO(rd^.timerPort, sizeof(Ttimerequest)));<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
//* open the timer.device */<br />
if (OpenDevice(TIMERNAME, UNIT_MICROHZ, PIORequest(rd^.timerIO), 0) = 0) then<br />
begin<br />
//* Success: let's set the TimerBase so we can call timer.device's functions */<br />
TimerBase := PLibrary(rd^.timerIO^.tr_node.io_Device);<br />
result := RETURN_OK;<br />
end;<br />
end;<br />
end;<br />
<br />
if (result <> RETURN_OK) then<br />
begin<br />
//* in case of an error: cleanup immediatly */<br />
FreeTimerDevice(rd);<br />
end;<br />
end;<br />
</source><br />
<br />
And a corresponding function FreeTimerDevice():<br />
<br />
<source lang="pascal"><br />
procedure FreeTimerDevice(rd: PRenderEngineData);<br />
begin<br />
//* close the timer.device */<br />
if Assigned(TimerBase) then<br />
begin<br />
CloseDevice(PIORequest(rd^.timerIO));<br />
TimerBase := nil;<br />
end;<br />
<br />
//* free our timerequest */<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
DeleteExtIO(PIORequest(rd^.timerIO));<br />
rd^.timerIO := nil;<br />
end;<br />
<br />
//* free our message port */<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
DeletePort(rd^.timerPort);<br />
rd^.timerPort := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
An additional short explanation for UNIT_MICROHZ used above: Timer.device offers several different modes, which mainly differ in their used resolution and accuracy. The mode we use here is characteristic for its high resolution. However it is also quite inaccurate: If you use it to count seconds then you'll notice that it will deviate from a price clock after a few minutes. However, this shortcoming is not important for our purpose.<br />
<br />
Anyhow, we call these functions immediately after the creation of our RenderEngineData and directly before its release respectively. Because we will send our timerequests asynchronously they are therefor not available for us at timer.device operations. Therefor we do not use our freshly created timerrequest but use it as a template only in order to retrieve the actual used request. For now we only need one for our timer, which we will also add to the RenderEngineData structure:<br />
<br />
<source lang="pascal"><br />
tickRequest : Ttimerequest;<br />
</source><br />
<br />
We initialize this field by simply assigning the contents of TimerIO to it:<br />
<br />
<source lang="pascal"><br />
rd^.tickRequest := rd^.timerIO^;<br />
</source><br />
<br />
Finally our RunEngine() that now looks like this:<br />
<br />
<source lang="pascal"><br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
//* allocate the memory for our runtime data and initialize it with zeros */<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
result := InitTimerDevice(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
//* setup our tick request */<br />
rd^.tickRequest := rd^.timerIO^;<br />
rd^.tickRequest.tr_node.io_Command := TR_ADDREQUEST;<br />
<br />
//* now let's open our window */<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
FreeTimerDevice(rd);<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
Inside Mainloop(), because we cannot leave our loop before a request is answered by timer.device, we have to remember whether or not the tickRequest is just waiting or not. We do this by using a simple boolean:<br />
<br />
<source lang="pascal"><br />
tickRequestPending : Boolean;<br />
</source><br />
<br />
We also need to expand our wait mask and include the TimerPort:<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the timer port */<br />
tickSig := 1 shl rd^.timerPort^.mp_SigBit;<br />
<br />
//* combine them to our final waitmask */<br />
signals := winSig or tickSig or SIGBREAKF_CTRL_C;<br />
</source><br />
<br />
We don't have to set DoRender to true, we'll do that later when we receive our ticks.<br />
<br />
Immediately before our main loop we send the first tick-request:<br />
<br />
<source lang="pascal"><br />
{* <br />
we start with a no-time request so we receive a tick immediately<br />
(we have to set 2 micros because of a bug in timer.device for 1.3) <br />
*}<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 2;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
tickRequestPending := TRUE;<br />
</source><br />
<br />
Instead of using setsignal () we are now waiting properly for the arrival of window messages or timers.device ticks:<br />
<br />
<source lang="pascal"><br />
sig := Wait(signals);<br />
</source><br />
<br />
When we receive a tick signal we send it immediately so that we can be signaled about a 1/25 second later:<br />
<br />
<source lang="pascal"><br />
if (sig and tickSig) <> 0 then<br />
begin<br />
//* our tickRequest signalled us, let's remove it from the replyport */<br />
WaitIO(PIORequest(@rd^.tickRequest));<br />
<br />
if (rd^.run) then<br />
begin<br />
//* if we are running then we immediately request another tick... */<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 1000000 div 25;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
rd^.doRender := TRUE;<br />
end<br />
else<br />
begin<br />
//* ... if not we acknowledge that our tickRequest returned */<br />
tickRequestPending := FALSE;<br />
end;<br />
end;<br />
</source><br />
<br />
Only if we want to leave our main loop we set TickRequestPending to False instead, because we can now safely close the timer.device again.<br />
<br />
We also check whether we want to leave the loop but still have a tick-request pending in which case it must be canceled.:<br />
<br />
<source lang="pascal"><br />
if (not(rd^.run) and tickRequestPending) then<br />
begin<br />
//* We want to leave, but there is still a tick request pending? Let's abort it */<br />
AbortIO(PIORequest(@rd^.tickRequest));<br />
end;<br />
</source><br />
<br />
Now, if we compile and execute engine5.pas, we'll immediately see that our animation is now much more stable and smoother.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== More on timing ==<br />
<br />
Our current implementation has now reached the point where it will provide you with a maximum number of desired frames per second and that any other CPU time can be used by the system. However, the opposite has not yet been taken into account sufficiently: if we are unable to reach the desired frame rate, then our animation will also run slower as it simply advances a fixed amount of frames. This is undesired behavior: the animation will of course continue to stutter in the absence of a frame rate but if for example our rectangle makes a quarter rotation per second then it should always stay that way, whether we run with 50fps or only with 5fps. To accomplish this we still have to consider the factor time when drawing. We also retrieve this from timer.device by sending a corresponding request. Because our first request is already managed by timer.device and waits there until answered we have to create a second request. For this purpose we will fill rd^.timerIO() analogue to the initial first request. We also need to keep track of the time of our last frame. So we're going to expand our RenderEngineData with two entries:<br />
<br />
<source lang="pascal"><br />
getTimeRequest : Ttimerequest;<br />
lastRenderTime : Ttimeval;<br />
</source><br />
<br />
In RunEngine (), we'll initialize them:<br />
<br />
<source lang="pascal"><br />
//* setup our getTime request... */<br />
rd^.getTimeRequest := rd^.timerIO^;<br />
<br />
//* ... get the current time... */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* ... and initialize our lastRenderTime */<br />
rd^.lastRenderTime := rd^.getTimeRequest.tr_time;<br />
</source><br />
<br />
Now we have to rewrite our RenderBackbuffer() function: So far we have stubbornly increased our rectangular coordinates by a 1/32 of the output size per frame but now, instead, we want our coordinates to be incremented every four seconds by the output size. In order to make this as precise as possible we do not declare the current position as a word but as a floating point number.<br />
<br />
While FLOAT is a predefined type for c, it is not for Free Pascal so for consistency we declared a new FLOAT type first:<br />
<br />
<source lang="pascal"><br />
Type<br />
FLOAT = single;<br />
</source><br />
<br />
And define currentStep to be of type FLOAT:<br />
<br />
<source lang="pascal"><br />
currentStep : FLOAT;<br />
</source><br />
<br />
CurrentStep will now save the current position as a value between 0.0 and 1.0, where 0.0 will stand for the minimum coordinate and 1.0 for the maximum coordinate.<br />
<br />
To calculate this value, we need to determine the elapsed time per call to RenderBackbuffer() since the last frame. We do this by retrieving the current time and subtract the time of our last frame:<br />
<br />
<source lang="pascal"><br />
diff : Ttimeval;<br />
<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* get the time passed since our last render call */<br />
diff := rd^.getTimeRequest.tr_time;<br />
SubTime(@diff, @rd^.lastRenderTime);<br />
</source><br />
<br />
Diff now contains the difference in timeval format. This is a bit clumsy for our purposes, we'd rather have it as a floating point number in seconds: <br />
<br />
<source lang="pascal"><br />
secondsPassed : FLOAT;<br />
micros : ULONG;<br />
</source><br />
<br />
Therefor we also need to divide the complete number of microseconds in diff by 1000000.0:<br />
<br />
<source lang="pascal"><br />
if (diff.tv_secs <> 0) then<br />
begin<br />
micros := diff.tv_secs * 1000000;<br />
end<br />
else<br />
begin<br />
micros := 0;<br />
end;<br />
micros := micros + diff.tv_micro;<br />
secondsPassed := FLOAT(micros) / 1000000.0;<br />
</source><br />
<br />
Now we just need to increase currentStep by a quarter of secondsPassed so that we can reach our maximum value of 1.0 every four seconds. Once we have achieved this, we must deduct the absolute value from it. Because in practice we only expect an absolute value of 1.0, we do this with a simple subtraction. The while loop serves only as a safety net, just in case we somehow manage to get a value over 2.0; Normally we will only go through them once:<br />
<br />
<source lang="pascal"><br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
<br />
while (rd^.currentStep >= 1.0) do<br />
begin<br />
rd^.currentStep := rd^.currentStep - 1.0;<br />
end;<br />
</source><br />
<br />
Then we just have to insert the product from our maximum position and currentStep as current position:<br />
<br />
<source lang="pascal"><br />
pos.x := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.x)));<br />
pos.y := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.y)));<br />
</source><br />
<br />
If we start the executable then we can see a smooth rotating rectangle. Now if we would increase the system load (for example, by starting Engine6 several times) then we see that the animation starts to become choppy, but the rectangle rotates at the same speed.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Refactoring ==<br />
<br />
Actually, we're done now: We have an animation that is controlled by timer.device and which does not get disturbed even under full load. However, the different functions are very entangled. We want to separate those so that we are able to use the engine regardless of what we want to (re-)render. We also want to add a few smaller tweaks.<br />
<br />
So we want to isolate the renderer from our current code. This consists mainly of code for the drawing operations in RenderBackbuffer(), but also lastRenderTime and its logic belongs to it. Our renderer should also be able to determine the desired frame rate.<br />
<br />
The above makes it clear that our renderer at least consist of three functions:<br />
<br />
* InitRenderer() to create it and set the frame rate<br />
* RenderFrame() to draw a frame and<br />
* DestroyRenderer() to destroy the renderer again.<br />
<br />
We also want to make sure that both InitRenderer() and RenderFrame() can fail, so we use our integer return value that's proven to work. At DestroyRenderer() Nothing can go wrong (we cannot do any meaningful error handling here anyway), so this procedure remains:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(): integer;<br />
function RenderFrame(): integer;<br />
procedure DestroyRenderer();<br />
</source><br />
<br />
Naturally the renderer must also be able to store its own data during its lifetime. Therefor we enable this functionality and store the information inside the init function by passing it as a var pointer to the userdata and retrieve this information at RenderFrame() and DestroyRenderer() by passing the userdata as parameter:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer): integer;<br />
function RenderFrame(userData: pointer: integer;<br />
procedure DestroyRenderer(userData: pointer);<br />
</source><br />
<br />
It is useful to define a structure for the renderer data, which is then initialized in InitRenderer():<br />
<br />
<source lang="pascal"><br />
type<br />
PRendererData = ^TRendererData;<br />
TRendererData = <br />
record<br />
end;<br />
<br />
function InitRenderer(var userdata: pointer): integer;<br />
begin<br />
userData := AllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source> <br />
<br />
... and accordingly freed in DestroyRenderer():<br />
<br />
<source lang="pascal"><br />
procedure DestroyRenderer(userData: pointer);<br />
begin<br />
ExecFreeMem(userData, sizeof(TRendererData));<br />
end;<br />
</source><br />
<br />
In this renderer structure we copy our data from RenderEngineData too:<br />
<br />
<source lang="pascal"><br />
TRendererData = record<br />
lastRenderTime : Ttimeval;<br />
currentStep : FLOAT;<br />
end;<br />
</source><br />
<br />
To initialize this structure, we also need the current system time. And because we also want to set the framerate, our InitRenderer() looks like this:<br />
<br />
<source lang="pascal"><br />
function InitRenderer(var userdata: pointer; const sysTime: Ptimeval; refreshRate: Ptimeval): integer;<br />
var<br />
rd : PRendererData;<br />
begin<br />
//* allocate our user data */<br />
userData := ExecAllocMem(sizeof(TRendererData), MEMF_ANY or MEMF_CLEAR);<br />
if assigned(userData) then<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
//* set our lastRenderTime to now */<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
//* we would like to get a refresh rate of 25 frames per second */<br />
refreshRate^.tv_secs := 0;<br />
refreshRate^.tv_micro := 1000000 div 25;<br />
<br />
result := RETURN_OK;<br />
end<br />
else<br />
begin<br />
result := RETURN_ERROR;<br />
end;<br />
end;<br />
</source><br />
<br />
In RenderFrame we use our previous used drawing operations. We are also tinkering with an auxiliary function to convert the difference between the two timeval structures in seconds as a float:<br />
<br />
<source lang="pascal"><br />
function DiffInSeconds(const early: Ptimeval; const late: Ptimeval): FLOAT;<br />
var<br />
diff : Ttimeval;<br />
micros : ULONG;<br />
begin<br />
diff := late^;<br />
SubTime(@diff, Ptimeval(early));<br />
<br />
if (diff.tv_secs <> 0)<br />
then micros := diff.tv_secs * 1000000<br />
else micros := 0;<br />
micros := micros + diff.tv_micro;<br />
<br />
result := FLOAT(micros) / 1000000.0;<br />
end;<br />
</source><br />
<br />
RenderFrame() looks a bit more tidy:<br />
<br />
check this function, it is the wrong code as it was taken from engine7.pas while the workshop is still preparing things<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; const sysTime: Ptimeval): integer;<br />
var<br />
secondsPassed : FLOAT;<br />
pos : TPoint;<br />
maxPos : TPoint;<br />
rd : PRendererData;<br />
begin<br />
rd := PRendererData(userData);<br />
<br />
secondsPassed := DiffInSeconds(@rd^.lastRenderTime, sysTime);<br />
<br />
rd^.maxPos.x := renderTargetSize^.x - 1;<br />
rd^.maxPos.y := renderTargetSize^.y - 1;<br />
<br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
while (currentStep >= 1.0) do<br />
begin<br />
currentStep := currentStep - 1.0;<br />
end;<br />
<br />
//* now compute our new position */<br />
pos.x := SmallInt(trunc(rd^.currentStep * maxPosX));<br />
pos.y := SmallInt(trunc(rd^.currentStep * maxPosY));<br />
<br />
//* clear our bitmap */<br />
SetRast(renderTarget, 0);<br />
<br />
//* draw our rectangle */<br />
SetAPen(renderTarget, 1);<br />
GfxMove(renderTarget, 0 , LongInt(pos.y) );<br />
Draw(renderTarget , LongInt(maxPos.x - pos.x), 0 );<br />
Draw(renderTarget , LongInt(maxPos.x) , LongInt(maxPos.y - pos.y) );<br />
Draw(renderTarget , LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(renderTarget , 0 , LongInt(pos.y) );<br />
<br />
//* remember our render time */<br />
<br />
rd^.lastRenderTime := sysTime^;<br />
<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
Now we just have to make sure that these functions are called in the engine at the appropriate places. Because we also have to retrieve the current system time on multiple occasions, we write a separate function for it:<br />
<br />
<source lang="pascal"><br />
procedure UpdateTime(rd: PRenderEngineData);<br />
begin<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
end;<br />
</source><br />
<br />
If necessary, we read the system time from our getTimeRequest.<br />
<br />
In our RenderEngineData we also keep track of the desired refresh time and the UserData pointer for the renderer:<br />
<br />
<source lang="pascal"><br />
refreshRate : Ttimeval;<br />
userData : pointer;<br />
</source><br />
<br />
And we place the InitRenderer() call in RunEngine() immediately before opening our window when jumping into MainLoop():<br />
<br />
<source lang="pascal"><br />
//* get the current time... */<br />
UpdateTime(rd);<br />
<br />
//* ... and initialize our Renderer */<br />
result := InitRenderer(rd^.userData,<br />
@rd^.getTimeRequest.tr_time,<br />
@rd^.refreshRate);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
[...] //* open window and do MainLoop */<br />
<br />
DestroyRenderer(rd^.userData);<br />
end;<br />
</source><br />
<br />
RenderFrame() is then then simply called in RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize,<br />
@rd^.getTimeRequest.tr_time<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Directly before the call to RenderFrame() we obtain the current time and pass it to RenderFrame().<br />
<br />
This means that we have completely refactored our renderer logic into three functions. In a real program you could now <br />
conveniently place the remaining engine into a separate unit. As part of this workshop, however, we do not want to have to deal with setting up a project or makefile at this time (red: Free Pascal does not require makefiles and/or projects setup (that is, if you do not wish to do so), so it's very easy to store the engine into a separate unit, as long as the compiler is able to locate this unit (which is no problem if a unit is located at the same location as where the main program file is stored).<br />
<br />
Instead, there is still one more thing that is bothering us: it may very well be that a call to RenderFrame determines that it does not actually has to render anything because the contents of the frame hasn't changed since the last call. However, we need to tell whether the last frame is still present in the backbuffer (e.g. because in the meantime the back buffer had to be recreated), otherwise the frame always have to be redrawn. To accomplish this, we expand RenderFrame with two parameters:<br />
<br />
<source lang="pascal"><br />
function RenderFrame(userData: pointer; renderTarget: PRastPort; const renderTargetSize: PtPoint; renderTargetDirty: boolean; const sysTime: Ptimeval; var updateDone: Boolean): integer;<br />
</source><br />
<br />
So RenderTargetDirty lets the renderer know whether the last frame is still present in the renderTarget. UpdateDone informs the caller whether or not the renderer actually drew a frame in renderTarget.<br />
<br />
To determine whether the backbuffer always has to be redrawn or if the previous frame is still intact, our PrepareBackBuffer function can be used. Therefor we also need to expand this function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData; var backBufferDirty: boolean): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
[Allocate new bitmap code snippet...]<br />
<br />
backBufferDirty := TRUE;<br />
end<br />
else<br />
begin<br />
backBufferDirty := FALSE;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
So we set BackBufferDirty to true as soon as we had to create our bitmap again.<br />
<br />
Now we put these two function together in RenderBackBuffer() and return the DoRepaint of RenderBackbuffer():<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData; var doRepaint: boolean): integer;<br />
var<br />
backBufferDirty : boolean;<br />
begin<br />
result := PrepareBackBuffer(rd, backBufferDirty);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
UpdateTime(rd);<br />
<br />
result := RenderFrame<br />
(<br />
rd^.userData, @rd^.renderPort,<br />
@rd^.backBufferSize, backBufferDirty,<br />
@rd^.getTimeRequest.tr_time,<br />
doRepaint<br />
);<br />
end;<br />
end;<br />
</source><br />
<br />
Now all we have to do is update the rendercall in MainLoop():<br />
<br />
<source lang="pascal"><br />
var<br />
doRepaint: boolean;<br />
<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd, doRepaint);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, set repaint if required */<br />
if not(rd^.doRepaint) then<br />
begin<br />
rd^.doRepaint := doRepaint;<br />
end;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
end;<br />
</source><br />
<br />
It should be noted that we do not overwrite an already set RD^.DoRepaint with a false value.<br />
<br />
<br />
We have now reached our first goal: we have a window in which we can draw using double-buffering and were we can even change the frame rate which can accurately be controlled by timer.device.<br />
<br />
Engine7.pas is compiled with this call:<br />
<br />
<source lang="pascal"><br />
fpc engine7.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=867Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-24T19:21:11Z<p>Molly: /* Heading text */ Add content for chapter: More on timing</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
[...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Our image learns to walk ==<br />
<br />
Now we want to animate our image by drawing only one iteration of our loop in RenderBackbuffer() per frame. To accomplish this we store the current counter in our RenderEngineData structure.<br />
<br />
<source lang="pascal"><br />
currentStep : integer;<br />
</source><br />
<br />
The RenderBackbuffer() function is modified so that it only uses the current value for four lines per call, and then increments the value by one:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* setup our maximum coordinates and our step width */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
//* compute our current coordinates */<br />
pos.x := rd^.currentStep * lineStep.x;<br />
pos.y := rd^.currentStep * lineStep.y;<br />
<br />
//* increase our step for the next frame */<br />
rd^.currentStep := rd^.currentStep + 1;<br />
if (rd^.currentStep >= stepCount) then<br />
begin<br />
rd^.currentStep := 0;<br />
end;<br />
<br />
//* now draw our line pattern */<br />
SetAPen(rastPort, 1);<br />
GfxMove(rastPort, 0, SLONG(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0 );<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(rastPort, 0 , LongInt(pos.y) );<br />
end;<br />
end;<br />
</source><br />
<br />
As soon as currentStep becomes greater or equal to StepCount, we will have to reset the value back to 0 again.<br />
<br />
The only thing left is to make sure that our RenderBackbuffer () is called regularly. In order to accomplish this we ignore the DoRender flag inside our loop and simply always draw in this situation.<br />
<br />
The Wait() is replaced by a setsignal() and allows us to query if a message from the windows was received but without the need to wait for such a message to arrive.<br />
<br />
<source lang="pascal"><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
signals : ULONG;<br />
<br />
sig : ULONG;<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* combine it with the CTRL-C signal */<br />
signals := winSig or SIGBREAKF_CTRL_C;<br />
<br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
sig := SetSignal(0, signals);<br />
<br />
if (sig and winSig) <> 0 then<br />
begin<br />
//* our window signaled us, so let's harvest all its messages in a loop... */<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
<br />
if (sig and SIGBREAKF_CTRL_C) <> 0 then<br />
begin<br />
//* we leave on CTRL-C */<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
result := rd^.returnCode;<br />
end;<br />
</source><br />
<br />
Here is the source engine4.pas, that can be compiled again with<br />
<br />
<source lang="pascal"><br />
fpc engine4.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== What's timer.device got to do with it ==<br />
<br />
As expected, our window now shows an animation. However, in practice this implementation is far from ideal: On the one hand the speed of our animation directly dependents on the computing and graphical performance of the Amiga on which it runs and, on the other hand it will consume all the processor time that is available. So we need a different solution here. We could simply add a call to Delay() which at the least would sort out the problem with consuming CPU speed. However, if we want to display an animation with a single image every 2 seconds, it would cause the window to be blocked during those two seconds and as a result will respond very sluggish when clicking on the close button or other user actions. We would also need to take the time it takes to render and display into account and for that a simple Delay() is not suitable because of its 1/50-second resolution. We need another timer and timer.device provides one that is perfectly suited for our purposes.<br />
<br />
Like any device, timer.device is also controlled by exec functions OpenDevice()/CloseDevice() and SendIO()/WaitIO()/DoIO()/AbortIO(). Additionally timer.device has a small set of functions that are called in a similar way as functions from a library. These functions are declared inside unit timer and require an initialized base pointer as for any library.<br />
<br />
Note that unit timer already provides a variable named TimerBase for this purpose.<br />
<br />
As it should be for any device, also timer.device is controlled by sending IORequests. These IORequests are generated by us and not by the device itself and in order to send these back and forth we also need a MsgPort. Timer.device also defines its own IORequest structure which is a structure named timerequest and is located in unit timer. In addition to the IORequest, there is also a structure TTimeval defined which supports timing with a resolution of microseconds (1/1000000 seconds).<br />
<br />
Therefor we're going to expand our RenderEngineData structure, this time with a MsgPort and a timerequest:<br />
<br />
<source lang="pascal"><br />
timerPort : PMsgPort;<br />
timerIO : Ptimerequest;<br />
</source><br />
<br />
We also need to make sure the following two units are in our uses clause:<br />
<br />
<source lang="pascal"><br />
Uses<br />
Exec, Timer;<br />
</source><br />
<br />
We create our own function to open timer.device:<br />
<br />
<source lang="pascal"><br />
function InitTimerDevice(rd: PRenderEngineData): integer;<br />
begin<br />
//* we do not return success until we've opened the timer.device */<br />
result := RETURN_FAIL;<br />
<br />
//* create a message port through which we will communicate with the timer.device */<br />
rd^.timerPort := CreatePort(nil, 0);<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
//* create a timerequest which we will we pass between the timer.device and ourself */<br />
rd^.timerIO := Ptimerequest(CreateExtIO(rd^.timerPort, sizeof(Ttimerequest)));<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
//* open the timer.device */<br />
if (OpenDevice(TIMERNAME, UNIT_MICROHZ, PIORequest(rd^.timerIO), 0) = 0) then<br />
begin<br />
//* Success: let's set the TimerBase so we can call timer.device's functions */<br />
TimerBase := PLibrary(rd^.timerIO^.tr_node.io_Device);<br />
result := RETURN_OK;<br />
end;<br />
end;<br />
end;<br />
<br />
if (result <> RETURN_OK) then<br />
begin<br />
//* in case of an error: cleanup immediatly */<br />
FreeTimerDevice(rd);<br />
end;<br />
end;<br />
</source><br />
<br />
And a corresponding function FreeTimerDevice():<br />
<br />
<source lang="pascal"><br />
procedure FreeTimerDevice(rd: PRenderEngineData);<br />
begin<br />
//* close the timer.device */<br />
if Assigned(TimerBase) then<br />
begin<br />
CloseDevice(PIORequest(rd^.timerIO));<br />
TimerBase := nil;<br />
end;<br />
<br />
//* free our timerequest */<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
DeleteExtIO(PIORequest(rd^.timerIO));<br />
rd^.timerIO := nil;<br />
end;<br />
<br />
//* free our message port */<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
DeletePort(rd^.timerPort);<br />
rd^.timerPort := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
An additional short explanation for UNIT_MICROHZ used above: Timer.device offers several different modes, which mainly differ in their used resolution and accuracy. The mode we use here is characteristic for its high resolution. However it is also quite inaccurate: If you use it to count seconds then you'll notice that it will deviate from a price clock after a few minutes. However, this shortcoming is not important for our purpose.<br />
<br />
Anyhow, we call these functions immediately after the creation of our RenderEngineData and directly before its release respectively. Because we will send our timerequests asynchronously they are therefor not available for us at timer.device operations. Therefor we do not use our freshly created timerrequest but use it as a template only in order to retrieve the actual used request. For now we only need one for our timer, which we will also add to the RenderEngineData structure:<br />
<br />
<source lang="pascal"><br />
tickRequest : Ttimerequest;<br />
</source><br />
<br />
We initialize this field by simply assigning the contents of TimerIO to it:<br />
<br />
<source lang="pascal"><br />
rd^.tickRequest := rd^.timerIO^;<br />
</source><br />
<br />
Finally our RunEngine() that now looks like this:<br />
<br />
<source lang="pascal"><br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
//* allocate the memory for our runtime data and initialize it with zeros */<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
result := InitTimerDevice(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
//* setup our tick request */<br />
rd^.tickRequest := rd^.timerIO^;<br />
rd^.tickRequest.tr_node.io_Command := TR_ADDREQUEST;<br />
<br />
//* now let's open our window */<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
FreeTimerDevice(rd);<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
Inside Mainloop(), because we cannot leave our loop before a request is answered by timer.device, we have to remember whether or not the tickRequest is just waiting or not. We do this by using a simple boolean:<br />
<br />
<source lang="pascal"><br />
tickRequestPending : Boolean;<br />
</source><br />
<br />
We also need to expand our wait mask and include the TimerPort:<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the timer port */<br />
tickSig := 1 shl rd^.timerPort^.mp_SigBit;<br />
<br />
//* combine them to our final waitmask */<br />
signals := winSig or tickSig or SIGBREAKF_CTRL_C;<br />
</source><br />
<br />
We don't have to set DoRender to true, we'll do that later when we receive our ticks.<br />
<br />
Immediately before our main loop we send the first tick-request:<br />
<br />
<source lang="pascal"><br />
{* <br />
we start with a no-time request so we receive a tick immediately<br />
(we have to set 2 micros because of a bug in timer.device for 1.3) <br />
*}<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 2;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
tickRequestPending := TRUE;<br />
</source><br />
<br />
Instead of using setsignal () we are now waiting properly for the arrival of window messages or timers.device ticks:<br />
<br />
<source lang="pascal"><br />
sig := Wait(signals);<br />
</source><br />
<br />
When we receive a tick signal we send it immediately so that we can be signaled about a 1/25 second later:<br />
<br />
<source lang="pascal"><br />
if (sig and tickSig) <> 0 then<br />
begin<br />
//* our tickRequest signalled us, let's remove it from the replyport */<br />
WaitIO(PIORequest(@rd^.tickRequest));<br />
<br />
if (rd^.run) then<br />
begin<br />
//* if we are running then we immediately request another tick... */<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 1000000 div 25;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
rd^.doRender := TRUE;<br />
end<br />
else<br />
begin<br />
//* ... if not we acknowledge that our tickRequest returned */<br />
tickRequestPending := FALSE;<br />
end;<br />
end;<br />
</source><br />
<br />
Only if we want to leave our main loop we set TickRequestPending to False instead, because we can now safely close the timer.device again.<br />
<br />
We also check whether we want to leave the loop but still have a tick-request pending in which case it must be canceled.:<br />
<br />
<source lang="pascal"><br />
if (not(rd^.run) and tickRequestPending) then<br />
begin<br />
//* We want to leave, but there is still a tick request pending? Let's abort it */<br />
AbortIO(PIORequest(@rd^.tickRequest));<br />
end;<br />
</source><br />
<br />
Now, if we compile and execute engine5.pas, we'll immediately see that our animation is now much more stable and smoother.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== More on timing ==<br />
<br />
Our current implementation has now reached the point where it will provide you with a maximum number of desired frames per second and that any other CPU time can be used by the system. However, the opposite has not yet been taken into account sufficiently: if we are unable to reach the desired frame rate, then our animation will also run slower as it simply advances a fixed amount of frames. This is undesired behavior: the animation will of course continue to stutter in the absence of a frame rate but if for example our rectangle makes a quarter rotation per second then it should always stay that way, whether we run with 50fps or only with 5fps. To accomplish this we still have to consider the factor time when drawing. We also retrieve this from timer.device by sending a corresponding request. Because our first request is already managed by timer.device and waits there until answered we have to create a second request. For this purpose we will fill rd^.timerIO() analogue to the initial first request. We also need to keep track of the time of our last frame. So we're going to expand our RenderEngineData with two entries:<br />
<br />
<source lang="pascal"><br />
getTimeRequest : Ttimerequest;<br />
lastRenderTime : Ttimeval;<br />
</source><br />
<br />
In RunEngine (), we'll initialize them:<br />
<br />
<source lang="pascal"><br />
//* setup our getTime request... */<br />
rd^.getTimeRequest := rd^.timerIO^;<br />
<br />
//* ... get the current time... */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* ... and initialize our lastRenderTime */<br />
rd^.lastRenderTime := rd^.getTimeRequest.tr_time;<br />
</source><br />
<br />
Now we have to rewrite our RenderBackbuffer() function: So far we have stubbornly increased our rectangular coordinates by a 1/32 of the output size per frame but now, instead, we want our coordinates to be incremented every four seconds by the output size. In order to make this as precise as possible we do not declare the current position as a word but as a floating point number.<br />
<br />
While FLOAT is a predefined type for c, it is not for Free Pascal so for consistency we declared a new FLOAT type first:<br />
<br />
<source lang="pascal"><br />
Type<br />
FLOAT = single;<br />
</source><br />
<br />
And define currentStep to be of type FLOAT:<br />
<br />
<source lang="pascal"><br />
currentStep : FLOAT;<br />
</source><br />
<br />
CurrentStep will now save the current position as a value between 0.0 and 1.0, where 0.0 will stand for the minimum coordinate and 1.0 for the maximum coordinate.<br />
<br />
To calculate this value, we need to determine the elapsed time per call to RenderBackbuffer() since the last frame. We do this by retrieving the current time and subtract the time of our last frame:<br />
<br />
<source lang="pascal"><br />
diff : Ttimeval;<br />
<br />
//* get our current system time */<br />
rd^.getTimeRequest.tr_node.io_Command := TR_GETSYSTIME;<br />
rd^.getTimeRequest.tr_node.io_Flags := IOF_QUICK;<br />
DoIO(PIORequest(@rd^.getTimeRequest));<br />
<br />
//* get the time passed since our last render call */<br />
diff := rd^.getTimeRequest.tr_time;<br />
SubTime(@diff, @rd^.lastRenderTime);<br />
</source><br />
<br />
Diff now contains the difference in timeval format. This is a bit clumsy for our purposes, we'd rather have it as a floating point number in seconds: <br />
<br />
<source lang="pascal"><br />
secondsPassed : FLOAT;<br />
micros : ULONG;<br />
</source><br />
<br />
Therefor we also need to divide the complete number of microseconds in diff by 1000000.0:<br />
<br />
<source lang="pascal"><br />
if (diff.tv_secs <> 0) then<br />
begin<br />
micros := diff.tv_secs * 1000000;<br />
end<br />
else<br />
begin<br />
micros := 0;<br />
end;<br />
micros := micros + diff.tv_micro;<br />
secondsPassed := FLOAT(micros) / 1000000.0;<br />
</source><br />
<br />
Now we just need to increase currentStep by a quarter of secondsPassed so that we can reach our maximum value of 1.0 every four seconds. Once we have achieved this, we must deduct the absolute value from it. Because in practice we only expect an absolute value of 1.0, we do this with a simple subtraction. The while loop serves only as a safety net, just in case we somehow manage to get a value over 2.0; Normally we will only go through them once:<br />
<br />
<source lang="pascal"><br />
//* we do a quarter rotate every four seconds */<br />
rd^.currentStep := rd^.currentStep + (secondsPassed / 4.0);<br />
<br />
while (rd^.currentStep >= 1.0) do<br />
begin<br />
rd^.currentStep := rd^.currentStep - 1.0;<br />
end;<br />
</source><br />
<br />
Then we just have to insert the product from our maximum position and currentStep as current position:<br />
<br />
<source lang="pascal"><br />
pos.x := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.x)));<br />
pos.y := SmallInt(trunc(rd^.currentStep * FLOAT(maxPos.y)));<br />
</source><br />
<br />
If we start the executable then we can see a smooth rotating rectangle. Now if we would increase the system load (for example, by starting Engine6 several times) then we see that the animation starts to become choppy, but the rectangle rotates at the same speed.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=866Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-24T14:43:49Z<p>Molly: /* Heading text */ Add content for chapter: What's timer.device got to do with it</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
[...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Our image learns to walk ==<br />
<br />
Now we want to animate our image by drawing only one iteration of our loop in RenderBackbuffer() per frame. To accomplish this we store the current counter in our RenderEngineData structure.<br />
<br />
<source lang="pascal"><br />
currentStep : integer;<br />
</source><br />
<br />
The RenderBackbuffer() function is modified so that it only uses the current value for four lines per call, and then increments the value by one:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* setup our maximum coordinates and our step width */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
//* compute our current coordinates */<br />
pos.x := rd^.currentStep * lineStep.x;<br />
pos.y := rd^.currentStep * lineStep.y;<br />
<br />
//* increase our step for the next frame */<br />
rd^.currentStep := rd^.currentStep + 1;<br />
if (rd^.currentStep >= stepCount) then<br />
begin<br />
rd^.currentStep := 0;<br />
end;<br />
<br />
//* now draw our line pattern */<br />
SetAPen(rastPort, 1);<br />
GfxMove(rastPort, 0, SLONG(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0 );<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(rastPort, 0 , LongInt(pos.y) );<br />
end;<br />
end;<br />
</source><br />
<br />
As soon as currentStep becomes greater or equal to StepCount, we will have to reset the value back to 0 again.<br />
<br />
The only thing left is to make sure that our RenderBackbuffer () is called regularly. In order to accomplish this we ignore the DoRender flag inside our loop and simply always draw in this situation.<br />
<br />
The Wait() is replaced by a setsignal() and allows us to query if a message from the windows was received but without the need to wait for such a message to arrive.<br />
<br />
<source lang="pascal"><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
signals : ULONG;<br />
<br />
sig : ULONG;<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* combine it with the CTRL-C signal */<br />
signals := winSig or SIGBREAKF_CTRL_C;<br />
<br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
sig := SetSignal(0, signals);<br />
<br />
if (sig and winSig) <> 0 then<br />
begin<br />
//* our window signaled us, so let's harvest all its messages in a loop... */<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
<br />
if (sig and SIGBREAKF_CTRL_C) <> 0 then<br />
begin<br />
//* we leave on CTRL-C */<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
result := rd^.returnCode;<br />
end;<br />
</source><br />
<br />
Here is the source engine4.pas, that can be compiled again with<br />
<br />
<source lang="pascal"><br />
fpc engine4.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== What's timer.device got to do with it ==<br />
<br />
As expected, our window now shows an animation. However, in practice this implementation is far from ideal: On the one hand the speed of our animation directly dependents on the computing and graphical performance of the Amiga on which it runs and, on the other hand it will consume all the processor time that is available. So we need a different solution here. We could simply add a call to Delay() which at the least would sort out the problem with consuming CPU speed. However, if we want to display an animation with a single image every 2 seconds, it would cause the window to be blocked during those two seconds and as a result will respond very sluggish when clicking on the close button or other user actions. We would also need to take the time it takes to render and display into account and for that a simple Delay() is not suitable because of its 1/50-second resolution. We need another timer and timer.device provides one that is perfectly suited for our purposes.<br />
<br />
Like any device, timer.device is also controlled by exec functions OpenDevice()/CloseDevice() and SendIO()/WaitIO()/DoIO()/AbortIO(). Additionally timer.device has a small set of functions that are called in a similar way as functions from a library. These functions are declared inside unit timer and require an initialized base pointer as for any library.<br />
<br />
Note that unit timer already provides a variable named TimerBase for this purpose.<br />
<br />
As it should be for any device, also timer.device is controlled by sending IORequests. These IORequests are generated by us and not by the device itself and in order to send these back and forth we also need a MsgPort. Timer.device also defines its own IORequest structure which is a structure named timerequest and is located in unit timer. In addition to the IORequest, there is also a structure TTimeval defined which supports timing with a resolution of microseconds (1/1000000 seconds).<br />
<br />
Therefor we're going to expand our RenderEngineData structure, this time with a MsgPort and a timerequest:<br />
<br />
<source lang="pascal"><br />
timerPort : PMsgPort;<br />
timerIO : Ptimerequest;<br />
</source><br />
<br />
We also need to make sure the following two units are in our uses clause:<br />
<br />
<source lang="pascal"><br />
Uses<br />
Exec, Timer;<br />
</source><br />
<br />
We create our own function to open timer.device:<br />
<br />
<source lang="pascal"><br />
function InitTimerDevice(rd: PRenderEngineData): integer;<br />
begin<br />
//* we do not return success until we've opened the timer.device */<br />
result := RETURN_FAIL;<br />
<br />
//* create a message port through which we will communicate with the timer.device */<br />
rd^.timerPort := CreatePort(nil, 0);<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
//* create a timerequest which we will we pass between the timer.device and ourself */<br />
rd^.timerIO := Ptimerequest(CreateExtIO(rd^.timerPort, sizeof(Ttimerequest)));<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
//* open the timer.device */<br />
if (OpenDevice(TIMERNAME, UNIT_MICROHZ, PIORequest(rd^.timerIO), 0) = 0) then<br />
begin<br />
//* Success: let's set the TimerBase so we can call timer.device's functions */<br />
TimerBase := PLibrary(rd^.timerIO^.tr_node.io_Device);<br />
result := RETURN_OK;<br />
end;<br />
end;<br />
end;<br />
<br />
if (result <> RETURN_OK) then<br />
begin<br />
//* in case of an error: cleanup immediatly */<br />
FreeTimerDevice(rd);<br />
end;<br />
end;<br />
</source><br />
<br />
And a corresponding function FreeTimerDevice():<br />
<br />
<source lang="pascal"><br />
procedure FreeTimerDevice(rd: PRenderEngineData);<br />
begin<br />
//* close the timer.device */<br />
if Assigned(TimerBase) then<br />
begin<br />
CloseDevice(PIORequest(rd^.timerIO));<br />
TimerBase := nil;<br />
end;<br />
<br />
//* free our timerequest */<br />
if Assigned(rd^.timerIO) then<br />
begin<br />
DeleteExtIO(PIORequest(rd^.timerIO));<br />
rd^.timerIO := nil;<br />
end;<br />
<br />
//* free our message port */<br />
if Assigned(rd^.timerPort) then<br />
begin<br />
DeletePort(rd^.timerPort);<br />
rd^.timerPort := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
An additional short explanation for UNIT_MICROHZ used above: Timer.device offers several different modes, which mainly differ in their used resolution and accuracy. The mode we use here is characteristic for its high resolution. However it is also quite inaccurate: If you use it to count seconds then you'll notice that it will deviate from a price clock after a few minutes. However, this shortcoming is not important for our purpose.<br />
<br />
Anyhow, we call these functions immediately after the creation of our RenderEngineData and directly before its release respectively. Because we will send our timerequests asynchronously they are therefor not available for us at timer.device operations. Therefor we do not use our freshly created timerrequest but use it as a template only in order to retrieve the actual used request. For now we only need one for our timer, which we will also add to the RenderEngineData structure:<br />
<br />
<source lang="pascal"><br />
tickRequest : Ttimerequest;<br />
</source><br />
<br />
We initialize this field by simply assigning the contents of TimerIO to it:<br />
<br />
<source lang="pascal"><br />
rd^.tickRequest := rd^.timerIO^;<br />
</source><br />
<br />
Finally our RunEngine() that now looks like this:<br />
<br />
<source lang="pascal"><br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
//* allocate the memory for our runtime data and initialize it with zeros */<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
result := InitTimerDevice(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
//* setup our tick request */<br />
rd^.tickRequest := rd^.timerIO^;<br />
rd^.tickRequest.tr_node.io_Command := TR_ADDREQUEST;<br />
<br />
//* now let's open our window */<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
FreeTimerDevice(rd);<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
Inside Mainloop(), because we cannot leave our loop before a request is answered by timer.device, we have to remember whether or not the tickRequest is just waiting or not. We do this by using a simple boolean:<br />
<br />
<source lang="pascal"><br />
tickRequestPending : Boolean;<br />
</source><br />
<br />
We also need to expand our wait mask and include the TimerPort:<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the timer port */<br />
tickSig := 1 shl rd^.timerPort^.mp_SigBit;<br />
<br />
//* combine them to our final waitmask */<br />
signals := winSig or tickSig or SIGBREAKF_CTRL_C;<br />
</source><br />
<br />
We don't have to set DoRender to true, we'll do that later when we receive our ticks.<br />
<br />
Immediately before our main loop we send the first tick-request:<br />
<br />
<source lang="pascal"><br />
{* <br />
we start with a no-time request so we receive a tick immediately<br />
(we have to set 2 micros because of a bug in timer.device for 1.3) <br />
*}<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 2;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
tickRequestPending := TRUE;<br />
</source><br />
<br />
Instead of using setsignal () we are now waiting properly for the arrival of window messages or timers.device ticks:<br />
<br />
<source lang="pascal"><br />
sig := Wait(signals);<br />
</source><br />
<br />
When we receive a tick signal we send it immediately so that we can be signaled about a 1/25 second later:<br />
<br />
<source lang="pascal"><br />
if (sig and tickSig) <> 0 then<br />
begin<br />
//* our tickRequest signalled us, let's remove it from the replyport */<br />
WaitIO(PIORequest(@rd^.tickRequest));<br />
<br />
if (rd^.run) then<br />
begin<br />
//* if we are running then we immediately request another tick... */<br />
rd^.tickRequest.tr_time.tv_secs := 0;<br />
rd^.tickRequest.tr_time.tv_micro := 1000000 div 25;<br />
SendIO(PIORequest(@rd^.tickRequest));<br />
rd^.doRender := TRUE;<br />
end<br />
else<br />
begin<br />
//* ... if not we acknowledge that our tickRequest returned */<br />
tickRequestPending := FALSE;<br />
end;<br />
end;<br />
</source><br />
<br />
Only if we want to leave our main loop we set TickRequestPending to False instead, because we can now safely close the timer.device again.<br />
<br />
We also check whether we want to leave the loop but still have a tick-request pending in which case it must be canceled.:<br />
<br />
<source lang="pascal"><br />
if (not(rd^.run) and tickRequestPending) then<br />
begin<br />
//* We want to leave, but there is still a tick request pending? Let's abort it */<br />
AbortIO(PIORequest(@rd^.tickRequest));<br />
end;<br />
</source><br />
<br />
Now, if we compile and execute engine5.pas, we'll immediately see that our animation is now much more stable and smoother.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=File:HighSpeed_Pascal_1.10.jpg&diff=865File:HighSpeed Pascal 1.10.jpg2017-09-23T22:56:47Z<p>Molly: HiSoft's HighSpeed Pascal 1.10 screenshot</p>
<hr />
<div>HiSoft's HighSpeed Pascal 1.10 screenshot</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=File:FPClogogif.gif&diff=864File:FPClogogif.gif2017-09-23T22:03:16Z<p>Molly: Free Pascal Logo</p>
<hr />
<div>Free Pascal Logo</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=863Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-23T18:03:26Z<p>Molly: /* Heading text */ Add content for chapter: Our image learns to walk</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
[...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Our image learns to walk ==<br />
<br />
Now we want to animate our image by drawing only one iteration of our loop in RenderBackbuffer() per frame. To accomplish this we store the current counter in our RenderEngineData structure.<br />
<br />
<source lang="pascal"><br />
currentStep : integer;<br />
</source><br />
<br />
The RenderBackbuffer() function is modified so that it only uses the current value for four lines per call, and then increments the value by one:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* setup our maximum coordinates and our step width */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
//* compute our current coordinates */<br />
pos.x := rd^.currentStep * lineStep.x;<br />
pos.y := rd^.currentStep * lineStep.y;<br />
<br />
//* increase our step for the next frame */<br />
rd^.currentStep := rd^.currentStep + 1;<br />
if (rd^.currentStep >= stepCount) then<br />
begin<br />
rd^.currentStep := 0;<br />
end;<br />
<br />
//* now draw our line pattern */<br />
SetAPen(rastPort, 1);<br />
GfxMove(rastPort, 0, SLONG(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0 );<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y) );<br />
Draw(rastPort, 0 , LongInt(pos.y) );<br />
end;<br />
end;<br />
</source><br />
<br />
As soon as currentStep becomes greater or equal to StepCount, we will have to reset the value back to 0 again.<br />
<br />
The only thing left is to make sure that our RenderBackbuffer () is called regularly. In order to accomplish this we ignore the DoRender flag inside our loop and simply always draw in this situation.<br />
<br />
The Wait() is replaced by a setsignal() and allows us to query if a message from the windows was received but without the need to wait for such a message to arrive.<br />
<br />
<source lang="pascal"><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
signals : ULONG;<br />
<br />
sig : ULONG;<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* combine it with the CTRL-C signal */<br />
signals := winSig or SIGBREAKF_CTRL_C;<br />
<br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint.. */<br />
rd^.doRepaint := FALSE;<br />
<br />
//* but signal ourself to leave instead */<br />
Signal(FindTask(nil), SIGBREAKF_CTRL_C);<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
sig := SetSignal(0, signals);<br />
<br />
if (sig and winSig) <> 0 then<br />
begin<br />
//* our window signaled us, so let's harvest all its messages in a loop... */<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
<br />
if (sig and SIGBREAKF_CTRL_C) <> 0 then<br />
begin<br />
//* we leave on CTRL-C */<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
result := rd^.returnCode;<br />
end;<br />
</source><br />
<br />
Here is the source engine4.pas, that can be compiled again with<br />
<br />
<source lang="pascal"><br />
fpc engine4.pas<br />
</source><br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=862Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-23T05:26:12Z<p>Molly: /* Heading text */ Add content for Chapter: Foolproof error-handling</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Foolproof error-handling ==<br />
<br />
Now that our code contains parts that can fail our program at runtime, we have to take care of proper error-handling. We want to be able to exit our program at any time, leaving things in a clean state and return the error code.<br />
<br />
Leaving things in a clean state means that we will have to handle all pending messages of our window without crashing and release all allocated resources.<br />
<br />
To accomplish this task, we will once again expand our RenderEngineData structure, this time with a return code that we can use for a error case inside our MainLoop() and to return the error code at program exit:<br />
<br />
<source lang="pascal"><br />
returnCode: integer;<br />
</source><br />
<br />
In order to facilitate the error handling we want our code to call our render function from a single location only, and immediately before our call to Wait(). In order to be able determine whether or not the routine should be called we add a flag to our RenderEngineData structure. We also implement something similar for RepaintWindow():<br />
<br />
<source lang="pascal"><br />
doRepaint : boolean;<br />
doRender : boolean;<br />
</source><br />
<br />
This way we can simply set DoRender to true to render our image just before entering the MainLoop. This is how the beginning of our MainLoop looks like: <br />
<br />
<source lang="pascal"><br />
//* paint our window for the first time */<br />
rd^.doRender := TRUE;<br />
<br />
//* we need to compute our output size initially */<br />
ComputeOutputSize(rd);<br />
<br />
//* enter our main loop */<br />
while (rd^.run) do<br />
begin<br />
if (rd^.doRender) then<br />
begin<br />
rd^.returnCode := RenderBackbuffer(rd);<br />
if (rd^.returnCode = RETURN_OK) then<br />
begin<br />
//* Rendering succeeded, we need to repaint */<br />
rd^.doRepaint := TRUE;<br />
rd^.doRender := FALSE;<br />
end<br />
else<br />
begin<br />
//* Rendering failed, do not repaint, leave our main loop instead */<br />
rd^.doRepaint := FALSE;<br />
rd^.run := FALSE;<br />
end;<br />
end;<br />
<br />
if (rd^.doRepaint) then<br />
begin<br />
RepaintWindow(rd);<br />
rd^.doRepaint := FALSE;<br />
end;<br />
<br />
if (rd^.run) then<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
end;<br />
[...]<br />
</source><br />
<br />
And this shows our IDCMP_NEWSIZE handler in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
IDCMP_NEWSIZE:<br />
begin<br />
//* On resize we compute our new output size... */<br />
ComputeOutputSize(rd);<br />
<br />
//* ... and trigger a render call */<br />
rd^.doRender := TRUE;<br />
end;<br />
</source><br />
<br />
When we compile and execute engine3.pas we will have reinstated our window and line pattern. However, this time without flickering when the image is redrawing - a condition that will proof to be very helpful for our next steps to animate things.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=861Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-22T22:52:09Z<p>Molly: /* Heading text */ Add content for chapter: Double-buffering</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Double-buffering ==<br />
<br />
We solve this problem by drawing on a second invisible bitmap first - a so-called backbuffer - and only when we finished drawing the image we replace the previous one with the new one as soon as possible. This technique is called double-buffering. The exchange itself can be done using different methods: when we display our image on a separate screen, then we can simply say to the graphics chip that it should display the second image now. This happens very quickly and without much effort. However, we are running on the workbench in a window and therefore we have to use a different method: we copy the new image over the old one. This is more complex and slower, however it is still fast enough for our purpose, especially because graphics.library can use the blitter for this. Btw "Blit" stands for "Block Image Transfer", so that it should be possible to understand where the blitter got its name from. In the context of this Workshop we will address this process as "blit" as well.<br />
<br />
So we have to create a second bitmap and its Bitplanes now. Inside graphics.library there is a nice function that is able to do this and which is named AllocBitmap() . Unfortunately, this function is only available since OS 3.0. For previous versions of the OS we have to create the bitmap and allocate the bitplanes manually. In case there are smart readers amongst us that have come up with the idea to simply implement this by manually creating two bitmaps to never look at AllocBitmap() again, then such reader will deceive itself: AllocBitMap() specifically creates bitmaps depending on the graphics card which results in bitmaps that can be drawn faster and can blit faster to the display and should therefor always be used from OS 3.0 and onwards. That is why we implement our own AllocBitmap(), which uses one or the other method depending on the version of the OS:<br />
<br />
<source lang="pascal"><br />
function MyAllocBitMap(width: ULONG; height: ULONG; depth: ULONG; likeBitMap: PBitMap): PBitMap;<br />
var<br />
bitmap: PBitMap;<br />
i : SWORD;<br />
begin<br />
//* AllocBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* sanity check */<br />
if (depth <= 8) then<br />
begin<br />
//* let's allocate our BitMap */<br />
bitmap := PBitMap(ExecAllocMem(sizeof(TBitMap), MEMF_ANY or MEMF_CLEAR));<br />
if Assigned(bitmap) then<br />
begin<br />
InitBitMap(bitmap, depth, width, height);<br />
<br />
//* now allocate all our bitplanes */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
bitmap^.Planes[i] := AllocRaster(width, height);<br />
if not Assigned(bitmap^.Planes[i]) then<br />
begin<br />
MyFreeBitMap(bitmap);<br />
bitmap := nil;<br />
break;<br />
end;<br />
end;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := nil;<br />
end;<br />
end<br />
else<br />
begin<br />
bitmap := AllocBitMap(width, height, depth, 0, likeBitMap);<br />
end;<br />
<br />
result := bitmap;<br />
end;<br />
</source><br />
<br />
In a similar fashion, we also need a MyFreeBitmap():<br />
<br />
<source lang="pascal"><br />
procedure MyFreeBitMap(bitmap: PBitMap);<br />
var<br />
width : ULONG;<br />
i : integer;<br />
begin<br />
//* FreeBitMap() is available since OS3.0 */<br />
if (GfxBase^.LibNode.lib_Version < 39) then<br />
begin<br />
//* warning: this assumption is only safe for our own bitmaps */<br />
width := bitmap^.BytesPerRow * 8;<br />
<br />
//* free all the bitplanes... */<br />
for i := 0 to Pred(bitmap^.Depth) do<br />
begin<br />
if Assigned(bitmap^.Planes[i]) then<br />
begin<br />
FreeRaster(bitmap^.Planes[i], width, ULONG(bitmap^.Rows));<br />
bitmap^.Planes[i] := nil;<br />
end;<br />
end;<br />
//* ... and finally free the bitmap itself */<br />
ExecFreeMem(bitmap, sizeof(TBitMap));<br />
end<br />
else<br />
begin<br />
FreeBitMap(bitmap);<br />
end;<br />
end;<br />
</source><br />
<br />
Then we need to expand our RenderEngineStruct. First we'll store the output size in the window:<br />
<br />
<source lang="pascal"><br />
outputSize : TPoint;<br />
</source><br />
<br />
In order to calculate the correct values, we write a small function:<br />
<br />
<source lang="pascal"><br />
procedure ComputeOutputSize(rd: PRenderEngineData);<br />
begin<br />
//* our output size is simply the window's size minus its borders */<br />
rd^.outputSize.x :=<br />
rd^.window^.Width - rd^.window^.BorderLeft - rd^.window^.BorderRight;<br />
rd^.outputSize.y :=<br />
rd^.window^.Height - rd^.window^.BorderTop - rd^.window^.BorderBottom;<br />
end;<br />
</source><br />
<br />
We call this function once after opening the window and every time when we receive a IDCMP_NEWSIZE message.<br />
<br />
Of course we still need our bitmap, its current size and a corresponding RastPort for our RenderEngineStruct:<br />
<br />
<source lang="pascal"><br />
backBuffer: PBitMap;<br />
backBufferSize: TPoint;<br />
renderPort: TRastPort;<br />
</source><br />
<br />
In order to create a backbuffer or adapt it to a new size, we also implement this in a function:<br />
<br />
<source lang="pascal"><br />
function PrepareBackBuffer(rd: PRenderEngineData): integer;<br />
begin<br />
if ( (rd^.outputSize.x <> rd^.backBufferSize.x) or<br />
(rd^.outputSize.y <> rd^.backBufferSize.y) ) then<br />
begin<br />
//* if output size changed free our current bitmap... */<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
MyFreeBitMap(rd^.backBuffer);<br />
rd^.backBuffer := nil;<br />
end;<br />
<br />
//* ... allocate a new one... */<br />
rd^.backBuffer := MyAllocBitMap(ULONG(rd^.outputSize.x),<br />
ULONG(rd^.outputSize.y),<br />
1, rd^.window^.RPort^.BitMap);<br />
if Assigned(rd^.backBuffer) then<br />
begin<br />
//* and on success remember its size */<br />
rd^.backBufferSize := rd^.outputSize;<br />
end;<br />
<br />
//* link the bitmap into our render port */<br />
InitRastPort(@rd^.renderPort);<br />
rd^.renderPort.BitMap := rd^.backBuffer;<br />
end;<br />
<br />
if Assigned(rd^.backBuffer)<br />
then result := RETURN_OK<br />
else result := RETURN_ERROR;<br />
end;<br />
</source><br />
<br />
As can be seen, this function can fail at runtime if, for some reason, the bitmap can't be created. Later we will go into the details on how to handle this situation. For the moment it is enough to return an error code in case such a situation occurs.<br />
<br />
Our RepaintWindow() function will only blit the backbuffer in the RastPort of our window:<br />
<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
begin<br />
//* on repaint we simply blit our backbuffer into our window's RastPort */<br />
BltBitMapRastPort<br />
(<br />
rd^.backBuffer, 0, 0, rd^.window^.RPort,<br />
LongInt(rd^.window^.BorderLeft),<br />
LongInt(rd^.window^.BorderTop),<br />
LongInt(rd^.outputSize.x), LongInt(rd^.outputSize.y),<br />
(ABNC or ABC)<br />
);<br />
end;<br />
</source><br />
<br />
The previously used drawing functions are migrated into a separate function:<br />
<br />
<source lang="pascal"><br />
function RenderBackbuffer(rd: PRenderEngineData): integer;<br />
var<br />
rastPort : PRastPort;<br />
maxpos : TPoint;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
result := PrepareBackBuffer(rd);<br />
<br />
if (result = RETURN_OK) then<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := @rd^.renderPort;<br />
<br />
//* clear our bitmap */<br />
SetRast(rastPort, 0);<br />
<br />
//* now draw our line pattern */<br />
maxPos.x := rd^.backBufferSize.x - 1;<br />
maxPos.y := rd^.backBufferSize.y - 1;<br />
<br />
lineStep.x := maxPos.x div stepCount;<br />
lineStep.y := maxPos.y div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0; pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, 0, LongInt(pos.y));<br />
Draw(rastPort, LongInt(maxPos.x - pos.x), 0);<br />
Draw(rastPort, LongInt(maxPos.x) , LongInt(maxPos.y - pos.y));<br />
Draw(rastPort, LongInt(pos.x) , LongInt(maxPos.y));<br />
Draw(rastPort, 0 , LongInt(pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
This function can also fail because it calls PrepareBackBuffer(). For now, we return the error code.<br />
Also note that because we have our own bitmap with corresponding RastPort, that we no longer have to pay attention to the window frames, so that we could replace the RectFill() with a SetRast().<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=860Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-21T21:36:24Z<p>Molly: /* We're finally going to draw something */ forgotten note about picture.</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=859Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-21T21:34:57Z<p>Molly: /* Heading text */ Add content for chapter: We're finally going to draw something</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== We're finally going to draw something ==<br />
<br />
With this knowledge in mind we now return to our window and its messages again: we have to draw our graphics on several occasions:<br />
<br />
* First, immediately after the window opens, so that initially the graphic is displayed at all.<br />
* Then, when a IDCMP_REFRESHWINDOW message is recieved, to refresh those regions on the window that are destroyed.<br />
* And of course also after the user has changed the size of the window.<br />
<br />
It is therefore logical that we implement our drawing functionality into a separate function that we can call when needed:<br />
<source lang="pascal"><br />
procedure RepaintWindow(rd: PRenderEngineData);<br />
var<br />
rastPort : PRastPort;<br />
outputRect : TRectangle;<br />
lineStep : TPoint;<br />
pos : TPoint;<br />
i : integer;<br />
const<br />
stepCount = 32;<br />
begin<br />
//* we make a local copy of our RastPort pointer for ease of use */<br />
rastPort := rd^.window^.RPort;<br />
<br />
//* our output rectangle is our whole window area minus its borders */<br />
outputRect.MinY := rd^.window^.BorderTop;<br />
outputRect.MinX := rd^.window^.BorderLeft;<br />
outputRect.MaxX := rd^.window^.Width - rd^.window^.BorderRight - 1;<br />
outputRect.MaxY := rd^.window^.Height - rd^.window^.BorderBottom - 1;<br />
<br />
//* clear our output rectangle */<br />
SetDrMd(rastPort, JAM1);<br />
SetAPen(rastPort, 0);<br />
RectFill(rastPort, LongInt(outputRect.MinX), LongInt(outputRect.MinY),<br />
LongInt(outputRect.MaxX), LongInt(outputRect.MaxY));<br />
<br />
//* now draw our line pattern */<br />
lineStep.x := (outputRect.MaxX - outputRect.MinX) div stepCount;<br />
lineStep.y := (outputRect.MaxY - outputRect.MinY) div stepCount;<br />
<br />
SetAPen(rastPort, 1);<br />
pos.x := 0;<br />
pos.y := 0;<br />
for i := 0 to Pred(stepCount) do<br />
begin<br />
GfxMove(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
Draw(rastPort, LongInt(outputRect.MaxX - pos.x), LongInt(outputRect.MinY ));<br />
Draw(rastPort, LongInt(outputRect.MaxX) , LongInt(outputRect.MaxY - pos.y));<br />
Draw(rastPort, LongInt(outputRect.MinX + pos.x), LongInt(outputRect.MaxY ));<br />
Draw(rastPort, LongInt(outputRect.MinX) , LongInt(outputRect.MinY + pos.y));<br />
<br />
pos.x := pos.x + lineStep.x;<br />
pos.y := pos.y + lineStep.y;<br />
end;<br />
end;<br />
</source><br />
<br />
First we determine the output rectangle by subtracting the corresponding borders fro the window width and height. This rectangle is then completely deleted by RectFill().After that we just paint a few lines with the Draw() function. Note that when lines are drawn that we only specify the end point. The starting point is stored in the RastPort and either can be set by Move() or act as end point for/to ? a previous drawing operation.<br />
<br />
Now we have to make sure that our repaint function is invoked at the appropriate locations:<br />
The first time immediately before we go into the main loop in MainLoop():<br />
<br />
<source lang="pascal"><br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* paint our window for the first time */<br />
RepaintWindow(rd);<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
</source><br />
<br />
Then at two places in DispatchWindowMessage():<br />
<br />
<source lang="pascal"><br />
case (msg^.IClass) of<br />
IDCMP_NEWSIZE:<br />
begin<br />
RepaintWindow(rd);<br />
end;<br />
<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
RepaintWindow(rd);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
</source><br />
<br />
Here i make a brief note with regards to a peculiarity of Beginrefresh() and Rerefresh (): The thought there is that only those parts of the window are redrawn that were initially obscured and are now made visible because the user moved the window. To accomplish this the layers.library is used and allows to perform drawing operations on other areas of our bitmap. This may significantly increase the speed of the redrawing, but can also cause problems with animations when you paint a "newer" image as parts of the old image persist and are not refreshed.<br />
<br />
For the functions SetAPen(), SetDrMd(), GfxMove() and Draw(), we also need to add a Unit:<br />
<br />
<source lang="pascal"><br />
Uses<br />
AGraphics;<br />
</source><br />
<br />
When we compile engine2.pas with:<br />
<br />
<source lang="pascal"><br />
fpc engine2.pas<br />
</source><br />
<br />
and execute it, we get a window in which a line pattern is drawn. When we resize the window, the graphics will also be adjusted to the new window size. However, on slow computers we can encounter the effect that you can 'see' (red: follow ?) the drawing operations: It is not very dramatic yet but there is a visible flicker when redrawing, because the lines appear after the background is deleted first. For now this is not too annoying yet, but we would like to play an animation, and for animations this is not acceptable behavior. The image must be visible at once.<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=858Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-20T05:46:20Z<p>Molly: /* Heading text */ Add content for chapter: Bitplane, BitMap and RastPort</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Bitplane, BitMap and RastPort ==<br />
<br />
So how do we actually draw into our window ? In order to accomplish this, i would like to take a few steps back first and clarify some of the terminology used by graphics.library.<br />
<br />
First there is the graphics memory itself. For the classic graphics.library, this is always arranged in planar format, meaning that depending of the number of colors we have a corresponding number of bitplanes where each single bit represents a pixel. This allows for maximum flexibility in terms of the desired number of colors, but the downside is that drawing operations are quite complicated because for every pixel you need to read one byte for each bitplane, perform a and/or operation and write back the byte again. Even if the graphics memory is located in chip RAM, then performing slow actions on it slow things down even further because access to this kind of memory is slow to begin with. Initially we do not have to worry about these things, because graphics.library will handle this for us but, if you need fast and up-to-date graphics then it is difficult to circumvent the use of a chunky2planar-routine. But for now, this topic will not be an issue.<br />
<br />
In order for graphics.library to determine which Bitplanes actually belong to a image as well as be able to tell what its dimensions are, there is a record structure TBitmap declared in agraphics.pas:<br />
<br />
type<br />
TBitMap = record<br />
BytesPerRow: Word;<br />
Rows: Word;<br />
Flags: Byte;<br />
Depth: Byte;<br />
Pad: Word;<br />
Planes: array[0..7] of TPlanePtr;<br />
end;<br />
<br />
''BytesPerRow'' specifies how many bytes per line are used. More specifically, it is the number of bytes you have to add to a point in order to locate the pixel from the same column in the next row. Because there are no half-bytes this means that the width in pixels is always a multiple of 8. Due to some characteristics of the Amiga chipset, this number is in practise even a multiple of 16, e.g. the memory usage of a bitmap with a width of 33 pixels is identical to that of a bitmap with a width of 48 pixels.<br />
<br />
''rows'' specifies the number of lines and thus directly corresponds to the height of a bitmap in pixels.<br />
<br />
''depth'' specifies the number of Bitplanes and thus corresponds to the available color depth: With only one Bitplane you have two colors, with eight Bitplanes 256.<br />
<br />
''Planes'' is an array of addresses that point to our Bitplanes in memory. Although the size of the array is defined with 8, one must not rely - at least with bitmaps that you have not created yourself - that there are actually eight addresses available. <br />
<br />
For a bitmap with a depth of 2, it may very well be that the memory for the last 6 Bitplane pointers is not allocated. This means that when you access this array, you always have to take the depth into consideration: if depth is 2, then you must not access planes [2] - not even to test whether the pointer is nil ! Planes that are outside the range specified by depth are considered non-existent !<br />
<br />
Also assignments in the form of:<br />
<source lang="pascal"><br />
var <br />
bmp: TBitmap;<br />
begin<br />
bmp:= foreignBmp^;<br />
end;<br />
</source><br />
<br />
are doubtful and should be avoided if you have not allocated the foreignBmp directly yourself !<br />
<br />
That also means that different bitmap objects can refer to the same Bitplanes in memory.<br />
<br />
By using the bitmap structure graphics.library knows about the basic structure of the Bitplanes, how many there are and their position. However for drawing operations it needs some more information: In addition to the bitmap, graphics.library has to memorize its state somewhere such as which pen is set, which draw-mode is active, which font is used, and much more. This is done with the rastport structure , which is defined agraphics.pas. Such a RastPort is needed for most of the drawing routines of graphics.library. And, of course, several Rasports with different settings can point to the same Bitmap as drawing-target.<br />
<br />
Very thoughtful, our window already provides an initialized RastPort that we can use. But beware: this RastPort covers the entire window, including frames and system gadgets. So when you color this rastport completely using SetRast you'll end up with a single solid area on your workbench that has the exact size as the window.<br />
<br />
This is how a simplified representation of the connection between RastPort, bitmap and Bitplanes looks like:<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
The smart ones amongst us that draw the bitmap using the RastPort of the window are likely to experience another surprise: this bitmap actually maps to the content of the complete screen and as long as you use the RastPort of the window, layers.library ensures that drawing operations do not occur on areas outside the window or other hidden/covered areas. If you do this in such a way that you encapsulate the window rastport-bitmap in your own rastport and color it by using SetRast() then you'll end up with a complete single-colored screen and for sure will upset some users ;-)<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=857Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-17T21:48:25Z<p>Molly: /* Free Pascal installation on AmigaOS */ Add information on download archive and installation</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found:<br />
* [http://blog.alb42.de/fpc-amigaaros-m68k/ here] for Amiga OS3/AROS-m68k<br />
* [http://blog.alb42.de/fpc-amigaos-4/ here] for Amiga OS4<br />
* [http://blog.alb42.de/fpc-aros/ here] for AROS (select the correct target CPU)<br />
* [http://blog.alb42.de/fpc-morphos/ here] for MorphOS<br />
<br />
Make sure you download the archive that has "fpc 3.1.1" + "LCL" in its name, except for AROS that should have te word "trunk" in its name. Note that this archive is around 250MB in size when extracted.<br />
<br />
<br />
Then take the following steps:<br />
* Extract the archive where the archive's root-folder named pp can be extracted.<br />
* create an assign Freepascal: to this folder, preferably in your Startup Sequence or User Startup.<br />
* add a path to the drawer where fpc executable is located, e.g: "path add Freepascal:bin/m68k-amiga". Replace m68k-amiga with ppc-amiga for OS4, with ppc-morphos for MorphOS and do something similar for AROS depending on the architecture on which you run the compiler. Do this preferably in your Startup Sequence or User Startup.<br />
* reboot to make sure the assign and paths are active.<br />
<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=856Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-17T19:59:55Z<p>Molly: /* Here we go */ typo</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found (red: link missing).<br />
<br />
Extract the archive. (red: assign and path ?), then reboot.<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=855Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-17T17:45:44Z<p>Molly: /* Here we go */ typo</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found (red: link missing).<br />
<br />
Extract the archive. (red: assign and path ?), then reboot.<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*)<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=854Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-17T16:20:39Z<p>Molly: /* Heading text */ Add content for chapter: Here we go</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found (red: link missing).<br />
<br />
Extract the archive. (red: assign and path ?), then reboot.<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Here we go ==<br />
<br />
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.<br />
<br />
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.<br />
<br />
First we define two global variables that represent the base address for the libraries:<br />
<br />
<source lang="pascal"><br />
//* our system libraries addresses */<br />
var<br />
GfxBase : PGfxBase absolute AGraphics.GfxBase;<br />
IntuitionBase : PIntuitionBase absolute Intuition.IntuitionBase;<br />
</source><br />
<br />
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.<br />
<br />
(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).<br />
<br />
Because the libraries are also opened and closed automatically for us when the corresponding unit is included we do not initialize these variables.<br />
<br />
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:<br />
<br />
<source lang=pascal><br />
var<br />
result : Integer;<br />
begin<br />
//* as long we did not execute RunEngine() we report a failure */<br />
result := RETURN_FAIL;<br />
<br />
//* we need at least 1.2 graphic.library's drawing functions */<br />
if Assigned(GfxBase) and (GfxBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* we need at least 1.2 intuition.library for our window */<br />
if Assigned(IntuitionBase) and (IntuitionBase^.LibNode.lib_Version >= 33) then<br />
begin<br />
//* All libraries needed are available, so let's run... */<br />
result := RETURN_OK;<br />
//* Closing Intuition library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
//* Closing Graphics library and setting its baseaddress to nil */<br />
//* is not necessary as Pascal does that automatically for us */<br />
end;<br />
<br />
//* Pascal uses System variable ExitCode to report back a value to caller<br />
ExitCode := result;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
type<br />
PRenderEngineData = ^TRenderEngineData;<br />
TRenderEngineData = <br />
record<br />
window : PWindow;<br />
run : boolean;<br />
end;<br />
<br />
function RunEngine: integer;<br />
var<br />
rd : PRenderEngineData;<br />
newWindow : TNewWindow;<br />
begin<br />
//* as long we did not enter our main loop we report an error */<br />
result := RETURN_ERROR;<br />
<br />
(* <br />
allocate the memory for our runtime data and initialize it<br />
with zeros <br />
*}<br />
rd := PRenderEngineData(ExecAllocMem(sizeof(TRenderEngineData), MEMF_ANY or MEMF_CLEAR));<br />
if assigned(rd) then<br />
begin<br />
//* now let's open our window */<br />
with newWindow do<br />
begin<br />
LeftEdge := 0; TopEdge := 14;<br />
Width := 320; Height := 160;<br />
DetailPen := UBYTE(not(0)); BlockPen := UBYTE(not(0));<br />
IDCMPFlags := IDCMP_CLOSEWINDOW or IDCMP_NEWSIZE or IDCMP_REFRESHWINDOW;<br />
Flags := WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_SIMPLE_REFRESH or WFLG_SIZEBBOTTOM or WFLG_SIZEGADGET;<br />
FirstGadget := nil; CheckMark := nil;<br />
Title := 'Gfx Workshop';<br />
Screen := nil;<br />
BitMap := nil;<br />
MinWidth := 96; MinHeight := 48;<br />
MaxWidth := UWORD(not(0)); MaxHeight := UWORD(not(0));<br />
WType := WBENCHSCREEN_f;<br />
end;<br />
<br />
rd^.window := OpenWindow(@newWindow);<br />
if Assigned(rd^.window) then<br />
begin<br />
//* the main loop will run as long this is TRUE */<br />
rd^.run := TRUE;<br />
<br />
result := MainLoop(rd);<br />
<br />
//* cleanup: close the window */<br />
CloseWindow(rd^.window);<br />
rd^.window := nil;<br />
end;<br />
<br />
//* free our runtime data */<br />
ExecFreeMem(rd, sizeof(TRenderEngineData));<br />
rd := nil;<br />
end;<br />
end;<br />
</source><br />
<br />
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 ?)<br />
<br />
If everything worked out as intended then we jump into our MainLoop():<br />
<br />
<source lang=pascal><br />
function MainLoop(rd: PRenderEngineData): integer;<br />
var<br />
winport : PMsgPort;<br />
winsig : ULONG;<br />
<br />
msg : PMessage;<br />
begin<br />
//* remember the window port in a local variable for more easy use */<br />
winport := rd^.window^.UserPort;<br />
<br />
//* create our waitmask for the window port */<br />
winSig := 1 shl winport^.mp_SigBit;<br />
<br />
//* our main loop */<br />
while (rd^.run) do<br />
begin<br />
//* let's sleep until a message from our window arrives */<br />
Wait(winSig);<br />
<br />
{* <br />
our window signaled us, so let's harvest all its messages<br />
in a loop... <br />
*}<br />
while true do<br />
begin<br />
msg := GetMsg(winport);<br />
if not assigned(msg) then break;<br />
<br />
//* ...and dispatch and reply each of them */<br />
DispatchWindowMessage(rd, PIntuiMessage(msg));<br />
ReplyMsg(msg);<br />
end;<br />
end;<br />
result := RETURN_OK;<br />
end;<br />
</source><br />
<br />
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:<br />
<br />
<source lang=pascal><br />
procedure DispatchWindowMessage(rd: PRenderEngineData; msg: PIntuiMessage);<br />
begin<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
begin<br />
{* <br />
User pressed the window's close gadget: exit the main loop as<br />
soon as possible<br />
*}<br />
rd^.run := FALSE;<br />
end;<br />
IDCMP_REFRESHWINDOW:<br />
begin<br />
BeginRefresh(rd^.window);<br />
EndRefresh(rd^.window, TRUE);<br />
end;<br />
end;<br />
end;<br />
</source><br />
<br />
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.<br />
<br />
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.<br />
<br />
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? ;-)<br />
<br />
fpc engine1.pas<br />
<br />
[ you should be looking at a picture here ]<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=853Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-17T15:14:16Z<p>Molly: /* Heading text */ Add content for chapter Pascal and AmigaOS</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found (red: link missing).<br />
<br />
Extract the archive. (red: assign and path ?), then reboot.<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Pascal and AmigaOS ==<br />
<br />
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:<br />
<br />
* 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.<br />
* 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.<br />
* 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)<br />
<br />
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.<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=852Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-16T14:20:19Z<p>Molly: /* HSPascal */ typo</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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.<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found (red: link missing).<br />
<br />
Extract the archive. (red: assign and path ?), then reboot.<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=851Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-16T14:19:53Z<p>Molly: /* High Speed Pascal */ typo</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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 High Speed 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.<br />
<br />
<br />
=== HighSpeed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found (red: link missing).<br />
<br />
Extract the archive. (red: assign and path ?), then reboot.<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=850Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-16T10:32:56Z<p>Molly: Add some headers</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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 High Speed 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.<br />
<br />
<br />
=== High Speed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found (red: link missing).<br />
<br />
Extract the archive. (red: assign and path ?), then reboot.<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=849Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-16T10:30:38Z<p>Molly: /* Heading text */ initial content for chapter: Free Pascal installation on AmigaOS</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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 High Speed 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.<br />
<br />
<br />
=== High Speed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Free Pascal installation on AmigaOS ==<br />
<br />
At least one archive is required in order to be able to use the compiler for Amiga projects.<br />
<br />
This archive can be found (red: link missing).<br />
<br />
Extract the archive. (red: assign and path ?), then reboot.<br />
<br />
Now we make a quick test to verify your setup:<br />
<br />
Create a file named test.pas with the following content:<br />
<br />
<source lang="pascal"><br />
program test;<br />
<br />
uses<br />
AmigaDOS;<br />
<br />
var<br />
hello : PChar;<br />
<br />
begin<br />
hello := 'Hello Amiga!' + sLinebreak;<br />
DOSWrite(DOSOutput, hello, Length(hello));<br />
WriteLn('Hello Pascal!');<br />
ExitCode := RETURN_OK;<br />
end.<br />
</source><br />
<br />
You can compile that with FPC using the following statement:<br />
<br />
<source><br />
fpc test.pas<br />
</source><br />
<br />
If this is compiled without error, then start your test. This should simply output two lines:<br />
<br />
<source><br />
Hello Amiga!<br />
Hello Pascal!<br />
</source><br />
<br />
If this test was successful then you can continue the workshop with your compiler setup.<br />
<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=848Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-16T10:21:37Z<p>Molly: /* Heading text */ initial content for chapter: A quick view on Pascal compilers</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== A quick view on Pascal compilers ==<br />
<br />
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:<br />
<br />
<br />
=== UCSD Pascal ===<br />
<br />
More research required.<br />
<br />
<br />
=== Amiga Pascal ===<br />
<br />
Also known as MCC Pascal. Distributed by Commodore, developed by MetaComCo (a division of Tenchstar, Ltd.).<br />
<br />
<br />
=== AmigaPascal ===<br />
<br />
A mini Pascal compiler developed by Daniel Amor and released as freeware (binary only, closed source). Appeared on Fred Fish in 1993.<br />
<br />
<br />
=== HSPascal ===<br />
<br />
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 High Speed 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.<br />
<br />
<br />
=== High Speed Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== Kick Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== MAXON Pascal ===<br />
<br />
See HSPascal. Closed source commercial product. Development seized.<br />
<br />
<br />
=== PCQ Pascal ===<br />
<br />
Originally published as Public Domain Pascal compiler. Developed by Nils, Patrick and ????. Later released as freeware and as Open Source.<br />
<br />
<br />
=== Free Pascal ===<br />
<br />
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.<br />
<br />
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.<br />
<br />
== Heading text ==<br />
<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=847Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-16T05:17:25Z<p>Molly: /* Heading text */ foreword added</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Foreword ==<br />
<br />
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 :-)<br />
<br />
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.<br />
<br />
The engine itself uses [https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics double-buffering] to display the graphics: drawing operations are performed on a non-visible [https://en.wikipedia.org/wiki/Raster_graphics 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.<br />
<br />
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.<br />
<br />
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 [https://en.wikipedia.org/wiki/Retargetable_graphics RTG-systems]: Our renderer is therefor limited to 8-bit graphics.<br />
Nor is there any support for special features of the Amiga chipset: So there will be no hardware scrolling, sprites or copperlists.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<br />
And now for some fun!<br />
<br />
== Heading text ==<br />
<br />
== Heading text ==<br />
<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=846Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-14T19:54:19Z<p>Molly: Add extra dummy headers to enable showing content table</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Heading text ==<br />
<br />
== Heading text ==<br />
<br />
== Heading text ==<br />
<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=845Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-14T19:52:29Z<p>Molly: Added preface content</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
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. <br />
<br />
This should hopefully clear things up with regards of the use of the words "I", "we" and "me".<br />
<br />
<br />
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. <br />
<br />
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.<br />
<br />
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.<br />
<br />
<br />
'''notes with regards to Free Pascal'''<br />
<br />
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.<br />
<br />
Another wicked alternative for compiling single-file projects is using [http://home.alb42.de/fpamiga/ the online compiler]. There is even [http://home.alb42.de/fpamiga/indexold.html a special version of the online compiler] for old browsers that don't quite handle javascript<br />
<br />
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.<br />
<br />
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.<br />
<br />
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:<br />
<br />
<source lang="pascal"><br />
// c main like entry-point.<br />
function main(argc: Integer; argv: PPChar): integer;<br />
begin<br />
if EverthingElseWentOk() <br />
then result := RETURN_OK <br />
else result := RETURN_FAIL;<br />
end;<br />
<br />
// This is the Pascal equivalent of main program entry point<br />
begin<br />
ExitCode := main(ArgC, ArgV);<br />
end.<br />
</source><br />
<br />
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 ?)<br />
<br />
<br />
== Heading text ==<br />
<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=844Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-14T18:02:12Z<p>Molly: Add copyright notice</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
<div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Respect the copyright'''<br />
</center><br />
This workshop is based on the workshop titled "Retrocoding: Amiga, C, graphics.library und timer.device" which is written and copyrighted by Kai Scherrer. <br />
<br />
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.<br />
<br />
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.<br />
</div><br />
<br />
blah blah<br />
<br />
== Heading text ==<br />
<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=843Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-13T18:44:21Z<p>Molly: Add to category Workshops</p>
<hr />
<div>[[Category:Workshops]]<br />
<br />
blah blah<br />
<br />
== Heading text ==<br />
<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Workshop:Amiga,_Pascal,_graphics.library_and_timer.device&diff=842Workshop:Amiga, Pascal, graphics.library and timer.device2017-09-13T18:43:05Z<p>Molly: Dummy content</p>
<hr />
<div>blah blah<br />
<br />
== Heading text ==<br />
<br />
== Heading text ==</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Category:Workshops&diff=841Category:Workshops2017-09-13T18:31:54Z<p>Molly: Add new Category Workshops</p>
<hr />
<div></div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Intuition_window_goes_OOP(ish)&diff=840Intuition window goes OOP(ish)2017-09-13T18:29:27Z<p>Molly: </p>
<hr />
<div>[[Category:Examples]]<br />
<br />
A long while ago, someone on the aros-exec forums suggested/asked to use some (more) OOP to create f.i. native Intuition Windows.<br />
<br />
Although that is fairly easy to accomplish, there never was an actual example showing how to do such a thing for those wanting to have a look. So, here we go :-)<br />
<br />
Do note however, that the code showed herein is not complete, nor does it show good programing practice (even far from it). It is merely shown as a possible solution to the problem. Much more abstraction is required in order to be able to make practical use of the implementation as showed.<br />
<br />
== Step 1: A starting point ==<br />
<br />
In order to be able to show the reader how things are accomplished, we have to start with at least some bit of code. So, let's start out with a simple intuition window example. This example was taken from (and is copyrighted by) Thomas Rapp.<br />
<br />
<source lang="pascal"><br />
program Step1_SimpleWindow;<br />
<br />
{$MODE OBJFPC}{$H+}<br />
<br />
Uses<br />
Exec, AGraphics, Intuition, InputEvent, Utility;<br />
<br />
<br />
Function AsTag(tag: LongWord): LongInt; inline;<br />
begin<br />
Result := LongInt(tag);<br />
end;<br />
<br />
<br />
//*-------------------------------------------------------------------------*/<br />
//* */<br />
//*-------------------------------------------------------------------------*/<br />
<br />
procedure print_text(rp: PRastPort; x: LongInt; y: LongInt; txt: PChar);<br />
begin<br />
GfxMove(rp, x, y);<br />
SetABPenDrMd(rp, 1, 0, JAM2);<br />
GfxText(rp, txt, strlen(txt));<br />
ClearEOL(rp);<br />
end;<br />
<br />
<br />
//*-------------------------------------------------------------------------*/<br />
//* Main routine */<br />
//*-------------------------------------------------------------------------*/<br />
<br />
function main: integer;<br />
var<br />
win : PWindow;<br />
cont : Boolean;<br />
msg : PIntuiMessage;<br />
buffer : String[80];<br />
begin<br />
win := OpenWindowTags( nil,<br />
[<br />
AsTag(WA_Left) , 100,<br />
AsTag(WA_Top) , 100,<br />
AsTag(WA_Width) , 250,<br />
AsTag(WA_Height) , 150,<br />
AsTag(WA_Flags) , AsTag(WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_ACTIVATE or WFLG_GIMMEZEROZERO or WFLG_NOCAREREFRESH or WFLG_RMBTRAP or WFLG_REPORTMOUSE),<br />
AsTag(WA_IDCMP) , AsTag(IDCMP_CLOSEWINDOW or IDCMP_MOUSEMOVE or IDCMP_MOUSEBUTTONS),<br />
TAG_END<br />
]);<br />
<br />
if Assigned(win) then<br />
begin<br />
cont := TRUE;<br />
<br />
while (cont) do<br />
begin<br />
WaitPort(win^.UserPort);<br />
while true do<br />
begin<br />
msg := PIntuiMessage(GetMsg(win^.UserPort));<br />
if not Assigned(msg) then break;<br />
<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
cont := FALSE;<br />
IDCMP_MOUSEMOVE:<br />
begin<br />
WriteStr(buffer, 'Mouseposition: x=', msg^.MouseX, ' y=', msg^.MouseY, #0);<br />
print_text(win^.RPort, 10, 30, @buffer[1]);<br />
end;<br />
IDCMP_MOUSEBUTTONS:<br />
case (msg^.Code) of<br />
IECODE_LBUTTON : print_text(win^.RPort, 10, 60, 'Left mousebutton pressed');<br />
IECODE_LBUTTON or IECODE_UP_PREFIX : print_text(win^.RPort, 10, 60, 'Left mousebutton released');<br />
IECODE_RBUTTON : print_text(win^.RPort, 10, 90, 'Right mousebutton pressed');<br />
IECODE_RBUTTON or IECODE_UP_PREFIX : print_text(win^.RPort, 10, 90, 'Right mousebutton released');<br />
end;<br />
end; // case<br />
ReplyMsg(pMessage(msg));<br />
end;<br />
end; // while<br />
CloseWindow(win);<br />
end;<br />
<br />
result := (0);<br />
end;<br />
<br />
begin<br />
ExitCode := Main;<br />
end.<br />
</source><br />
<br />
The code itself doesn't do anything particularly difficult to understand. It opens a Intuition Window and processes the IDCMP messages and based on those messages give some feedback to the user.<br />
<br />
The AsTag function is there for our convenience, and which is missing from FPC 3.0.x compiler (it is present for FPC 3.1.1 trunk compiler)<br />
<br />
== Step 2: Classify the window ==<br />
<br />
So, now the question becomes: how do we turn the previous example into a Free Pascal Class ?<br />
<br />
Therefor we have to take a look at some of the used properties.<br />
<br />
We can see that opening an Intuition window returns us a pointer to the created window (handle), and that this pointer is also used to close the window again. So for these basics we at least requires a handle variable for our window and a open() and close() method.<br />
<br />
When the window is created with OpenWindowTags() we can see that some tags are provided such as the placement of the window (left and top) and the dimensions of the window (width and height). Also a title is provided as a tag.<br />
<br />
We turn all these into private variables and add properties for them in our class.<br />
<br />
We put the code for our newly created class into a separate unit, so that things can be re-used with more convenience.<br />
<br />
<source lang="pascal"><br />
unit Step2_IntWinClass;<br />
<br />
{$MODE OBJFPC}{$H+}<br />
<br />
interface<br />
<br />
uses<br />
Intuition;<br />
<br />
type<br />
TIntuitionWindowClass = class<br />
private<br />
FHandle : PWindow;<br />
FLeft : LongInt;<br />
FTop : LongInt;<br />
FWidth : LongInt;<br />
FHeight : LongInt;<br />
FTitle : AnsiString;<br />
protected<br />
public<br />
Constructor Create; <br />
Destructor Destroy; override;<br />
public<br />
procedure Open;<br />
procedure Close;<br />
public<br />
property Left : LongInt read FLeft write FLeft;<br />
property Top : LongInt read FTop write FTop;<br />
property Width : LongInt read FWidth write FWidth;<br />
property Height: LongInt read FHeight write FHeight;<br />
property Title : String read FTitle write FTitle;<br />
property Handle : PWindow read FHandle;<br />
end;<br />
<br />
<br />
implementation<br />
<br />
uses<br />
SysUtils;<br />
<br />
<br />
Function AsTag(tag: LongWord): LongInt; inline;<br />
begin<br />
Result := LongInt(tag);<br />
end;<br />
<br />
<br />
procedure error(Const msg : string); <br />
begin <br />
raise exception.create(Msg) at <br />
get_caller_addr(get_frame), <br />
get_caller_frame(get_frame); <br />
end; <br />
<br />
<br />
Constructor TIntuitionWindowClass.Create;<br />
begin<br />
Inherited;<br />
<br />
FHandle := nil;<br />
FLeft := 10;<br />
FTop := 10;<br />
FHeight := 30;<br />
FWidth := 30;<br />
FTitle := '';<br />
end;<br />
<br />
<br />
Destructor TIntuitionWindowClass.Destroy;<br />
begin<br />
inherited;<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.Open;<br />
begin<br />
FHandle := OpenWindowTags( nil,<br />
[<br />
AsTag(WA_Left) , FLeft,<br />
AsTag(WA_Top) , FTop,<br />
AsTag(WA_Width) , FWidth,<br />
AsTag(WA_Height) , FHeight,<br />
AsTag(WA_Title) , PChar(FTitle),<br />
// Non use settable flags (for now)<br />
AsTag(WA_Flags) , AsTag(WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_ACTIVATE or WFLG_GIMMEZEROZERO or WFLG_NOCAREREFRESH or WFLG_RMBTRAP or WFLG_REPORTMOUSE),<br />
AsTag(WA_IDCMP) , AsTag(IDCMP_CLOSEWINDOW or IDCMP_MOUSEMOVE or IDCMP_MOUSEBUTTONS),<br />
TAG_END<br />
]);<br />
if not Assigned(FHandle) then Error('Unable to Open Window');<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.Close;<br />
begin<br />
if Assigned(FHandle) <br />
then CloseWindow(FHandle)<br />
else Error('Unable to Close Window because the handle is invalid');<br />
end;<br />
<br />
end.<br />
</source><br />
<br />
As you can see we've also added a constructor (Create) which initializes some default values for the FHandle and private window dimension variables as well as clear the private title variable.<br />
<br />
We've added a destructor (Destroy) that doesn't do anything useful atm, and is there just in case we need it (we can always remove it later on)<br />
<br />
Other then that we've added two methods, one to Open the Intuition Window and one to Close the Intuition Window and added code that actually perform these actions.<br />
<br />
Now, we're going to make use of this new class and create a new program:<br />
<br />
<source lang="pascal"><br />
program Step2_ClassWindow;<br />
<br />
{$MODE OBJFPC}{$H+}<br />
<br />
uses<br />
Step2_IntWinClass, Exec, AGraphics, Intuition, InputEvent;<br />
<br />
<br />
//*-------------------------------------------------------------------------*/<br />
//* */<br />
//*-------------------------------------------------------------------------*/<br />
<br />
procedure print_text(rp: PRastPort; x: LongInt; y: LongInt; txt: PChar);<br />
begin<br />
GfxMove(rp, x, y);<br />
SetABPenDrMd(rp, 1, 0, JAM2);<br />
GfxText(rp, txt, strlen(txt));<br />
ClearEOL(rp);<br />
end;<br />
<br />
<br />
//*-------------------------------------------------------------------------*/<br />
//* Main routine */<br />
//*-------------------------------------------------------------------------*/<br />
<br />
function main: integer;<br />
var<br />
Window1 : TIntuitionWindowClass;<br />
cont : Boolean;<br />
msg : PIntuiMessage;<br />
buffer : String[80];<br />
begin<br />
Window1 := TIntuitionWindowClass.Create;<br />
Window1.Left := 10;<br />
Window1.Top := 20;<br />
Window1.Height := 200;<br />
Window1.Width := 320;<br />
Window1.Title := 'This is window 1';<br />
Window1.Open;<br />
<br />
WaitPort(Window1.Handle^.UserPort);<br />
<br />
cont := TRUE;<br />
<br />
while (cont) do<br />
begin<br />
while true do<br />
begin<br />
msg := PIntuiMessage(GetMsg(Window1.Handle^.UserPort));<br />
if not Assigned(msg) then break;<br />
<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
cont := FALSE;<br />
IDCMP_MOUSEMOVE:<br />
begin<br />
WriteStr(buffer, 'Mouseposition: x=', msg^.MouseX, ' y=', msg^.MouseY, #0);<br />
print_text(Window1.Handle^.RPort, 10, 30, @buffer[1]);<br />
end;<br />
IDCMP_MOUSEBUTTONS:<br />
case (msg^.Code) of<br />
IECODE_LBUTTON : print_text(Window1.Handle^.RPort, 10, 60, 'Left mousebutton pressed');<br />
IECODE_LBUTTON or IECODE_UP_PREFIX : print_text(Window1.Handle^.RPort, 10, 60, 'Left mousebutton released');<br />
IECODE_RBUTTON : print_text(Window1.Handle^.RPort, 10, 90, 'Right mousebutton pressed');<br />
IECODE_RBUTTON or IECODE_UP_PREFIX : print_text(Window1.Handle^.RPort, 10, 90, 'Right mousebutton released');<br />
end;<br />
end; // case<br />
ReplyMsg(pMessage(msg));<br />
end;<br />
end;<br />
Window1.Close;<br />
Window1.Free;<br />
<br />
result := (0);<br />
end;<br />
<br />
<br />
begin<br />
ExitCode := Main;<br />
end.<br />
</source><br />
<br />
Also here, no real rocket science. We have to take care of Creating and Destroying our class and we've replaced the 'normal' code that opened and closed the intuition Window by calling the Methods that we've just implemented. The message handling itself is still part of our main program.<br />
<br />
== Step 3: Moving around message handling ==<br />
<br />
We're going to add to our class again, by moving the message handling from our main program to our class.<br />
<br />
<source lang=pascal><br />
unit Step3_IntWinClass;<br />
<br />
{$MODE OBJFPC}{$H+}<br />
<br />
interface<br />
<br />
uses<br />
Intuition;<br />
<br />
<br />
type<br />
TIntuitionWindowClass = class<br />
private<br />
FHandle : PWindow;<br />
FLeft : LongInt;<br />
FTop : LongInt;<br />
FWidth : LongInt;<br />
FHeight : LongInt;<br />
FTitle : AnsiString;<br />
protected<br />
public<br />
Constructor Create; <br />
Destructor Destroy; override;<br />
public<br />
procedure Open;<br />
procedure Close;<br />
procedure HandleMessages;<br />
public<br />
property Left : LongInt read FLeft write FLeft;<br />
property Top : LongInt read FTop write FTop;<br />
property Width : LongInt read FWidth write FWidth;<br />
property Height: LongInt read FHeight write FHeight;<br />
property Title : String read FTitle write FTitle;<br />
property Handle : PWindow read FHandle;<br />
end;<br />
<br />
<br />
implementation<br />
<br />
uses<br />
SysUtils, Exec, AGraphics, InputEvent;<br />
<br />
<br />
Function AsTag(tag: LongWord): LongInt; inline;<br />
begin<br />
Result := LongInt(tag);<br />
end;<br />
<br />
<br />
procedure error(Const msg : string); <br />
begin <br />
raise exception.create(Msg) at <br />
get_caller_addr(get_frame), <br />
get_caller_frame(get_frame); <br />
end; <br />
<br />
<br />
Constructor TIntuitionWindowClass.Create;<br />
begin<br />
Inherited;<br />
<br />
FHandle := nil;<br />
FLeft := 10;<br />
FTop := 10;<br />
FHeight := 30;<br />
FWidth := 30;<br />
FTitle := '';<br />
end;<br />
<br />
<br />
Destructor TIntuitionWindowClass.Destroy;<br />
begin<br />
inherited;<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.Open;<br />
var<br />
aTitle : PChar;<br />
begin<br />
if FTitle <> '' then aTitle := PChar(FTitle) else aTitle := nil;<br />
<br />
FHandle := OpenWindowTags( nil,<br />
[<br />
AsTag(WA_Left) , FLeft,<br />
AsTag(WA_Top) , FTop,<br />
AsTag(WA_Width) , FWidth,<br />
AsTag(WA_Height) , FHeight,<br />
AsTag(WA_Title) , aTitle,<br />
// Non use settable flags (for now)<br />
AsTag(WA_Flags) , AsTag(WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_ACTIVATE or WFLG_GIMMEZEROZERO or WFLG_NOCAREREFRESH or WFLG_RMBTRAP or WFLG_REPORTMOUSE),<br />
AsTag(WA_IDCMP) , AsTag(IDCMP_CLOSEWINDOW or IDCMP_MOUSEMOVE or IDCMP_MOUSEBUTTONS),<br />
TAG_END<br />
]);<br />
if not Assigned(FHandle) then Error('Unable to Open Window');<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.Close;<br />
begin<br />
if Assigned(FHandle) <br />
then CloseWindow(FHandle)<br />
else Error('Unable to Close Window because the handle is invalid');<br />
end;<br />
<br />
<br />
procedure print_text(rp: PRastPort; x: LongInt; y: LongInt; txt: PChar);<br />
begin<br />
GfxMove(rp, x, y);<br />
SetABPenDrMd(rp, 1, 0, JAM2);<br />
GfxText(rp, txt, strlen(txt));<br />
ClearEOL(rp);<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.HandleMessages;<br />
var<br />
cont : Boolean;<br />
msg : PIntuiMessage;<br />
buffer : String[80];<br />
begin<br />
cont := TRUE;<br />
<br />
while (cont) do<br />
begin<br />
WaitPort(FHandle^.UserPort);<br />
<br />
while true do<br />
begin<br />
msg := PIntuiMessage(GetMsg(FHandle^.UserPort));<br />
if not Assigned(msg) then break;<br />
<br />
case (msg^.IClass) of<br />
IDCMP_CLOSEWINDOW:<br />
cont := FALSE;<br />
IDCMP_MOUSEMOVE:<br />
begin<br />
WriteStr(buffer, 'Mouseposition: x=', msg^.MouseX, ' y=', msg^.MouseY, #0);<br />
print_text(FHandle^.RPort, 10, 30, @buffer[1]);<br />
end;<br />
IDCMP_MOUSEBUTTONS:<br />
case (msg^.Code) of<br />
IECODE_LBUTTON : print_text(FHandle^.RPort, 10, 60, 'Left mousebutton pressed');<br />
IECODE_LBUTTON or IECODE_UP_PREFIX : print_text(FHandle^.RPort, 10, 60, 'Left mousebutton released');<br />
IECODE_RBUTTON : print_text(FHandle^.RPort, 10, 90, 'Right mousebutton pressed');<br />
IECODE_RBUTTON or IECODE_UP_PREFIX : print_text(FHandle^.RPort, 10, 90, 'Right mousebutton released');<br />
end;<br />
end; // case<br />
ReplyMsg(pMessage(msg));<br />
end;<br />
end;<br />
end;<br />
<br />
end.<br />
</source><br />
<br />
The message handling is performed by our newly added method HandleMessages and as you can see the code is literally the code that was used in the main program before.<br />
<br />
Now we have to make our main program make use of this new method.<br />
<br />
<source lang="pascal"><br />
program Step3_ClassWindow;<br />
<br />
{$MODE OBJFPC}{$H+}<br />
<br />
uses<br />
Step3_IntWinClass, Exec, AGraphics, Intuition, InputEvent;<br />
<br />
<br />
//*-------------------------------------------------------------------------*/<br />
//* Main routine */<br />
//*-------------------------------------------------------------------------*/<br />
<br />
function main: integer;<br />
var<br />
Window1 : TIntuitionWindowClass;<br />
begin<br />
Window1 := TIntuitionWindowClass.Create;<br />
Window1.Left := 10;<br />
Window1.Top := 20;<br />
Window1.Height := 200;<br />
Window1.Width := 320;<br />
Window1.Title := 'This is window 1';<br />
Window1.Open;<br />
<br />
Window1.HandleMessages;<br />
<br />
Window1.Close;<br />
Window1.Free;<br />
<br />
result := (0);<br />
end;<br />
<br />
<br />
begin<br />
ExitCode := Main;<br />
end.<br />
</source><br />
<br />
Easy enough, and things still work. A bit awkward perhaps, but it works (for this one window).<br />
<br />
== Step 4: Dispatching messages ==<br />
<br />
Here is where things become a bit more interesting, as we're going to add a (IDCMP) message dispatcher to our class.<br />
<br />
<source lang="pascal"><br />
unit Step4_IntWinClass;<br />
<br />
{$MODE OBJFPC}{$H+}<br />
<br />
interface<br />
<br />
uses<br />
Intuition;<br />
<br />
<br />
type<br />
TIntuitionMessageRec = record<br />
MsgCode : DWord;<br />
IMsg : PIntuiMessage;<br />
end;<br />
<br />
type<br />
TIntuitionWindowClass = class<br />
private<br />
FHandle : PWindow;<br />
FLeft : LongInt;<br />
FTop : LongInt;<br />
FWidth : LongInt;<br />
FHeight : LongInt;<br />
FTitle : AnsiString;<br />
FStopped : boolean;<br />
protected<br />
procedure MsgCloseWindow(var msg: TIntuitionMessageRec); Message IDCMP_CLOSEWINDOW;<br />
procedure MsgMouseMove(var msg: TIntuitionMessageRec); Message IDCMP_MOUSEMOVE;<br />
procedure MsgMouseButtons(var msg: TIntuitionMessageRec); Message IDCMP_MOUSEBUTTONS;<br />
public<br />
constructor Create; <br />
destructor Destroy; override;<br />
public<br />
procedure Open;<br />
procedure Close;<br />
procedure HandleMessages;<br />
procedure DefaultHandler(var message); override;<br />
public<br />
property Left : LongInt read FLeft write FLeft;<br />
property Top : LongInt read FTop write FTop;<br />
property Width : LongInt read FWidth write FWidth;<br />
property Height : LongInt read FHeight write FHeight;<br />
property Title : String read FTitle write FTitle;<br />
property Handle : PWindow read FHandle;<br />
end;<br />
<br />
<br />
implementation<br />
<br />
uses<br />
SysUtils, Exec, AGraphics, InputEvent;<br />
<br />
<br />
function AsTag(tag: LongWord): LongInt; inline;<br />
begin<br />
Result := LongInt(tag);<br />
end;<br />
<br />
<br />
procedure error(Const msg : string); <br />
begin <br />
raise exception.create(Msg) at <br />
get_caller_addr(get_frame), <br />
get_caller_frame(get_frame); <br />
end; <br />
<br />
<br />
Constructor TIntuitionWindowClass.Create;<br />
begin<br />
Inherited;<br />
<br />
FHandle := nil;<br />
FLeft := 10;<br />
FTop := 10;<br />
FHeight := 30;<br />
FWidth := 30;<br />
FTitle := '';<br />
FStopped := false;<br />
end;<br />
<br />
<br />
Destructor TIntuitionWindowClass.Destroy;<br />
begin<br />
inherited;<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.Open;<br />
var<br />
aTitle : PChar;<br />
begin<br />
if FTitle <> '' then aTitle := PChar(FTitle) else aTitle := nil;<br />
<br />
FHandle := OpenWindowTags( nil,<br />
[<br />
AsTag(WA_Left) , FLeft,<br />
AsTag(WA_Top) , FTop,<br />
AsTag(WA_Width) , FWidth,<br />
AsTag(WA_Height) , FHeight,<br />
AsTag(WA_Title) , aTitle,<br />
// Non use settable flags (for now)<br />
AsTag(WA_Flags) , AsTag(WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_ACTIVATE or WFLG_GIMMEZEROZERO or WFLG_NOCAREREFRESH or WFLG_RMBTRAP or WFLG_REPORTMOUSE),<br />
AsTag(WA_IDCMP) , AsTag(IDCMP_CLOSEWINDOW or IDCMP_MOUSEMOVE or IDCMP_MOUSEBUTTONS),<br />
TAG_END<br />
]);<br />
if not Assigned(FHandle) then Error('Unable to Open Window');<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.Close;<br />
begin<br />
if Assigned(FHandle) <br />
then Intuition.CloseWindow(FHandle)<br />
else Error('Unable to Close Window because the handle is invalid');<br />
end;<br />
<br />
<br />
procedure print_text(rp: PRastPort; x: LongInt; y: LongInt; txt: PChar);<br />
begin<br />
GfxMove(rp, x, y);<br />
SetABPenDrMd(rp, 1, 0, JAM2);<br />
GfxText(rp, txt, strlen(txt));<br />
ClearEOL(rp);<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.HandleMessages;<br />
var<br />
msg : PIntuiMessage;<br />
msgrec : TIntuitionMessageRec;<br />
begin<br />
while not FStopped do<br />
begin<br />
WaitPort(FHandle^.UserPort);<br />
<br />
while true do<br />
begin<br />
msg := PIntuiMessage(GetMsg(FHandle^.UserPort));<br />
if not Assigned(msg) then break;<br />
<br />
// WriteLn('ReplyMsg');<br />
ReplyMsg(pMessage(msg));<br />
// WriteLn('Dispatch');<br />
MsgRec.MsgCode := msg^.IClass; <br />
MsgRec.IMsg := msg;<br />
Dispatch(msgrec);<br />
end;<br />
end;<br />
end;<br />
<br />
<br />
(*<br />
http://www.freepascal.org/docs-html/rtl/system/tobject.defaulthandler.html<br />
DefaultHandler is the default handler for messages. If a message has an <br />
unknown message ID (i.e. does not appear in the table with integer message <br />
handlers), then it will be passed to DefaultHandler by the Dispatch method.<br />
*)<br />
<br />
(*<br />
http://www.freepascal.org/docs-html/rtl/system/tobject.dispatch.html<br />
Dispatch looks in the message handler table for a handler that handles <br />
message. The message is identified by the first dword (cardinal) in the <br />
message structure. <br />
<br />
If no matching message handler is found, the message is passed to the <br />
DefaultHandler method, which can be overridden by descendent classes to add <br />
custom handling of messages. <br />
*)<br />
procedure TIntuitionWindowClass.DefaultHandler(var message);<br />
begin<br />
Writeln('invoked default handler');<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.MsgCloseWindow(var msg: TIntuitionMessageRec);<br />
begin<br />
WriteLn('IDCMP_CLOSEWINDOW message received');<br />
FStopped := true;<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.MsgMouseMove(var msg: TIntuitionMessageRec);<br />
var<br />
buffer : String[80];<br />
begin<br />
WriteLn('IDCMP_MOUSEMOVE message received');<br />
WriteStr(buffer, 'Mouseposition: x=', msg.IMsg^.MouseX, ' y=', msg.IMsg^.MouseY, #0);<br />
print_text(FHandle^.RPort, 10, 30, @buffer[1]);<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.MsgMouseButtons(var msg: TIntuitionMessageRec);<br />
begin<br />
WriteLn('IDCMP_MOUSEBUTTONS message received');<br />
case msg.IMsg^.Code of<br />
IECODE_LBUTTON : print_text(FHandle^.RPort, 10, 60, 'Left mousebutton pressed');<br />
IECODE_LBUTTON or IECODE_UP_PREFIX : print_text(FHandle^.RPort, 10, 60, 'Left mousebutton released');<br />
IECODE_RBUTTON : print_text(FHandle^.RPort, 10, 90, 'Right mousebutton pressed');<br />
IECODE_RBUTTON or IECODE_UP_PREFIX : print_text(FHandle^.RPort, 10, 90, 'Right mousebutton released');<br />
end;<br />
end;<br />
<br />
end.<br />
</source><br />
<br />
The changes perhaps look difficult but, it actually isn't.<br />
<br />
First we've added a new structure TIntuitionMessageRec, that is compatible with a Free Pascal Dispatch message, and at the same time is able to hold our intuition message information.<br />
<br />
In order to be able to keep track whether or not our window is closed we defined a new private variable named Stopped.<br />
<br />
Next thing we've added, are the 3 IDCMP message procedures. These get 'automatically' invoked by the dispatcher.<br />
<br />
We override the DefaultHandler that is standard part of TObject, so that we can give some feedback to the suer in case none of our message is intercepted correctly (and the default handler is invoked)<br />
<br />
Finally we adjust our HandleMessages method to call the TObject dispatcher.<br />
<br />
<br />
For our main program, and which comes to no surprise, nothing is changed. For the sake of completeness we post the code.<br />
<br />
<source lang="pascal"><br />
program Step4_ClassWindow;<br />
<br />
{$MODE OBJFPC}{$H+}<br />
<br />
uses<br />
Step4_IntWinClass, Exec, AGraphics, Intuition, InputEvent;<br />
<br />
<br />
//*-------------------------------------------------------------------------*/<br />
//* Main routine */<br />
//*-------------------------------------------------------------------------*/<br />
<br />
function main: integer;<br />
var<br />
Window1 : TIntuitionWindowClass;<br />
begin<br />
Window1 := TIntuitionWindowClass.Create;<br />
Window1.Left := 10;<br />
Window1.Top := 20;<br />
Window1.Height := 200;<br />
Window1.Width := 320;<br />
Window1.Title := 'This is window 1';<br />
Window1.Open;<br />
<br />
Window1.HandleMessages;<br />
<br />
Window1.Close;<br />
Window1.Free;<br />
<br />
result := (0);<br />
end;<br />
<br />
<br />
begin<br />
ExitCode := Main;<br />
end.<br />
</source><br />
<br />
== Step 5: Implement event handlers ==<br />
<br />
For this step, we're going to implement support for event-handlers, instead of our class actually performing actions.<br />
<br />
So, let's start doing so (yes, we need to shuffle a large portion of the code around again)<br />
<br />
<source lang="pascal"><br />
unit Step5_IntWinClass;<br />
<br />
{$MODE OBJFPC}{$H+}<br />
<br />
interface<br />
<br />
uses<br />
Intuition;<br />
<br />
<br />
type<br />
TIntuitionMessageRec = record<br />
MsgCode : DWord;<br />
IMsg : PIntuiMessage;<br />
end;<br />
<br />
type<br />
TOnCloseWindowProc = procedure(var DoClose: boolean);<br />
TOnMouseMoveProc = procedure(const IMsg: PIntuiMessage);<br />
TOnMouseButtonsProc = procedure(const IMsg: PIntuiMessage);<br />
<br />
TIntuitionWindowClass = class<br />
private<br />
FHandle : PWindow;<br />
FLeft : LongInt;<br />
FTop : LongInt;<br />
FWidth : LongInt;<br />
FHeight : LongInt;<br />
FTitle : AnsiString;<br />
FStopped : boolean;<br />
FOnCloseWindow : TOnCloseWindowProc;<br />
FOnMouseMove : TOnMouseMoveProc;<br />
FOnMouseButtons : TOnMouseButtonsProc;<br />
protected<br />
procedure MsgCloseWindow(var msg: TIntuitionMessageRec); Message IDCMP_CLOSEWINDOW;<br />
procedure MsgMouseMove(var msg: TIntuitionMessageRec); Message IDCMP_MOUSEMOVE;<br />
procedure MsgMouseButtons(var msg: TIntuitionMessageRec); Message IDCMP_MOUSEBUTTONS;<br />
public // creator/destructor<br />
constructor Create; <br />
destructor Destroy; override;<br />
public // methods<br />
procedure Open;<br />
procedure Close;<br />
procedure HandleMessages;<br />
procedure DefaultHandler(var message); override;<br />
public // properties<br />
property Left : LongInt read FLeft write FLeft;<br />
property Top : LongInt read FTop write FTop;<br />
property Width : LongInt read FWidth write FWidth;<br />
property Height : LongInt read FHeight write FHeight;<br />
property Title : String read FTitle write FTitle;<br />
property Handle : PWindow read FHandle;<br />
public // events<br />
property OnCloseWindow : TOnCloseWindowProc read FOnCloseWindow write FOnCloseWindow;<br />
property OnMouseMove : TOnMouseMoveProc read FOnMouseMove write FOnMouseMove;<br />
property OnMouseButtons : TOnMouseButtonsProc read FOnMouseButtons write FOnMouseButtons;<br />
end;<br />
<br />
<br />
implementation<br />
<br />
uses<br />
SysUtils, Exec, AGraphics, InputEvent;<br />
<br />
<br />
function AsTag(tag: LongWord): LongInt; inline;<br />
begin<br />
Result := LongInt(tag);<br />
end;<br />
<br />
<br />
procedure error(Const msg : string); <br />
begin <br />
raise exception.create(Msg) at <br />
get_caller_addr(get_frame), <br />
get_caller_frame(get_frame); <br />
end; <br />
<br />
<br />
Constructor TIntuitionWindowClass.Create;<br />
begin<br />
Inherited;<br />
<br />
FHandle := nil;<br />
FLeft := 10;<br />
FTop := 10;<br />
FHeight := 30;<br />
FWidth := 30;<br />
FTitle := '';<br />
FStopped := false;<br />
end;<br />
<br />
<br />
Destructor TIntuitionWindowClass.Destroy;<br />
begin<br />
inherited;<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.Open;<br />
var<br />
aTitle : PChar;<br />
begin<br />
if FTitle <> '' then aTitle := PChar(FTitle) else aTitle := nil;<br />
<br />
FHandle := OpenWindowTags( nil,<br />
[<br />
AsTag(WA_Left) , FLeft,<br />
AsTag(WA_Top) , FTop,<br />
AsTag(WA_Width) , FWidth,<br />
AsTag(WA_Height) , FHeight,<br />
AsTag(WA_Title) , aTitle,<br />
// Non use settable flags (for now)<br />
AsTag(WA_Flags) , AsTag(WFLG_CLOSEGADGET or WFLG_DRAGBAR or WFLG_DEPTHGADGET or WFLG_ACTIVATE or WFLG_GIMMEZEROZERO or WFLG_NOCAREREFRESH or WFLG_RMBTRAP or WFLG_REPORTMOUSE),<br />
AsTag(WA_IDCMP) , AsTag(IDCMP_CLOSEWINDOW or IDCMP_MOUSEMOVE or IDCMP_MOUSEBUTTONS),<br />
TAG_END<br />
]);<br />
if not Assigned(FHandle) then Error('Unable to Open Window');<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.Close;<br />
begin<br />
if Assigned(FHandle) <br />
then Intuition.CloseWindow(FHandle)<br />
else Error('Unable to Close Window because the handle is invalid');<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.HandleMessages;<br />
var<br />
msg : PIntuiMessage;<br />
msgrec : TIntuitionMessageRec;<br />
begin<br />
while not FStopped do<br />
begin<br />
WaitPort(FHandle^.UserPort);<br />
<br />
while true do<br />
begin<br />
msg := PIntuiMessage(GetMsg(FHandle^.UserPort));<br />
if not Assigned(msg) then break;<br />
<br />
// WriteLn('ReplyMsg');<br />
ReplyMsg(pMessage(msg));<br />
// WriteLn('Dispatch');<br />
MsgRec.MsgCode := msg^.IClass; <br />
MsgRec.IMsg := msg;<br />
Dispatch(msgrec);<br />
end;<br />
end;<br />
end;<br />
<br />
<br />
(*<br />
http://www.freepascal.org/docs-html/rtl/system/tobject.defaulthandler.html<br />
DefaultHandler is the default handler for messages. If a message has an <br />
unknown message ID (i.e. does not appear in the table with integer message <br />
handlers), then it will be passed to DefaultHandler by the Dispatch method.<br />
*)<br />
<br />
(*<br />
http://www.freepascal.org/docs-html/rtl/system/tobject.dispatch.html<br />
Dispatch looks in the message handler table for a handler that handles <br />
message. The message is identified by the first dword (cardinal) in the <br />
message structure. <br />
<br />
If no matching message handler is found, the message is passed to the <br />
DefaultHandler method, which can be overridden by descendent classes to add <br />
custom handling of messages. <br />
*)<br />
procedure TIntuitionWindowClass.DefaultHandler(var message);<br />
begin<br />
WriteLn('invoked default handler');<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.MsgCloseWindow(var msg: TIntuitionMessageRec);<br />
var<br />
DoClose: boolean = true;<br />
begin<br />
WriteLn('IDCMP_CLOSEWINDOW message received');<br />
<br />
if Assigned(FOnCloseWindow) then FOnCloseWindow(DoClose);<br />
FStopped := DoClose;<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.MsgMouseMove(var msg: TIntuitionMessageRec);<br />
begin<br />
WriteLn('IDCMP_MOUSEMOVE message received');<br />
<br />
if assigned(FOnMouseMove) then FOnMouseMove(msg.IMsg);<br />
end;<br />
<br />
<br />
procedure TIntuitionWindowClass.MsgMouseButtons(var msg: TIntuitionMessageRec);<br />
begin<br />
WriteLn('IDCMP_MOUSEBUTTONS message received');<br />
if Assigned(FOnMouseButtons) then FOnMouseButtons(msg.Imsg);<br />
end;<br />
<br />
end.<br />
</source><br />
<br />
Firstly we add 3 new type declarations for the event handlers (TOnCloseWindowProc, TOnMouseMoveProc and TOnMouseButtonsProc) as these declaration makes it easier for us to add event handling support.<br />
<br />
Then we add 3 new private variables that are able to hold the actual event handlers.<br />
<br />
Lastly we add the event properties so that the user is actually able to assign their custom event handler(s) to them.<br />
<br />
Of course the support for event handling needs to be implemented as well, as can been seen in the code above.<br />
<br />
<br />
Now that the event handling is actually implemented we can make use of it in our main program.<br />
<br />
<source lang="pascal"><br />
program Step5_ClassWindow;<br />
<br />
{$MODE OBJFPC}{$H+}<br />
<br />
uses<br />
Step5_IntWinClass, Exec, AGraphics, Intuition, InputEvent;<br />
<br />
<br />
//*-------------------------------------------------------------------------*/<br />
//* */<br />
//*-------------------------------------------------------------------------*/<br />
<br />
procedure print_text(rp: PRastPort; x: LongInt; y: LongInt; txt: PChar);<br />
begin<br />
GfxMove(rp, x, y);<br />
SetABPenDrMd(rp, 1, 0, JAM2);<br />
GfxText(rp, txt, strlen(txt));<br />
ClearEOL(rp);<br />
end;<br />
<br />
<br />
//*-------------------------------------------------------------------------*/<br />
//* Window1 events<br />
//*-------------------------------------------------------------------------*/<br />
<br />
procedure DoMouseMove(const IMsg: PIntuiMessage);<br />
var<br />
buffer : String[80];<br />
begin<br />
WriteStr(buffer, 'Mouseposition: x=', IMsg^.MouseX, ' y=', IMsg^.MouseY, #0);<br />
print_text(IMsg^.IDCMPWindow^.RPort, 10, 30, @buffer[1]);<br />
end;<br />
<br />
<br />
procedure DoMouseButtons(const IMsg: PIntuiMessage);<br />
begin<br />
case IMsg^.Code of<br />
IECODE_LBUTTON : print_text(IMsg^.IDCMPWindow^.RPort, 10, 60, 'Left mousebutton pressed');<br />
IECODE_LBUTTON or IECODE_UP_PREFIX : print_text(IMsg^.IDCMPWindow^.RPort, 10, 60, 'Left mousebutton released');<br />
IECODE_RBUTTON : print_text(IMsg^.IDCMPWindow^.RPort, 10, 90, 'Right mousebutton pressed');<br />
IECODE_RBUTTON or IECODE_UP_PREFIX : print_text(IMsg^.IDCMPWindow^.RPort, 10, 90, 'Right mousebutton released');<br />
end;<br />
end;<br />
<br />
<br />
procedure DoCloseWindow(var DoClose: boolean);<br />
begin<br />
DoClose := True;<br />
end;<br />
<br />
<br />
//*-------------------------------------------------------------------------*/<br />
//* Main routine */<br />
//*-------------------------------------------------------------------------*/<br />
<br />
function main: integer;<br />
var<br />
Window1 : TIntuitionWindowClass;<br />
begin<br />
Window1 := TIntuitionWindowClass.Create;<br />
Window1.Left := 10;<br />
Window1.Top := 20;<br />
Window1.Height := 200;<br />
Window1.Width := 320;<br />
Window1.Title := 'This is window 1';<br />
Window1.OnMouseMove := @DoMouseMove;<br />
Window1.OnMouseButtons := @DoMouseButtons;<br />
Window1.OnCloseWindow := @DoCloseWindow;<br />
Window1.Open;<br />
<br />
Window1.HandleMessages;<br />
<br />
Window1.Close;<br />
Window1.Free;<br />
<br />
result := (0);<br />
end;<br />
<br />
<br />
begin<br />
ExitCode := Main;<br />
end.<br />
</source><br />
<br />
First thing to notice is that all user-feedback related code has found it's way back in the main program again.<br />
<br />
The second thing that changed inside our main program code is that there are now 3 event-handler routines, and the main routine takes care to assign the event handlers to the instantiated class.<br />
<br />
Depending on which events are assigned our class is acting as desired. You can leave those events that you are not interested in or add new ones inside the class.<br />
<br />
== What's next ? ==<br />
<br />
The above example is far from a full working windowclass. There are several issues with the current implementation:<br />
* Properties such as Left and Height currently retrieve their values from private variables. This is plain wrong as the user can move the window around and resize it. None of the current properties are actually containing real live values. Several approaches can be taken to improve this situation, f.e. by retrieving the actual values or for example by also implementing and reacting on RESIZEWINDOW and MOVEWINDOW messages.<br />
* The current implementation only takes care of a single window. Calling HandleMessages would only work for this one window (and further message handing would be stalled). In order to add support for multiple windows a 'global' message-loop would have to implemented using a single messageport (that is used for all windows). Note that in that case the creation of the window can not have it's IDCMP_xxx flags set on creation, but needs to be done with function ModifyIDCMP()<br />
<br />
The example codes are also kindly provided by magorium and can be found [https://github.com/magorium/fpc-aros-wiki/tree/master/Topics/Intuition_Window_OOP here] (with an additional small bonus for those who are able to locate it).</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Examplecode&diff=839Examplecode2017-09-13T18:25:08Z<p>Molly: </p>
<hr />
<div>[[Category:Examples]]<br />
<br />
Some example-code that need preperation before they can go into the wikibook.<br />
<br />
Original c-code [http://aros.sourceforge.net/documentation/developers/samplecode/helloworld.c]<br />
<source lang="pascal"><br />
Program HelloWorld;<br />
Begin<br />
Writeln('Hello World');<br />
Exit(0);<br />
End.<br />
</source><br />
<br />
<br />
Original c-code [http://aros.sourceforge.net/documentation/developers/samplecode/graphics_simple.c]<br />
<source lang="pascal"><br />
Program graphics_simple;<br />
<br />
{$MODE OBJFPC} {$H+}<br />
<br />
(*<br />
Example for simple drawing routines<br />
*)<br />
<br />
<br />
Uses<br />
chelpers,<br />
amigalib,<br />
aros_exec,<br />
aros_graphics,<br />
aros_intuition,<br />
aros_utility;<br />
<br />
<br />
var<br />
window : pWindow;<br />
cm : pColorMap;<br />
rp : pRastPort;<br />
<br />
const<br />
(*<br />
ObtainBestPen() returns -1 when it fails, therefore we<br />
initialize the pen numbers with -1 to simplify cleanup.<br />
*)<br />
pen1 : A_LONG = -1;<br />
pen2 : A_LONG = -1;<br />
<br />
<br />
{ forward declarations }<br />
procedure draw_simple; forward;<br />
procedure clean_exit(const s: A_CONST_STRPTR); forward;<br />
procedure handle_events; forward;<br />
<br />
<br />
<br />
Function Main: Integer;<br />
begin<br />
window := OpenWindowTags(nil,<br />
[<br />
WA_Left , 50,<br />
WA_Top , 70,<br />
WA_Width , 400,<br />
WA_Height , 350,<br />
<br />
WA_Title , 'Simple Graphics',<br />
WA_Activate , True,<br />
WA_SmartRefresh , true,<br />
WA_NoCareRefresh, true,<br />
WA_GimmeZeroZero, true,<br />
WA_CloseGadget , true,<br />
WA_DragBar , true,<br />
WA_DepthGadget , true,<br />
WA_IDCMP , IDCMP_CLOSEWINDOW,<br />
TAG_END<br />
]);<br />
<br />
if not valid(window) then clean_exit('Can''t open window');<br />
<br />
rp := window^.RPort;<br />
cm := pScreen(window^.WScreen)^.ViewPort.Colormap;<br />
<br />
(* Let's obtain two pens *)<br />
{<br />
pen1 := ObtainBestPen(cm, $FFFF0000, 0, 0, TAG_END);<br />
pen2 := ObtainBestPen(cm, 0 ,0, $FFFF0000, TAG_END);<br />
}<br />
pen1 := ObtainBestPenA(cm, $FFFF0000, 0, 0, nil);<br />
pen2 := ObtainBestPenA(cm, 0 ,0, $FFFF0000, nil);<br />
If (not valid(pen1) or not valid(pen2)) then clean_exit('Can''t allocate pen');<br />
<br />
draw_simple;<br />
handle_events;<br />
<br />
clean_exit(nil);<br />
<br />
result := 0;<br />
end;<br />
<br />
<br />
<br />
procedure draw_simple;<br />
var<br />
array_ : array[0..8-1] of A_WORD; <br />
begin<br />
array_[0] := 50; array_[1] := 200; { Polygon for PolyDraw }<br />
array_[2] := 80; array_[3] := 180;<br />
array_[4] := 90; array_[5] := 220;<br />
array_[6] := 50; array_[7] := 200;<br />
<br />
SetAPen(rp, pen1); { Set foreground color }<br />
SetBPen(rp, pen2); { Set background color }<br />
<br />
WritePixel(rp, 30, 70); { Plot a point }<br />
<br />
SetDrPt(rp, $FF00); { Change line pattern. Set pixels are drawn }<br />
{ with APen, unset with BPen }<br />
Move(rp, 20, 50); { Move cursor to given point }<br />
Draw(rp, 100, 80); { Draw a line from current to given point }<br />
<br />
DrawEllipse(rp, 70, 30, 15, 10); { Draw an ellipse }<br />
<br />
(*<br />
Draw a polygon. Note that the first line is draw from the<br />
end of the last Move() or Draw() command <br />
*) <br />
<br />
PolyDraw(rp, sizeof(array_) div sizeof(A_WORD) div 2, @array_);<br />
<br />
SetDrMd(rp, JAM1); { We want to use only the foreground pen }<br />
Move(rp, 200, 80);<br />
GText(rp, 'Text in default font', 20);<br />
<br />
SetDrPt(rp, $FFFF); { Reset line pattern }<br />
end;<br />
<br />
<br />
<br />
procedure write_text(const s: A_CONST_STRPTR; x: A_WORD; y: A_WORD; mode: A_ULONG);<br />
begin<br />
SetDrMd(rp, mode);<br />
Move(rp, x, y);<br />
GText(rp, s, strlen(s));<br />
end; <br />
<br />
<br />
<br />
procedure handle_events;<br />
var<br />
imsg : pIntuiMessage;<br />
port : pMsgPort;<br />
terminated : boolean;<br />
begin<br />
(*<br />
A simple event handler. This will be exaplained ore detailed<br />
in the Intuition examples.<br />
*)<br />
port := window^.userPort;<br />
terminated := false;<br />
<br />
while not terminated do<br />
begin<br />
Wait(1 shl port^.mp_SigBit);<br />
if (Assign(imsg, GetMsg(port)) <> nil) then<br />
begin<br />
Case imsg^.IClass of<br />
IDCMP_CLOSEWINDOW : terminated := true;<br />
end; { case }<br />
ReplyMsg(pMessage(imsg));<br />
end;<br />
<br />
end;<br />
end;<br />
<br />
<br />
<br />
procedure clean_exit(const s: A_CONST_STRPTR);<br />
begin<br />
If valid(s) then WriteLn(s);<br />
<br />
(* Give back allocated resources *)<br />
if (pen1 <> -1) then ReleasePen(cm, pen1);<br />
if (pen2 <> -1) then ReleasePen(cm, pen2);<br />
if valid(window) then CloseWindow(window);<br />
end;<br />
<br />
<br />
<br />
Begin<br />
Main();<br />
end.<br />
</source><br />
<br />
<br />
<br />
Original c-code [http://aros.sourceforge.net/documentation/developers/samplecode/graphics_bitmap.c]<br />
<source lang="pascal"><br />
Program graphics_bitmap;<br />
<br />
{$MODE OBJFPC} {$H+}<br />
<br />
(*<br />
Example for bitmaps<br />
*)<br />
<br />
Uses<br />
chelpers,<br />
amigalib,<br />
aros_exec,<br />
aros_graphics,<br />
aros_intuition,<br />
aros_utility;<br />
<br />
<br />
var<br />
window : pWindow;<br />
cm : pColorMap;<br />
win_rp : pRastPort;<br />
<br />
Const<br />
BMWIDTH = (50); <br />
BMHEIGHT = (50);<br />
<br />
var<br />
bm : pBitmap;<br />
bm_rp : pRastPort;<br />
<br />
const<br />
(*<br />
ObtainBestPen() returns -1 when it fails, therefore we<br />
initialize the pen numbers with -1 to simplify cleanup.<br />
*)<br />
pen1 : A_LONG = -1;<br />
pen2 : A_LONG = -1;<br />
<br />
<br />
{ forward declarations }<br />
procedure draw_bitmap; forward;<br />
procedure clean_exit(const s: A_CONST_STRPTR); forward;<br />
procedure handle_events; forward;<br />
<br />
<br />
{<br />
function RASSIZE(w: integer; h: Integer): Integer; inline;<br />
begin<br />
result := ( (h) * ( ((w)+15) shr 3 and $FFFE ));<br />
end;<br />
}<br />
Procedure DrawCircle(rp: pRastPort; cx: A_LONG; cy: A_LONG; r:A_LONG); inline;<br />
begin<br />
DrawEllipse(rp, cx, cy, r, r);<br />
end;<br />
<br />
<br />
<br />
<br />
Function Main: Integer;<br />
begin<br />
window := OpenWindowTags(nil,<br />
[<br />
WA_Left , 50,<br />
WA_Top , 70,<br />
WA_Width , 400,<br />
WA_Height , 350,<br />
<br />
WA_Title , 'Bitmap Graphics',<br />
WA_Activate , True,<br />
WA_SmartRefresh , true,<br />
WA_NoCareRefresh, true,<br />
WA_GimmeZeroZero, true,<br />
WA_CloseGadget , true,<br />
WA_DragBar , true,<br />
WA_DepthGadget , true,<br />
WA_IDCMP , IDCMP_CLOSEWINDOW,<br />
TAG_END<br />
]);<br />
<br />
if not valid(window) then clean_exit('Can''t open window');<br />
<br />
win_rp := window^.RPort;<br />
cm := pScreen(window^.WScreen)^.ViewPort.Colormap;<br />
<br />
(* Let's obtain two pens *)<br />
{<br />
pen1 := ObtainBestPen(cm, $FFFF0000, 0, 0, TAG_END);<br />
pen2 := ObtainBestPen(cm, 0 ,0, $FFFF0000, TAG_END);<br />
}<br />
pen1 := ObtainBestPenA(cm, $FFFF0000, 0, 0, nil);<br />
pen2 := ObtainBestPenA(cm, 0 ,0, $FFFF0000, nil);<br />
<br />
If (not valid(pen1) or not valid(pen2)) then clean_exit('Can''t allocate pen');<br />
<br />
draw_bitmap;<br />
handle_events;<br />
<br />
clean_exit(nil);<br />
<br />
result := 0;<br />
end;<br />
<br />
<br />
<br />
procedure draw_bitmap;<br />
var<br />
Depth : A_UWORD; x: integer;<br />
begin<br />
(*<br />
Get the depth of the screen. Don't peek in the structures, always use<br />
GetBitMapAttr().<br />
*)<br />
<br />
depth := GetBitMapAttr(win_rp^.BitMap, BMA_DEPTH);<br />
<br />
(*<br />
Create new bitmap. With BMF_MINPLANES and the bitmap pointer we are saying<br />
that we want a bitmap which is smaller than the target bitmap.<br />
*)<br />
bm := AllocBitMap(BMWIDTH, BMHEIGHT, depth, BMF_MINPLANES, win_rp^.BitMap);<br />
if not valid(bm) then clean_exit('Can''t allocate bitmap');<br />
<br />
bm_rp := CreateRastPort; { create rastport for our bitmap }<br />
if not valid(bm_rp) then clean_exit('Can''t allocate rastport!');<br />
bm_rp^.Bitmap := bm;<br />
<br />
(*<br />
Now we can draw into our bitmap. Take care that the bitmap has no<br />
clipping rectangle. This means we must not draw over the limits.<br />
*)<br />
<br />
SetRast(bm_rp, 0); { fill whole bitmap with color 0 }<br />
SetAPen(bm_rp, pen1);<br />
DrawCircle(bm_rp, 24, 24, 24);<br />
SetAPen(bm_rp, pen2);<br />
move(bm_rp, 0, 0);<br />
Draw(bm_rp, 49, 49);<br />
Move(bm_rp, 49, 0);<br />
Draw(bm_rp, 0, 49);<br />
Draw(bm_rp, 49, 49);<br />
Draw(bm_rp, 49, 0);<br />
Draw(bm_rp, 0, 0); <br />
Draw(bm_rp, 0, 49);<br />
<br />
<br />
{ for x := 20 to pred(400) step 30 do }<br />
x := 20;<br />
while x < 400 do<br />
begin<br />
(* Blit the bitmap into the window *)<br />
ClipBlit(bm_rp, 0, 0, win_rp, x, x div 2, BMWIDTH, BMHEIGHT, $C0); <br />
inc(x, 30);<br />
end;<br />
end;<br />
<br />
<br />
<br />
procedure handle_events;<br />
var<br />
imsg : pIntuiMessage;<br />
port : pMsgPort;<br />
terminated : boolean;<br />
begin<br />
(*<br />
A simple event handler. This will be exaplained ore detailed<br />
in the Intuition examples.<br />
*)<br />
port := window^.userPort;<br />
terminated := false;<br />
<br />
while not terminated do<br />
begin<br />
Wait(1 shl port^.mp_SigBit);<br />
if (Assign(imsg, GetMsg(port)) <> nil) then<br />
begin<br />
Case imsg^.IClass of<br />
IDCMP_CLOSEWINDOW : terminated := true;<br />
end; { case }<br />
ReplyMsg(pMessage(imsg));<br />
end;<br />
<br />
end;<br />
end;<br />
<br />
<br />
<br />
procedure clean_exit(const s: A_CONST_STRPTR);<br />
begin<br />
If valid(s) then WriteLn(s);<br />
<br />
(* Give back allocated resources *)<br />
if valid(bm) then FreeBitMap(bm);<br />
if valid(bm_rp) then FreeRastPort(bm_rp);<br />
if (pen1 <> -1) then ReleasePen(cm, pen1);<br />
if (pen2 <> -1) then ReleasePen(cm, pen2);<br />
if valid(window) then CloseWindow(window);<br />
end;<br />
<br />
<br />
<br />
Begin<br />
Main();<br />
end.<br />
</source><br />
<br />
<br />
<br />
Original c-code [http://aros.sourceforge.net/documentation/developers/samplecode/graphics_area.c]<br />
<source lang="pascal"><br />
Program graphics_area;<br />
<br />
{$MODE OBJFPC} {$H+}<br />
<br />
(*<br />
Example for area drawing routines<br />
*)<br />
<br />
Uses<br />
chelpers,<br />
amigalib,<br />
aros_exec,<br />
aros_graphics,<br />
aros_intuition,<br />
aros_utility;<br />
<br />
<br />
var<br />
window : pWindow;<br />
cm : pColorMap;<br />
rp : pRastPort;<br />
<br />
const<br />
(*<br />
ObtainBestPen() returns -1 when it fails, therefore we<br />
initialize the pen numbers with -1 to simplify cleanup.<br />
*)<br />
pen1 : A_LONG = -1;<br />
pen2 : A_LONG = -1;<br />
<br />
MAX_POINTS = 50;<br />
<br />
var <br />
ai : TAreaInfo;<br />
tr : TTmpRas;<br />
trbuf : Pointer;<br />
aibuf : array[0..(MAX_POINTS+1)*5] of A_UBYTE;<br />
<br />
<br />
{ forward declarations }<br />
procedure draw_area; forward;<br />
procedure clean_exit(const s: A_CONST_STRPTR); forward;<br />
procedure handle_events; forward;<br />
<br />
<br />
<br />
function RASSIZE(w: integer; h: Integer): Integer; inline;<br />
begin<br />
result := ( (h) * ( ((w)+15) shr 3 and $FFFE ));<br />
end;<br />
<br />
<br />
<br />
<br />
Function Main: Integer;<br />
begin<br />
window := OpenWindowTags(nil,<br />
[<br />
WA_Left, 50,<br />
WA_Top, 70,<br />
WA_Width, 400,<br />
WA_Height, 350,<br />
<br />
WA_Title, 'Area Graphics',<br />
WA_Activate, True,<br />
WA_SmartRefresh, true,<br />
WA_NoCareRefresh, true,<br />
WA_GimmeZeroZero, true,<br />
WA_CloseGadget, true,<br />
WA_DragBar, true,<br />
WA_DepthGadget, true,<br />
WA_IDCMP, IDCMP_CLOSEWINDOW,<br />
TAG_END<br />
]);<br />
<br />
if not valid(window) then clean_exit('Can''t open window');<br />
<br />
rp := window^.RPort;<br />
cm := pScreen(window^.WScreen)^.ViewPort.Colormap;<br />
<br />
(* Let's obtain two pens *)<br />
{<br />
pen1 := ObtainBestPen(cm, $FFFF0000, 0, 0, TAG_END);<br />
pen2 := ObtainBestPen(cm, 0 ,0, $FFFF0000, TAG_END);<br />
}<br />
pen1 := ObtainBestPenA(cm, $FFFF0000, 0, 0, nil);<br />
pen2 := ObtainBestPenA(cm, 0 ,0, $FFFF0000, nil);<br />
<br />
If (not valid(pen1) or not valid(pen2)) then clean_exit('Can''t allocate pen');<br />
<br />
draw_area;<br />
handle_events;<br />
<br />
clean_exit(nil);<br />
<br />
result := 0;<br />
end;<br />
<br />
<br />
<br />
procedure draw_area;<br />
begin<br />
(*<br />
The area drawing functions need two additional<br />
structures, which have to be linked with the rastport.<br />
<br />
First we set the AreaInfo.<br />
The size of 'aibuf' must be at least 5 times the number<br />
of vertexes.<br />
Take care: when you define the variable 'aibuf' locally, you<br />
have to set all fields to 0. <br />
*)<br />
<br />
InitArea(@ai, @aibuf, sizeOf(aibuf) div 5);<br />
<br />
(*<br />
Then we allocate a raster. It must have the size of<br />
the drawing area. We have a GimmeZeroZero window with<br />
no size gadget, therefore we can use the GZZ sizes. <br />
*)<br />
<br />
trbuf := AllocRaster(window^.GZZWidth, window^.GZZHeight);<br />
if not valid(trbuf) then clean_exit('TmpRas buffer allocation failed!');<br />
<br />
(*<br />
The raster must be initialized. The reason for RASSIZE() is<br />
that we must round up the width to a 16 bit value<br />
*)<br />
InitTmpRas(@tr, trbuf, RASSIZE(window^.GZZWidth, Window^.GZZHeight));<br />
<br />
rp^.AreaInfo := @ai; { Link areainfo to rastport }<br />
rp^.TmpRas := @tr; { Link tempras to rastport }<br />
<br />
SetAPen(rp, pen1); { Set foreground color }<br />
SetBPen(rp, pen2); { Set background color }<br />
<br />
AreaMove(rp, 50, 200); { set start point of 1st triangle }<br />
AreaDraw(rp, 300, 100);<br />
AreaDraw(rp, 280, 300); <br />
<br />
AreaMove(rp, 200, 50); { Set start point of 2nd triangle }<br />
AreaDraw(rp, 210, 100);<br />
AreaDraw(rp, 300, 75);<br />
<br />
AreaEllipse(rp, 70, 70, 40, 30); { Add an ellipse }<br />
<br />
AreaEnd(rp); { Do the rendering }<br />
end;<br />
<br />
<br />
<br />
procedure handle_events;<br />
var<br />
imsg : pIntuiMessage;<br />
port : pMsgPort;<br />
terminated : boolean;<br />
begin<br />
(*<br />
A siple event handler. This will be exaplained ore detailed<br />
in the Intuition examples.<br />
*)<br />
port := window^.userPort;<br />
terminated := false;<br />
<br />
while not terminated do<br />
begin<br />
Wait(1 shl port^.mp_SigBit);<br />
if (Assign(imsg, GetMsg(port)) <> nil) then<br />
begin<br />
Case imsg^.IClass of<br />
IDCMP_CLOSEWINDOW : terminated := true;<br />
end; { case }<br />
ReplyMsg(pMessage(imsg));<br />
end;<br />
<br />
end;<br />
end;<br />
<br />
<br />
<br />
procedure clean_exit(const s: A_CONST_STRPTR);<br />
begin<br />
If valid(s) then WriteLn(s);<br />
<br />
(* Give back allocated resources *)<br />
if valid(trbuf) then FreeRaster(trbuf, window^.GZZWidth, window^.GZZHeight);<br />
if (pen1 <> -1) then ReleasePen(cm, pen1);<br />
if (pen2 <> -1) then ReleasePen(cm, pen2);<br />
if valid(window) then CloseWindow(window);<br />
<br />
end;<br />
<br />
<br />
<br />
Begin<br />
Main();<br />
end.<br />
</source><br />
<br />
<br />
<br />
Original c-code [http://aros.sourceforge.net/documentation/developers/samplecode/graphics_font.c]<br />
<source lang="pascal"><br />
Program graphics_font;<br />
<br />
{$MODE OBJFPC} {$H+}<br />
<br />
(*<br />
Example for fonts<br />
*)<br />
<br />
<br />
Uses<br />
chelpers,<br />
amigalib,<br />
aros_exec,<br />
aros_graphics,<br />
aros_intuition,<br />
aros_diskfont,<br />
aros_utility;<br />
<br />
<br />
var<br />
window : pWindow;<br />
cm : pColorMap;<br />
rp : pRastPort;<br />
font : pTextFont;<br />
<br />
const<br />
(*<br />
ObtainBestPen() returns -1 when it fails, therefore we<br />
initialize the pen numbers with -1 to simplify cleanup.<br />
*)<br />
pen1 : A_LONG = -1;<br />
pen2 : A_LONG = -1;<br />
<br />
<br />
{ forward declarations }<br />
procedure draw_font; forward;<br />
procedure write_text(const s: A_CONST_STRPTR; x: A_WORD; y: A_WORD; mode: A_ULONG); forward;<br />
procedure clean_exit(const s: A_CONST_STRPTR); forward;<br />
procedure handle_events; forward;<br />
<br />
<br />
<br />
Function Main: Integer;<br />
begin<br />
window := OpenWindowTags(nil,<br />
[<br />
WA_Left , 50,<br />
WA_Top , 70,<br />
WA_Width , 400,<br />
WA_Height , 350,<br />
<br />
WA_Title , 'Fonts',<br />
WA_Activate , True,<br />
WA_SmartRefresh , true,<br />
WA_NoCareRefresh, true,<br />
WA_GimmeZeroZero, true,<br />
WA_CloseGadget , true,<br />
WA_DragBar , true,<br />
WA_DepthGadget , true,<br />
WA_IDCMP , IDCMP_CLOSEWINDOW,<br />
TAG_END<br />
]);<br />
<br />
if not valid(window) then clean_exit('Can''t open window');<br />
<br />
rp := window^.RPort;<br />
cm := pScreen(window^.WScreen)^.ViewPort.Colormap;<br />
<br />
(* Let's obtain two pens *)<br />
{<br />
pen1 := ObtainBestPen(cm, $FFFF0000, 0, 0, TAG_END);<br />
pen2 := ObtainBestPen(cm, 0 ,0, $FFFF0000, TAG_END);<br />
}<br />
pen1 := ObtainBestPenA(cm, $FFFF0000, 0, 0, nil);<br />
pen2 := ObtainBestPenA(cm, 0 ,0, $FFFF0000, nil);<br />
<br />
If (not valid(pen1) or not valid(pen2)) then clean_exit('Can''t allocate pen');<br />
<br />
draw_font;<br />
handle_events;<br />
<br />
clean_exit(nil);<br />
<br />
result := 0;<br />
end;<br />
<br />
<br />
<br />
procedure draw_font;<br />
var<br />
style : A_ULONG;<br />
ta : TTextAttr;<br />
begin<br />
ta.ta_name := 'arial.font'; { Font name }<br />
ta.ta_YSize := 15; { Font size }<br />
ta.ta_Style := FSF_ITALIC or FSF_BOLD; { Font style }<br />
ta.ta_Flags := 0;<br />
<br />
if not valid(assign(font, OpenDiskFont(@ta))) then<br />
begin<br />
clean_exit('Can''t open font');<br />
end;<br />
<br />
<br />
SetAPen(rp, pen1);<br />
SetBPen(rp, pen2);<br />
<br />
SetFont(rp, font); { Linking the font to the rastport }<br />
<br />
(*<br />
In the TextAttr above we've queried a font with the styles italic and bold.<br />
OpenDiskFont() tries to open a font with this styles. If this fails<br />
the styles have to be generated algorithmically. To avoid that a<br />
style will be added to a font which has already the style intrinsically,<br />
we've first to ask. AskSoftStyle() returns a mask where all bits for styles<br />
which have to be added algorithmically are set.<br />
*)<br />
style := AskSoftStyle(rp);<br />
<br />
(*<br />
We finally set the style. SetSoftStyle() compares with the mask from<br />
AskSoftStyle() to avoid that an intrinsic style is applied again.<br />
*)<br />
SetSoftStyle(rp, style, FSF_ITALIC or FSF_BOLD);<br />
<br />
(*<br />
Now we write some text. Additionally the effects of the<br />
rastport modes are demonstrated<br />
*)<br />
write_text('JAM1' , 100, 60, JAM1);<br />
write_text('JAM2' , 100, 80, JAM2);<br />
write_text('COMPLEMENT' , 100, 100, COMPLEMENT);<br />
write_text('INVERSVID' , 100, 120, INVERSVID);<br />
write_text('JAM1|INVERSVID' , 100, 140, JAM1 or INVERSVID);<br />
write_text('JAM2|INVERSVID' , 100, 160, JAM2 or INVERSVID);<br />
write_text('COMPLEMENT|INVERSVID' , 100, 180, COMPLEMENT or INVERSVID);<br />
end;<br />
<br />
<br />
<br />
procedure write_text(const s: A_CONST_STRPTR; x: A_WORD; y: A_WORD; mode: A_ULONG);<br />
begin<br />
SetDrMd(rp, mode);<br />
Move(rp, x, y);<br />
GText(rp, s, strlen(s));<br />
end; <br />
<br />
<br />
<br />
procedure handle_events;<br />
var<br />
imsg : pIntuiMessage;<br />
port : pMsgPort;<br />
terminated : boolean;<br />
begin<br />
(*<br />
A simple event handler. This will be exaplained ore detailed<br />
in the Intuition examples.<br />
*)<br />
port := window^.userPort;<br />
terminated := false;<br />
<br />
while not terminated do<br />
begin<br />
Wait(1 shl port^.mp_SigBit);<br />
if (Assign(imsg, GetMsg(port)) <> nil) then<br />
begin<br />
Case imsg^.IClass of<br />
IDCMP_CLOSEWINDOW : terminated := true;<br />
end; { case }<br />
ReplyMsg(pMessage(imsg));<br />
end;<br />
<br />
end;<br />
end;<br />
<br />
<br />
<br />
procedure clean_exit(const s: A_CONST_STRPTR);<br />
begin<br />
If valid(s) then WriteLn(s);<br />
<br />
(* Give back allocated resources *)<br />
if (pen1 <> -1) then ReleasePen(cm, pen1);<br />
if (pen2 <> -1) then ReleasePen(cm, pen2);<br />
if valid(font) then CloseFont(font);<br />
if valid(window) then CloseWindow(window);<br />
end;<br />
<br />
<br />
<br />
Begin<br />
Main();<br />
end.<br />
</source><br />
<br />
<br />
<br />
Original c-code []<br />
<source lang="pascal"><br />
<br />
</source><br />
<br />
<br />
<br />
Original c-code []<br />
<source lang="pascal"><br />
<br />
</source><br />
<br />
<br />
<br />
Original c-code []<br />
<source lang="pascal"><br />
<br />
</source><br />
<br />
<br />
<br />
Original c-code []<br />
<source lang="pascal"><br />
<br />
</source><br />
<br />
<br />
<br />
Original c-code []<br />
<source lang="pascal"><br />
<br />
</source><br />
<br />
<br />
<br />
Original c-code []<br />
<source lang="pascal"><br />
<br />
</source><br />
<br />
<br />
<br />
Original c-code []<br />
<source lang="pascal"><br />
<br />
</source></div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Category:Examples&diff=838Category:Examples2017-09-13T18:23:54Z<p>Molly: Created blank page</p>
<hr />
<div></div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Trinity_in_Trouble&diff=837Trinity in Trouble2017-09-10T21:43:08Z<p>Molly: /* agraphics */ add entry for GfxBase variable</p>
<hr />
<div><div style="background-color: #FFFF99; -khtml-border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius:<br />
15px; border: 2px solid #000; padding: 10px; margin:10px 200px 10px;"><br />
<center><br />
'''Note'''<br />
</center><br />
<br />
<center><br />
Based on Free Pascal branch "fixes 3.0"<br />
</center><br />
<br />
Feel free to add delete or change status.<br />
</div><br />
<br />
Our trinity consist of Amiga, AROS and MorphOS.<br />
<br />
Unfortunately, there are (still) some incompatibilities and/or some lack of consistency here and there. The idea is to have a list here that mentions them all. Layout may change, i simply had to start somewhere.<br />
<br />
NOTE: I thought there is no use to mention the Tag, Tags, Taglist, etc. inconsistency and additional incompatibilities that this causes. We are all aware of those and will hopefully get some unity in the future<br />
<br />
== Table of units ==<br />
<br />
The triforce repo introduced the usage of unit [//github.com/magorium/fpc-triforce/blob/master/Sys/Trinity/TriniTypes.pas trinitypes] in order to fight the type inconsistencies (at least for the new units, not the examples). The contents of trinitype will be extended as things progresses. A simple load-search-replace-save routine/program can be applied since the used types are fairly unique.<br />
<br />
{| class="wikitable"<br />
|+ List of available units per platform:<br />
|-<br />
! Unit !! Category !! OS3.x !! AROS !! MorphOS !! Remark(s)<br />
|-<br />
| [[#agraphics]] || graphics.library || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| ahi || ahi.device || class="working" | yes || class="progress" | MAG || class="working" | yes || <br />
|-<br />
| ahi_sub || ahi_sub.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| akeyboard || keyboard.device || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/akeyboard.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/akeyboard.pas test] || <br />
|-<br />
| amarquee || amarquee.library || class="working" | yes || class="unknown" | n/a || class="unknown" | n/a || <br />
|-<br />
| [[#amigados]] || dos.library || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| amigaguide || amigaguide.library || class="working" | yes || class="progress" | MAG || class="not" | no || No use though, AROS' lib functions are not implemented<br />
|-<br />
| [[#amigalib]] || amigalib || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/amigalib.pas test] || class="working" | yes || Unit amigalib has it's own status page, [[AmigaLib]]<br />
|-<br />
| amigaprinter || printer.device || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/amigaprinter.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/amigaprinter.pas test] || <br />
|-<br />
| aroslib || aros.library || class="unknown" | n/a || class="working" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/aroslib.pas yes] || class="unknown" | n/a || <br />
|-<br />
| [[#asl]] || asl.library || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| audio || audio.device || class="working" | yes || class="progress" | MAG || class="not" | no || <br />
|-<br />
| bootblock || bootblock.device || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| bullet || bullet.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| cd || cd.device || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| clipboard || clipboard.device || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| colorwheel || colorwheel.gadget || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| [[#commodities]] || commodities.library || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/commodities.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/commodities.pas test] ||<br />
|-<br />
| configregs || see expansion || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/configregs.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/configregs.pas test] ||<br />
|-<br />
| configvars || see expansion || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/configvars.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/configvars.pas test] ||<br />
|-<br />
| console || console.device || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/console.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/console.pas test] ||<br />
|-<br />
| conunit || console.device || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/conunit.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/conunit.pas test] ||<br />
|-<br />
| cgxvideo || || class="not" | no || class="not" | no || class="working" | [//svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32343 yes] || Recently added to MorphOS by Chain-Q<br />
|-<br />
| cybergraphics || cybergraphics.library || class="working" | yes || class="working" | yes || class="working" | [//svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32343 yes] || Recently added to MorphOS by Chain-Q<br />
|-<br />
| datatypes || datatypes.library || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/datatypes.pas test] || class="working" | yes || Unit source (AROS) is still a bit of a mess. Amiga version needs an overhaul (no PObject_ being used where it should -> concerns most if not all of declared functions).<br />
|-<br />
| diskfont || diskfont.library || class="working" | yes || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/diskfont.pas test] || A diskfont unit was added to MorphOS by Chain-Q<br />
|-<br />
| [[#exec]] || exec.library || class="working" | yes || class="working" | yes || class="working" | yes || MorphOS: noticed some things missing in comparison to SDK 3.9 (this is meant as a reminder to verify this unit)<br />
|-<br />
| [[#expansion]] || expansion.library || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/expansion.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/expansion.pas test] ||<br />
|-<br />
| expansionbase || see expansion || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/expansionbase.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/expansionbase.pas test] ||<br />
|-<br />
| [[#gadtools]] || gadtools.library || class="working" | yes || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/gadtools.pas test] || <br />
|-<br />
| gameport || gameport.device || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| get9 || ? || class="unknown" | n/a || class="unknown" | n/a || class="working" | yes || silly MorphOS-only historic .library, Pascal interface unit exist as a joke, ignore this :) <br />
|-<br />
| gradientslider || gradientslider.gadget || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| gtlayout || gtlayout.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| guigfx || guigfx.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| hardblocks || hardblocks.device || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| hardware || hardware.resource || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| icon || icon.library || class="working" | yes || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/icon.pas test] ||<br />
|-<br />
| identify || identify.lbrary || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| iffparse || iffparse.library || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| input || input.device || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/input.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/input.pas test] || <br />
|-<br />
| inputevent || see input.device || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| [[#intuition]] || intuition.library || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| [[#keymap]] || keymap.library || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| kvm || ? || class="unknown" | n/a || class="unknown" | n/a || class="unknown" | n/a || This unit is now dropped. It was a helper unit for the KVM stuff, but it's no longer used, and it doesn't provide any other useful functionality. It's "API" was never meant for public use either. The idea was, mouse unit could be used w/o the video and keyboard, and doesn't depend on each other. But it doesn't really matter any more. I removed it from trunk.<br />
|-<br />
| layers || layers.library || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| locale || locale.library || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/locale.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/locale.pas test] || <br />
|-<br />
| lowlevel || lowlevel.library || class="working" | yes || class="progress" | MAG || class="not" | no || <br />
|-<br />
| lucyplay || lucyplay.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| mui || mui.library (ZUNE) || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| muihelper || see mui.library || class="not" | no || class="not" | no || class="working" | yes || MUIHelper contains some Pascal syntax-sugar and helpers for writing MUI code. It can be moved to ami-extra Package when it's verified it works everywhere.<br />
|-<br />
| mysticview || mysticview.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| nonvolatile || nonvolatile.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| parallel || parallel.device || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| picasso96api || picasso library || class="working" | yes || class="unknown" | n/a || class="unknown" | n/a || MorphOS and AROS don't have Picasso96 support.<br />
|-<br />
| preferences || preferences.library || class="working" | yes || class="not" | n/a || class="not" | no || <br />
|-<br />
| prefs || see preferences || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/prefs.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/prefs.pas test] || <br />
|-<br />
| prtbase || printer.device || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| prtgfx || ? || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| ptreplay || ptreplay.library || class="working" | yes || class="progress" | MAG || class="not" | no || <br />
|-<br />
| realtime || realtime.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| render || render.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| reqtools || reqtools.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| [[#rexx]] || rexxsyslib.library || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/rexx.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/rexx.pas test] || <br />
|-<br />
| romboot_base || || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| scsidisk || scscidisk.device || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| serial || serial.device || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| tapedeck || tapedeck.gadget || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| timer || timer.device || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| tinygl || tinygl library || class="unknown" | n/a || class="unknown" | n/a || class="working" | yes || TinyGL is MorphOS specific and the unit there is only used to get the OpenGL package of FPC running.<br />
|-<br />
| trackdisk || trackdisk.device || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| translator || translator.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| triton || triton.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| tritonmacros || macros for triton || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| ttengine || ttengine.library || class="working" | yes || class="progress" | MAG || class="not" | no || <br />
|-<br />
| utility || utility.library || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| [[#workbench]] || workbench.library || class="working" | yes || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/workbench.pas test] || <br />
|-<br />
| xadmaster || xadmaster.library || class="working" | yes || class="progress" | MAG || class="not" | no || <br />
|-<br />
| zlib || zlib.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
|<br />
<!-- Util Units --><br />
|-<br />
| <br />
|-<br />
| amigautils || amigautils || class="working" | yes || class="not" | no || class="not" | no || Should be moved to ami-extra when it's verified it works elsewhere and contains no deprecated code.<br />
|-<br />
| amsgbox || msgbox using easyasl || class="working" | yes || class="not" | no || class="not" | no || Should be moved to ami-extra when it's verified it works elsewhere and contains no deprecated code.<br />
|-<br />
| consoleio || crt using console || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| deadkeys || console deadkeys || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| doublebuffer || || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| easyasl || easyasl.library || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| hisoft || || class="working" | yes || class="not" | no || class="not" | no || Should be moved to ami-extra when it's verified it works elsewhere and contains no deprecated code.<br />
|-<br />
| linklist || || class="working" | yes || class="not" | no || class="not" | no || Should be moved to ami-extra when it's verified it works elsewhere and contains no deprecated code.<br />
|-<br />
| longarray || || class="working" | yes || class="working" | yes || class="not" | no || <br />
|-<br />
| pastoc || || class="working" | yes || class="not" | no || class="not" | no || This is deprecated, and must *not* be ported to other platforms.<br />
|-<br />
| pcq || || class="working" | yes || class="not" | no || class="not" | no || Should be moved to ami-extra when it's verified it works elsewhere and contains no deprecated code.<br />
|-<br />
| systemvartags || || class="working" | yes || class="not" | no || class="not" | no || NOTE: this unit cannot be used when trying to create executables to be run on AROS-m68k because AROS misses bullet.library.<br />
|-<br />
| tagsarray || || class="working" | yes || class="working" | yes || class="not" | no || Tagsarray implementation is not thread safe, at least on classic.<br />
|-<br />
| timerutils || || class="working" | yes || class="not" | no || class="not" | no || Should be moved to ami-extra when it's verified it works elsewhere.<br />
|-<br />
| vartags || || class="working" | yes || class="not" | no || class="not" | no || <br />
|-<br />
| wbargs || || class="working" | yes || class="not" | no || class="not" | no || <br />
|}<br />
<br />
== List of issues ==<br />
<br />
=== agraphics ===<br />
<br />
* '''record: Isrvstr'''<br />
: AROS: Missing<br />
* '''var: GfxBase'''<br />
: AROS: type is PGfxBase<br />
: Amiga: type is PLibrary<br />
: MorphOS: type is Pointer<br />
: Note: According to rkrm it should be PGfxBase<br />
<br />
=== amigados ===<br />
<br />
* '''function: DOSRename()'''<br />
: aros: function DOSRename(const OldName: STRPTR; const NewName: STRPTR): LongInt; syscall AOS_DOSBase 13;<br />
: os4: function DosRename(const OldName: STRPTR; const NewName: STRPTR): LongBool; syscall IDos 108;<br />
: os3: FUNCTION DOSRename(const oldName : pCHAR location 'd1';const newName : pCHAR location 'd2') : LongBool; syscall _DOSBase 078;<br />
: mos: function dosRename(oldName: PChar location 'd1'; newName: PChar location 'd2'): LongInt; SysCall MOS_DOSBase 78;<br />
: remark: PChar vs STRPTR (should be STRPTR) and LongInt vs LongBool (should be BOOL)<br />
<br />
* '''function: Examine()'''<br />
: aros: function Examine(Lock: BPTR; FileInfoBlock: PFileInfoBlock): LongInt; syscall AOS_DOSBase 17;<br />
: os4: function Examine(Lock: BPTR; FileInfoBlock: PFileInfoBlock): LongBool; syscall IDos 124;<br />
: os3: FUNCTION Examine(lock: BPTR location 'd1'; fileInfoBlock: pFileInfoBlock location 'd2'): LongBool; syscall _DOSBase 102;<br />
: mos: function Examine(lock: BPTR location 'd1'; fileInfoBlock: PFileInfoBlock location 'd2'): LongInt; SysCall MOS_DOSBase 102;<br />
: remark: LongInt vs LongBool (should be BOOL)<br />
<br />
* '''function: Execute()'''<br />
: aros: function Execute(const String_: STRPTR; Input: BPTR; Output: BPTR): LongInt; syscall AOS_DOSBase 37;<br />
: os4: function Execute(const String_: STRPTR; File_: BPTR; File2: BPTR): LongBool; syscall IDos 204;<br />
: os3: FUNCTION Execute(const string_ : pCHAR location 'd1'; file_ : LONGINT location 'd2'; file2 : LONGINT location 'd3') : LongBool; syscall _DOSBase 222;<br />
: mos: function Execute(string1: PChar location 'd1'; file1 : BPTR location 'd2'; file2 : BPTR location 'd3'): LongBool;<br />
: remark: PChar vs STRPTR (should be STRPTR), BPTR vs LONGINT (should be BPTR) and LongInt vs LongBool (should be BOOL)<br />
<br />
* '''function: ExNext()'''<br />
: aros: function ExNext(Lock: BPTR; FileInfoBlock: PFileInfoBlock): LongInt; syscall AOS_DOSBase 18;<br />
: os4: function ExNext(Lock: BPTR; FileInfoBlock: PFileInfoBlock): LongBool; syscall IDos 128;<br />
: os3: FUNCTION ExNext(lock: BPTR location 'd1'; fileInfoBlock: pFileInfoBlock location 'd2'): LongBool; syscall _DOSBase 108;<br />
: mos: function ExNext(lock: BPTR location 'd1'; fileInfoBlock: PFileInfoBlock location 'd2'): LongInt; SysCall MOS_DOSBase 108;<br />
: remark: LongInt vs LongBool (should be BOOL)<br />
<br />
=== amigalib ===<br />
<br />
* '''function: CreatePort()'''<br />
: Missing for AROS and MorphOS<br />
* '''function: DeletePort()'''<br />
: Missing for AROS and MorphOS<br />
* '''function: CreateExtIO()'''<br />
: Missing for AROS and MorphOS<br />
* '''function: DeleteExtIO()'''<br />
: Missing for AROS and MorphOS<br />
* '''function: DoMethod()'''<br />
: Amiga version seems missing completely.<br />
: Implemented versions for AROS and MorphOS don't follow autodocs 100% and are inconsistent.<br />
* '''function: CoerceMethod()'''<br />
: MorphOS version seems missing completely (including CoerceMethodA().<br />
: Amiga version has CoerceMethodA() implemented but no CoerceMethod()<br />
: Implemented versions for AROS and Amiga don't follow autodocs 100% and are inconsistent.<br />
<br />
<br />
=== asl ===<br />
<br />
AROS' implementation of asl uses 'modern' function names, ending with or without an A depending whether it's a varargs version or not. Amiga and MorphOS implementations uses 'old-style' naming scheme as dictated by classic autodocs. In order to 'fix' this, unit trinity re-declares asl functions using the 'old-style' naming scheme (it was the quickest fix).<br />
<br />
=== exec ===<br />
<br />
* '''structure: Hook'''<br />
: AROS version, entries are not IPTR rather APTR. [http://amigadev.elowar.com/read/ADCD_2.1/Includes_and_Autodocs_3._guide/node0617.html#line27 Amiga version] uses ULONG's for h_entry and h_subentry, but that doesn't comply on 64-bit. AROS version can be found [https://trac.aros.org/trac/browser/AROS/branches/ABI_V0-on-trunk-20141231/AROS/compiler/include/utility/hooks.h?rev=51123 here].<br />
: Remark: In case it's compatibility holding back the change, i'm willing to create a multiplatform advanced record solution<br />
* '''function: NewCreateTask()'''<br />
: AROS: missing.<br />
: Note: Most probably introduced when v0-on-trunk became v0.<br />
* '''function: AVL_FindNextNodeByKey()'''<br />
: AROS: Missing of 3th parameter 'comparefunction'<br />
: AROS sdk: struct AVLNode *AVL_FindNextNodeByKey(const struct AVLNode *node, AVLKey key, AVLKEYCOMP func) (A0, A1, A2)<br />
* '''function: AVL_FindPrevNodeByKey()'''<br />
: AROS: Missing of 3th parameter 'comparefunction'<br />
: AROS sdk: struct AVLNode *AVL_FindPrevNodeByKey(const struct AVLNode *root, AVLKey key, AVLKEYCOMP func) (A0, A1, A2)<br />
* '''function: AVL_FindNode()'''<br />
: AROS: 3th parameter Func is declared as type PAVLNODECOMP. Should be PAVLKEYCOMP.<br />
* '''function: AVL_RemNodeByKey()'''<br />
: AROS: 3th parameter Func is declared as type PAVLNODECOMP. Should be PAVLKEYCOMP.<br />
<br />
=== commodities ===<br />
<br />
=== expansion ===<br />
(see also other expansion support units)<br />
<br />
* '''syscall routine: ReadExpansionRom'''<br />
: Amiga: declared as procedure. autodocs are inconsistent (both procedure and function are being mentioned). AROS (also 68k) tells it is indeed a function and returning a bool.<br />
* '''syscall routine: ConfigBoard()'''<br />
: Amiga: declared as procedure. autodocs are inconsistent (both procedure and function are being mentioned). AROS (also 68k) tells it is indeed a function and returning a bool.<br />
<br />
=== gadtools ===<br />
<br />
* '''unit: gadtools'''<br />
: <strike>MorphOS: unit missing</strike> Available in fpc-triforce repo (for link see unit table). Waiting for testing/approval.<br />
* '''varargs function: CreateGadget()'''<br />
: AROS: missing<br />
* '''varargs function: CreateMenus()'''<br />
: AROS: missing<br />
* '''varargs function: DrawBevelBox()'''<br />
: AROS: missing<br />
* '''varargs function: GetVisualInfo()'''<br />
: AROS: missing<br />
* '''varargs function: GT_GetGadgetAttrs()'''<br />
: AROS: missing<br />
* '''varargs function: GT_SetGadgetAttrs()'''<br />
: AROS: missing<br />
* '''varargs function: LayoutMenuItems()'''<br />
: AROS: missing<br />
* '''varargs function: LayoutMenus()'''<br />
: AROS: missing<br />
* '''function: CreateContext()'''<br />
: Amiga SDK: struct Gadget *CreateContext(struct Gadget **);<br />
: AROS SDK: struct Gadget *CreateContext(struct Gadget **glistpointer) (A0)<br />
: Amiga: FUNCTION CreateContext(glistptr : pGadget location 'a0'): pGadget; syscall GadToolsBase 114;<br />
: AROS: function CreateContext(GListPtr: PGadget): PGadget; syscall GadToolsBase 19;<br />
: Note: glistpointer is a pointer to a pointer<br />
<br />
=== intuition ===<br />
<br />
=== keymap ===<br />
<br />
=== rexx ===<br />
<br />
=== workbench ===<br />
<br />
=== uncategorized ===<br />
<br />
<br />
== Fixed in triforce ==<br />
<br />
Additional units, added to triforce repo. Note that all issues listed above are already addressed with using unit trinity.<br />
<br />
* <strike>'''unit: akeyboard'''</strike> <!-- 20150928 --><br />
: AROS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/akeyboard.pas]<br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/akeyboard.pas]<br />
<br />
* <strike>'''unit: diskfont'''</strike> <!-- 20150929 --><br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/diskfont.pas]<br />
<br />
* <strike>'''unit: prefs'''</strike> <!-- 20151001 --><br />
: AROS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/prefs.pas]<br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/prefs.pas]<br />
<br />
* <strike>'''unit: AmigaPrinter'''</strike> <!-- 20151002 --><br />
: AROS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/amigaprinter.pas]<br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/amigaprinter.pas]<br />
<br />
* <strike>'''unit: Datatypes'''</strike> <!-- 20151003 --><br />
: AROS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/datatypes.pas]<br />
<br />
* <strike>'''unit: input'''</strike> <!-- 20151005 --><br />
: AROS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/input.pas]<br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/input.pas]<br />
<br />
* <strike>'''unit: workbench'''</strike> <!-- 20151026 --><br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/workbench.pas]<br />
<br />
* <strike>'''unit: icon'''</strike> <!-- 20151027 --><br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/icon.pas]<br />
<br />
* <strike>'''unit: locale'''</strike> <!-- 20151103 --><br />
: AROS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/locale.pas]<br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/locale.pas]<br />
<br />
* <strike>'''unit: gadtools'''</strike> <!-- 20151119 --><br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/gadtools.pas]<br />
<br />
* <strike>'''unit: commodities'''</strike> <!-- 20151223 --><br />
: AROS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/commodities.pas]<br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/commodities.pas]<br />
<br />
* <strike>'''unit: configregs'''</strike> <!-- 20160106 --><br />
: AROS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/configregs.pas]<br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/configregs.pas]<br />
<br />
* <strike>'''unit: configvars'''</strike> <!-- 20160107 --><br />
: AROS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/configvars.pas]<br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/configvars.pas]<br />
<br />
* <strike>'''unit: configregs'''</strike> <!-- 20160107 --><br />
: AROS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/configvars.pas]<br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/configvars.pas]<br />
<br />
* <strike>'''unit: expansionbase'''</strike> <!-- 20160108 --><br />
: AROS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/expansionbase.pas]<br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/expansionbase.pas]<br />
<br />
* <strike>'''unit: expansion'''</strike> <!-- 20160109 --><br />
: AROS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/expansion.pas]<br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/expansion.pas]<br />
<br />
* <strike>'''unit: rexx'''</strike> <!-- 20160116 --><br />
: AROS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/rexx.pas]<br />
: MorphOS: missing | fixed in triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/rexx.pas]<br />
<br />
* <strike>'''unit: aroslib'''</strike> <!-- 20160117 --><br />
: AROS: added to triforce[https://github.com/magorium/fpc-triforce/blob/master/Sys/AROS/rexx.pas]<br />
<br />
== Fixed in current trunk ==<br />
<br />
* <strike>'''unit: systemvartags'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33222]<br />
: This utility unit is Amiga specific and implements most if not all vartags versions of library-calls rendering it incompatible with AROS and MorphOS<br />
<br />
* <strike>'''function: AddAppIconA()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: Autodocs: struct AppIcon *AddAppIconA(ULONG, ULONG, char *, struct MsgPort *, BPTR, struct DiskObject *, struct TagItem *);<br />
: Amiga: FUNCTION AddAppIconA(id : ULONG location 'd0'; userdata : ULONG location 'd1'; text_ : pCHAR location 'a0'; msgport : pMsgPort location 'a1'; lock : pFileLock location 'a2'; diskobj : pDiskObject location 'a3'; const taglist : pTagItem location 'a4') : pAppIcon; syscall WorkbenchBase 060;<br />
: Note: lock parameter is of type BPTR not pFileLock<br />
* <strike>'''varargs function: AddAppIcon()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
:Amiga: Missing<br />
<br />
* <strike>'''function: DeleteArgstring()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: Amiga SDK: VOID DeleteArgstring(UBYTE* argstring:A0)<br />
: Amiga: procedure DeleteArgstring(argstring: PChar location 'd0'); syscall RexxSysBase 132;<br />
: Note: d0 as location ?<br />
<br />
* <strike>'''function: MapANSI()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: Amiga SDK: LONG MapANSI( STRPTR, LONG, STRPTR, LONG, struct KeyMap * );<br />
: MorphOS: function MapANSI(CONST strg : pSHORTINT location 'a0'; count : longint location 'd0'; buffer : pSHORTINT location 'a1'; length : longint location 'd1'; CONST keyMap : pKeyMap location 'a2') : longint; SysCall KeymapBase 048;<br />
: Note: PShortInt vs STRPTR for buffer/strg argument<br />
* <strike>'''function: MapRawKey()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: Amiga SDK: WORD MapRawKey( struct InputEvent *, STRPTR, WORD, struct Keymap * );<br />
: MorphOS: function MapRawKey(CONST event : pInputEvent location 'a0'; buffer : pSHORTINT location 'a1'; length : longint location 'd1'; CONST keyMap : pKeyMap location 'a2') : INTEGER; SysCall KeymapBase 042;<br />
: Note: PShortInt vs STRPTR for buffer argument<br />
<br />
* <strike>'''function: SetPointer()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: Amiga SDK: SetPointer( Window, Pointer, Height, Width, XOffset, YOffset )<br />
: AROS SDK: void SetPointer(struct Window * window, const UWORD* pointer, LONG height, LONG width, LONG xOffset, LONG yOffset );<br />
: MorphOS SDK: VOID SetPointer( struct Window *window, UWORD *pointer, LONG height, LONG width, LONG xOffset, LONG yOffset );<br />
: Amiga: PROCEDURE SetPointer(window : pWindow location 'a0'; pointer_ : pword location 'a1'; height : LONGINT location 'd0'; width : LONGINT location 'd1'; xOffset : LONGINT location 'd2'; yOffset : LONGINT location 'd3'); syscall _IntuitionBase 270;<br />
: AROS: procedure SetPointer(Window: PWindow; Pointer_: PWord; Height: LongInt; Width: LongInt; XOffset: LongInt; YOffset: LongInt); syscall IntuitionBase 45;<br />
: MorphOS: procedure SetPointer(window : pWindow location 'a0'; VAR pointer : Word location 'a1'; height : LongInt location 'd0'; width : LongInt location 'd1'; xOffset : LongInt location 'd2'; yOffset : LongInt location 'd3'); SysCall IntuitionBase 270;<br />
: Note: Using a var for pointerdata seems a bad idea. If must, then please provide both options.<br />
<br />
* <strike>'''structure: TExpansionControl()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: Amiga: field ec_Reserved11 is currently advertised with name ec_Z3_HighBase<br />
<br />
* <strike>'''type: Tag'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: Autodocs: "typedef ULONG Tag;"<br />
: Amiga: "Type Tag = LongInt;"<br />
* <strike>'''record field: ti_Data of record tTagItem'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: Autodocs: "ULONG ti_Data;"<br />
: Amiga: "ti_Data : LongInt;"<br />
<br />
* <strike>'''function: ASLRequestTags()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: <strike>AROS implementation seems missing.</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31663]<br />
: Amiga implements it in utility unit systemvartags (see also unit: systemvartags)<br />
: MorphOS implements it in unit ASL<br />
* <strike>'''function: AslRequest()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: autodocs: BOOL AslRequest( APTR,struct TagItem * );<br />
: Amiga: FUNCTION AslRequest(requester : POINTER location 'a0'; tagList : pTagItem location 'a1') : LongInt; syscall AslBase 060;<br />
: AROS: function AslRequest(Requester: Pointer; const Tags: array of const): LongBool;<br />
: MorphOS: function AslRequest(requester: Pointer location 'a0'; tagList : pTagItem location 'a1'): LongBool; SysCall AslBase 060;<br />
* <strike>'''function: RequestFile()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: Autodocs: BOOL RequestFile(struct FileRequester *);<br />
: Amiga: FUNCTION RequestFile(fileReq : pFileRequester location 'a0') : LongInt; syscall AslBase 042;<br />
: Remark: Here the boolean return type is allowed (as is used on the other platforms)<br />
* <strike>'''function: AslRequest()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: Autodocs: BOOL AslRequest(APTR,struct TagItem *);<br />
: Amiga: FUNCTION AslRequest(requester: POINTER location 'a0'; tagList: pTagItem location 'a1'): LongInt; syscall AslBase 060;<br />
: Remark: Here the boolean return type is allowed (as is used on the other platforms)<br />
<br />
<br />
* <strike>'''function: BltClear()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: Amiga: PROCEDURE BltClear(memBlock : pCHAR location 'a1'; byteCount : ULONG location 'd0'; flags : ULONG location 'd1'); syscall GfxBase 300;<br />
: AROS: procedure BltClear(MemBlock: Pointer; ByteCount: LongWord; Flags: LongWord); syscall GfxBase 50; deprecated;<br />
: MorphOS: procedure BltClear(memBlock : pCHAR location 'a1'; byteCount : CARDINAL location 'd0'; flags : CARDINAL location 'd1'); SysCall GfxBase 300;<br />
: Note: Parameter MemBlock should really be a generic pointer.<br />
* <strike>'''function: VideoControl()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: Amiga: FUNCTION VideoControl(colorMap : pColorMap location 'a0'; tagarray : pTagItem location 'a1') : LongBool; syscall GfxBase 708;<br />
: AROS: function VideoControl(Cm: PColorMap; Tags: PTagItem): LongWord; syscall GfxBase 118; unimplemented;<br />
: MorphOS: function VideoControl(colorMap : pColorMap location 'a0'; tagarray : pTagItem location 'a1') : LongBool; SysCall GfxBase 708;<br />
: Note: suggest to use LongBool as return-type.<br />
* <strike>'''function: LoadRGB4()'''</strike> [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33225]<br />
: Amiga: PROCEDURE LoadRGB4(vp : pViewPort location 'a0';const colors : pWord location 'a1'; count : LONGINT location 'd0'); syscall GfxBase 192;<br />
: AROS: procedure LoadRGB4(Vp: PViewPort; Colors: PWord; Count: LongInt); syscall GfxBase 32;<br />
: MorphOS: procedure LoadRGB4(vp : pViewPort location 'a0'; VAR colors : Integer location 'a1'; count : LongInt location 'd0'); SysCall GfxBase 192;<br />
: Note: This time MorphOS is the odd one out using var for colour parameter.<br />
<br />
<br />
* <strike>'''function: GetAttr()'''</strike> fixed in trunk [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31606]<br />
: MorphOS uses a var for parameter Return-Value while Amiga + AROS uses a pointer. Autodocs states it to be a pointer.<br />
* <strike>'''function: AllocMem()''' '''(high priority)'''</strike> Fixed in trunk [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31600]<br />
: MorphOS implemented it as ExecAllocMem<br />
: Amiga + AROS version have this function declared as AllocMem(), which is ambiguous with Free Pascal's AllocMem function.<br />
* <strike>'''function: Info()'''</strike> fixed in trunk [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31606]<br />
: AutoDocs: BOOL = Info( BPTR, struct InfoData * )<br />
: Amiga declaration: FUNCTION Info(lock : LONGINT location 'd1'; parameterBlock : pInfoData location 'd2') : LongBool; syscall _DOSBase 114;<br />
: AROS declaration: function Info(Lock: BPTR; ParameterBlock: PInfoData): LongInt; syscall AOS_DOSBase 19;<br />
: MorphOS declaration: function Info(lock : LongInt location 'd1'; parameterBlock: PInfoData location 'd2'): LongInt; SysCall MOS_DOSBase 114;<br />
* <strike>'''macros: All MUI macros'''</strike> fixed in trunk [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31607]<br />
: Amiga: The OBJ_xxx() macros are not implemented as macro's at all, rather as a cast to a particular structure in order to obtain information -> that is totally completely wickedly wrong.<br />
: MorphOS: See Amiga.<br />
* <strike>'''Constants: MUIX_R, MUIX_C, MUIX_L, MUIX_N, MUIX_B, MUIX_I, MUIX_U, MUIX_PT and MUIX_PH'''</strike> fixed in trunk [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31607]<br />
: AMIGA + AROS: these MUI constants uses c-language escape code characters, which won't work for Free Pascal.<br />
: MorphOS: declared them as they should.<br />
* <strike>'''function: NextTagItem()'''</strike> fixed in trunk [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31606]<br />
: autodocs: struct TagItem *NextTagItem(struct TagItem **);<br />
: Amiga: function NextTagItem(Item : ppTagItem location 'a0') : pTagItem; syscall _UtilityBase 048;<br />
: AROS: function NextTagItem(var Item: PTagItem): PTagItem; syscall AOS_UtilityBase 8;<br />
: MorphOS: function NextTagItem(tagListPtr: pPTagItem location 'a0'): PTagItem; SysCall MOS_UtilityBase 048;<br />
* <strike>'''function: ReadPixelArray8()'''</strike> fixed in [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31852]<br />
: autodocs: LONG ReadPixelArray8(struct RastPort *, UWORD, UWORD, UWORD, UWORD, UBYTE *, struct RastPort *);<br />
: Amiga: FUNCTION ReadPixelArray8(rp: pRastPort location 'a0'; xstart: ULONG location 'd0'; ystart: ULONG location 'd1'; xstop: ULONG location 'd2'; ystop: ULONG location 'd3'; array_: pointer location 'a2'; temprp: pRastPort location 'a1'): LONGINT; syscall GfxBase 780;<br />
: AROS: function ReadPixelArray8(Rp: PRastPort; xStart, yStart, xStop, yStop: LongWord; Array_: PByte; TempRp: PRastPort): LongInt; syscall GfxBase 130;<br />
: MorphOS: function ReadPixelArray8(rp: pRastPort location 'a0'; xstart: CARDINAL location 'd0'; ystart: CARDINAL location 'd1'; xstop: CARDINAL location 'd2'; ystop: CARDINAL location 'd3'; array1: pCHAR location 'a2'; temprp: pRastPort location 'a1'): LongInt; SysCall GfxBase 780;<br />
: Remark: PChar for pointing to Array data ?<br />
* <strike>'''function: WritePixelArray8()'''</strike><br />
: See: ReadPixelArray8()<br />
* <strike>'''function: PolyDraw()'''</strike> fixed in trunk [http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31852]<br />
: Autodocs: void PolyDraw( struct RastPort *, WORD, WORD * );<br />
: Amiga: PROCEDURE PolyDraw(rp : pRastPort location 'a1'; count : LONGINT location 'd0';const polyTable : pLongint location 'a0'); syscall GfxBase 336;<br />
: AROS: procedure PolyDraw(Rp: PRastPort; Count: LongInt; PolyTable: PSmallInt); syscall GfxBase 56;<br />
: MorphOS: procedure PolyDraw(rp : pRastPort location 'a1'; count : LongInt location 'd0'; VAR polyTable : INTEGER location 'a0'); SysCall GfxBase 336;<br />
: Remark: MorphOS' use of var for argument polyTable is imho just plain weird and also dictates the array to consist out of integers. Amiga version dictates using LongInt for the PolyTable array.<br />
* <strike>'''type: PPObject_'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31854]<br />
: Missing for Amiga and MorphOS<br />
* <strike>'''function: TextLength()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31854]<br />
: MorphOS: types the string parameter as pShortInt. Autodocs/Amiga/AROS uses type STRPTR.<br />
* <strike>'''function: Text()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31854]<br />
: MorphOS: still called Text() while Amiga and AROS named it GfxText(). Also the string parameter for MorphOS is declared as pShortInt. Autodocs/Amiga/AROS uses type STRPTR.<br />
* <strike>'''const: ACTION_READ'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31854]<br />
: MorphOS: Defines this constant as 'R', which is incompatible with TDOSPacket.dp_Type (LONG)<br />
: Remark: Amiga + AROS defines this constant as ACTION_READ = $52; // 'R' <br />
* <strike>'''const: ACTION_WRITE'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31854]<br />
: MorphOS: Defines this constant as 'W', which is incompatible with TDOSPacket.dp_Type (LONG)<br />
: Remark: Amiga + AROS defines this constant as ACTION_WRITE = $57; // 'W' <br />
* <strike>'''Function: ReadArgs()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31854]<br />
: autodocs: struct RDArgs * ReadArgs(STRPTR, LONG *, struct RDArgs *)<br />
: Amiga: FUNCTION ReadArgs(const arg_template : pCHAR location 'd1'; arra : pLONGINT location 'd2'; args : pRDArgs location 'd3') : pRDArgs; syscall _DOSBase 798;<br />
: AROS: function ReadArgs(const Template: STRPTR; var Array_: IPTR; RdArgs: PRDArgs): PRDArgs; syscall AOS_DOSBase 133;<br />
: MorphOS: function ReadArgs(arg_template: PChar location 'd1'; var array1: LongInt location 'd2'; args: PRDArgs location 'd3'): PRDArgs; SysCall MOS_DOSBase 798;<br />
: Remark: using var for Array_ parameter is ok, but restricts when attempting to pass f.i. a record structure. Why not declare both variants in such cases ?<br />
* <strike>'''macro: RASSIZE()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31854]<br />
: Amiga: missing<br />
: MorphOS: missing<br />
* <strike>'''Const: MIDDLEUP'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31854]<br />
: Amiga: Missing<br />
: MorphOS: Missing<br />
* <strike>'''Const: MIDDLEDOWN'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31854]<br />
: Amiga: Missing<br />
: MorphOS: Missing<br />
* <strike>'''unit: diskfont'''</strike> fixed in triforce[https://github.com/magorium/fpc-triforce/commit/95dc1c869dac53da5a70f908a2f63c06d0853bd4], fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=31854]<br />
: MorphOS: missing<br />
* <strike>'''unit: CyberGraphics'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32343]<br />
: MorphOS: Missing<br />
<br />
* <strike>'''function: ObtainBestPen()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32642]<br />
: MorphOS: Missing<br />
* <strike>''' macro: DrawCircle'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32642]<br />
: Amiga: missing<br />
: MorphOS: missing<br />
* <strike>'''varargs function: BestModeID()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32642]<br />
: MorphOS: missing<br />
<br />
* <strike>'''function: AllocDosObjectTags()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32642]<br />
: MorphOS: Function missing.<br />
* <strike>'''function FPuts()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32642]<br />
: Autodocs: LONG FPuts(BPTR, STRPTR)<br />
: Amiga1: FUNCTION FPuts(fh : LONGINT location 'd1';const str : pCHAR location 'd2') : LongBool; syscall _DOSBase 342;<br />
: Amiga2: FUNCTION FPuts(fh : LONGINT;const str : string) : BOOLEAN;<br />
: AROS: function FPuts(File_: BPTR; const String_: STRPTR): LongInt; syscall AOS_DOSBase 57;<br />
: MorphOS: function FPuts(fh : LongInt location 'd1'; str: PChar location 'd2'): LongInt; SysCall MOS_DOSBase 342;<br />
: Remark: note the use of different return-types as well as not using BPTR for filehandle type.<br />
: Note: returns zero on success, -1 if an error occurs, so please forget using a boolean return type.<br />
* <strike>'''function: VFPrintf()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32642]<br />
: AutoDocs: LONG = VFPrintf(BPTR, STRPTR, LONG *)<br />
: Amiga declaration: FUNCTION VFPrintf(fh : LONGINT location 'd1';const format : pCHAR location 'd2';const argarray : POINTER location 'd3') : LONGINT; syscall _DOSBase 354;<br />
: AROS declaration: function VFPrintf(Fh: BPTR; const format: STRPTR; const ArgArray: PLongInt): LongInt; syscall AOS_DOSBase 59;<br />
: MorphOS declaration: function VFPrintf(fh : LongInt location 'd1'; format: PChar location 'd2'; argarray: Pointer location 'd3'): LongInt; SysCall MOS_DOSBase 354;<br />
: NOTE: the generic pointer declaration prevents using "VFPrintf(nil/0, 'text', vargs );" where vargs = array of long.<br />
: Remark: AFAIK for AROS it is theoretically possible to pass 64-bit formatted values.<br />
<br />
* <strike>'''function: SetAttrs()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32642]<br />
: Amiga + MorphOS implementations seems missing<br />
* <strike>'''function: SetGadgetAttrs()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32642]<br />
: MorphOS version seems missing<br />
* <strike>'''function: EasyRequest()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32642]<br />
: Amiga: Missing<br />
: AROS: version with no array of const is missing -> forced to use [TAG_END, 0] <- extra 0 required for AROS due to small issue<br />
: MorphOS: Missing<br />
* <strike>'''field: dri_pens of structure tDrawInfo'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32642]<br />
: Autodocs: UWORD *dri_Pens; /* pointer to pen array */<br />
: Amiga: dri_Pens : Pointer; { pointer to pen array }<br />
: AROS: dri_Pens : PWord; // pointer to pen array<br />
: MorphOS: dri_Pens : Pointer; { pointer to pen array }<br />
: Remark: afaik the pen array is an array of word (for all platforms), so the only really practical type for dri_pens would then be a Pointer to an unsigned word<br />
* <strike>'''varargs function: SetWindowPointer()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32642]<br />
: Amiga: Seems missing<br />
: MorphOS: Seems missing<br />
* <strike>'''function: CloseScreen()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32642]<br />
: Amiga: Amiga declaration is still pre v36 (procedure), and does not return a boolean value (function) on success/failure which is the case on v36+ systems.<br />
<br />
* <strike>'''structure: TWindow field WScreen'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32643]<br />
: Autodocs: struct Screen *WScreen;<br />
: Amiga: WScreen : Pointer;<br />
: AROS: WScreen : PScreen; <br />
: MorphOS: WScreen : Pointer; <br />
: Status: Needs complete rewrite of intuition unit<br />
<br />
* <strike>'''type: TDateTime'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32644]<br />
: Amiga + AROS: declares this structure (and accompanied pointer) as TDateTime, <br />
: MorphOS: declares this structure _TDateTime and accompanied pointer _PDateTime<br />
: Remark: TDateTime declared in AmigaDOS conflicts with Free Pascal's declared TDateTime structure.<br />
* <strike>'''function: DateToStr()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32644]<br />
: All: This function conflicts with Free Pascal own DateToStr function. Renamed to DOSSateToStr (and StrToDate to DOSStrToDate)<br />
<br />
* <strike>'''record TmemChunk'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32682]<br />
: MorphOS: the field names right now are nc_Next and nc_Bytes -> should read '''m'''c_Next and '''m'''c_Bytes<br />
* <strike>'''vararg function: SystemTags()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32682]<br />
: MorphOS: missing.<br />
<br />
* <strike>'''record: TNewBroker'''</strike> (was already) fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32660]<br />
: Amiga: This structure seems aligned wrongly.<br />
: Note1: Packrecords c failed, packrecords 2 seems to work, but its influence on other record structure (InputXpression) was not tested (it uses two bytes as first entry in its structure).<br />
: Note2: Amiga and morphos sdk seems to use pragmapack #2, so also for the second structure.<br />
* <strike>'''function CxBroker()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32705]<br />
: AutoDocs: CxObj *CxBroker(struct NewBroker *,LONG *);<br />
: Amiga: 238 FUNCTION CxBroker(nb : pNewBroker location 'a0'; error : pCxObj location 'd0') : pCxObj; syscall CxBase 036;<br />
: Note: according to autodocs, error is a pointer to a generic LONG, not pCxObj;<br />
* <strike>'''function CreateCxObj()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32702]<br />
: AutoDocs: co = CreateCxObj(type,arg1,arg2); D0 = D0 A0 A1<br />
: Amiga: 237 FUNCTION CreateCxObj(typ : ULONG location 'd0'; arg1 : LONGINT location 'a1'; arg2 : LONGINT location 'a2') : pCxObj; syscall CxBase 030;<br />
: Note: notice different use of address registers. I have not faintest idea why they don't match. Maybe there's a valid reason ? (although aros and mos also uses same registers as stated by autodocs).<br />
<br />
* <strike>'''function: WriteStr()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32709]<br />
: On MorphOS this function seems declared as Amiga-function, which clashes with Free Pascal build-in function WriteStr. Strange as WriteStr seems only declared as dos/stdio.h macro.<br />
<br />
* <strike>'''function: ChangeSprite()'''</strike> fixed in trunk[http://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=33235]<br />
: Amiga: PROCEDURE ChangeSprite(vp : pViewPort location 'a0'; sprite : pSimpleSprite location 'a1'; newData : pWORD location 'a2'); syscall GfxBase 420;<br />
: AROS: procedure ChangeSprite(Vp: PViewPort; s: PSimpleSprite; NewData: Pointer); syscall GfxBase 70; unimplemented;<br />
: MorphOS: procedure ChangeSprite(vp : pViewPort location 'a0'; sprite : pSimpleSprite location 'a1'; VAR newData : Integer location 'a2'); SysCall GfxBase 420;<br />
: Note: MOS version uses var for sprite data parameter. Should be opaque pointer type (perhaps also for amiga ?)<br />
<br />
<br />
== Some of your finest ==<br />
<br />
* AslRequest()<br />
<source lang="pascal"><br />
{$IFDEF AMIGA}<br />
if (AslRequest(fr, nil) <> 0) then<br />
{$ENDIF}<br />
{$IFDEF AROS}<br />
if (AslRequestA(fr, nil)) then<br />
{$ENDIF}<br />
{$IFDEF MORPHOS}<br />
if (AslRequest(fr, nil)) then<br />
{$ENDIF}<br />
begin<br />
// Could we now please check what the requester returned ?<br />
end;<br />
</source><br />
<br />
<br />
== Hardening trinity ==<br />
<br />
In order to circumvent some of the inconsistencies and incompatibilities, there was need for a solution without tempering with the RTL and/or default support units.<br />
<br />
That's were unit trinity comes into play, which solves some of the encountered issues (the unit itself is a work in progress). It provides the user with a way to solve things and let sources compile without too much hassle/workarounds.<br />
<br />
The latest version of unit trinity is kindly provided by Magorium and can be found [https://github.com/magorium/fpc-triforce/tree/master/Base/Trinity here].</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Wiki_related&diff=836Wiki related2017-09-03T20:06:18Z<p>Molly: windowed highlighting</p>
<hr />
<div>internal links:<br />
<br />
* [http://fpcaroswiki.alb42.de/index.php?title=MediaWiki:Common.css Here] is the common stylesheet. Can be found on the search allpages in the WikiMedia namespace (somehow i always have trouble finding this page).<br />
<br />
mediawiki links:<br />
* [https://www.mediawiki.org/wiki/Help:Contents Main Help page]<br />
* [https://www.mediawiki.org/wiki/Help:Starting_a_new_page Starting a (fresh) new page]<br />
* [https://www.mediawiki.org/wiki/Help:Formatting Formatting tags]<br />
* [https://www.mediawiki.org/wiki/Help:Templates Templates]<br />
<br />
other external (3th party) links:<br />
* [http://community.wikia.com/wiki/Help:Template_parameters template box example]<br />
* [http://gettingtrickywithwikis.wikispaces.com/home getting tricky with wiki]<br />
* [http://truben.no/table/ online table editor (supports multiple formats, incl. wiki)]<br />
<br />
<br />
<br />
--- testing a notebox template example;<br />
{{Notebox<br />
|bgcolor = navy<br />
|textcolor = white<br />
|text = A colored box made, using a template<br />
}}<br />
<br />
--- Highlighting in a matchbox example<br />
<br />
<syntaxhighlight height="30%" lang="pascal" line start="100" highlight="1,5-7" style="width:30em;height:10em;overflow:scroll"><br />
program test;<br />
begin<br />
WriteLn(1);<br />
WriteLn(2);<br />
WriteLn(3);<br />
WriteLn(4);<br />
WriteLn(5);<br />
WriteLn(6);<br />
WriteLn(7);<br />
WriteLn(8);<br />
WriteLn(9);<br />
WriteLn('Pneumonoultramicroscopicsilicovolcanoconiosis');<br />
WriteLn(9);<br />
WriteLn(8);<br />
WriteLn(7);<br />
WriteLn(6);<br />
WriteLn(5);<br />
WriteLn(4);<br />
WriteLn(3);<br />
WriteLn(2);<br />
WriteLn(1);<br />
end.<br />
</syntaxhighlight></div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Main_Page&diff=835Main Page2017-08-20T20:37:32Z<p>Molly: /* Free Pascal on Amiga, AROS and MorphOS */ Add links to individual Free Pascal wiki-pages</p>
<hr />
<div>== Free Pascal on Amiga, AROS and MorphOS ==<br />
<br />
__NOTOC__<br />
<br />
This Page is meant as an documentation project for the Amiga systems implementation of Free Pascal and related topics (like Lazarus, LCL, fpgui).<br />
Freepascal is available for all Amiga systems (Amiga classic, AmigaOS4, AROS and MorphOS). This should not be a complete Free Pascal manual, but a compendium for the Amiga systems specialities of Free Pascal.<br />
<br />
{| id="mp-upper" style="width: 100%; margin:4px 0 0 0; background:none; border-spacing: 0px;"<br />
| class="MainPageBG" style="width:50%; border:1px solid #cef2e0; background:#f5fffa; vertical-align:top; color:#000;" |<br />
{| id="mp-left" style="width:100%; vertical-align:top; background:#f5fffa;"<br />
| style="padding:2px;" | <h2 id="mp-tfa-h2" style="margin:3px; background:#cef2e0; font-family:inherit; font-size:120%; font-weight:bold; border:1px solid #a3bfb1; text-align:left; color:#000; padding:0.2em 0.4em;"><br />
<br />
Introduction<br />
<br />
</h2><br />
|-<br />
| style="color:#000;" | <div id="mp-tfa" style="padding:2px 5px"><br />
<br />
* [[Introduction to Amiga|Introduction to Amiga systems]] - Short FAQ for those unfamiliar with Amiga Systems<br />
** [[Introduction to Amiga68k|Amiga 68k]]<br />
** [[Introduction to AmigaOS4|Amiga OS 4]]<br />
** [[Introduction to AROS|AROS]]<br />
** [[Introduction to MorphOS|MorphOS]]<br />
<br />
</div><br />
|-<br />
| style="padding:2px;" | <h2 id="mp-dyk-h2" style="margin:3px; background:#cef2e0; font-family:inherit; font-size:120%; font-weight:bold; border:1px solid #a3bfb1; text-align:left; color:#000; padding:0.2em 0.4em;"> <br />
<br />
Installation and Configuration<br />
<br />
</h2><br />
|-<br />
| style="color:#000; padding:2px 5px 5px;" | <div id="mp-dyk"><br />
<br />
How to install Free Pascal on Amiga systems<br />
* [[Installation Classic|Amiga 68k]]<br />
* [[Installation OS4|Amiga OS4]]<br />
* [[Installation|AROS]]<br />
* [[Installation MorphOS|MorphOS]]<br />
* [[Configuration]] - How to configure the compiler<br />
<br />
<br />
</div><br />
|}<br />
| style="border:1px solid transparent;" |<br />
| class="MainPageBG" style="width:50%; border:1px solid #cedff2; background:#f5faff; vertical-align:top;"|<br />
{| id="mp-right" style="width:100%; vertical-align:top; background:#f5faff;"<br />
| style="padding:2px;" | <h2 id="mp-itn-h2" style="margin:3px; background:#cedff2; font-family:inherit; font-size:120%; font-weight:bold; border:1px solid #a3b0bf; text-align:left; color:#000; padding:0.2em 0.4em;"><br />
<br />
Infos and Status<br />
<br />
</h2><br />
|-<br />
| style="color:#000; padding:2px 5px;" | <div id="mp-itn"><br />
<br />
* [[Specifics]] - Amiga Systems Specifics in Free Pascal.<br />
* [[Status]] - Status and known Bugs of Implementation and how to avoid<br />
** [[AROS Libraries]] - Overview of included AROS library<br />
** [[FPC trunk status| FPC RTL/Packages]] - Status of the FreePascal RTL and Packages<br />
** [[Library units]] - Status of the FreePascal Library units for Amiga systems<br />
** [[LCL status]] - Status of the LCL MUI/Zune implementation<br />
<br />
</div><br />
|-<br />
| style="padding:2px;" | <h2 id="mp-otd-h2" style="margin:3px; background:#cedff2; font-family:inherit; font-size:120%; font-weight:bold; border:1px solid #a3b0bf; text-align:left; color:#000; padding:0.2em 0.4em;"><br />
<br />
Coding with FreePascal for Amiga systems<br />
<br />
</h2><br />
|-<br />
| style="color:#000; padding:2px 5px 5px;" | <div id="mp-otd"><br />
<br />
* [[Source Examples]] - examples to use on Amiga systems<br />
* [[AROS Programs]] - programs use Free Pascal on AROS or tested to work on.<br />
* [[AROS compatible projects]] - Projects written in Free Pascal that are compatible with AROS<br />
<br />
</div><br />
|}<br />
|}<br />
<br />
{| id="mp-lower" style="margin:4px 0 0 0; width:100%; background:none; border-spacing: 0px;"<br />
| class="MainPageBG" style="width:100%; border:1px solid #ddcef2; background:#faf5ff; vertical-align:top; color:#000;" |<br />
{| id="mp-bottom" style="width:100%; vertical-align:top; background:#faf5ff; color:#000;"<br />
| style="padding:2px;" | <h2 id="mp-tfp-h2" style="margin:3px; background:#ddcef2; font-family:inherit; font-size:120%; font-weight:bold; border:1px solid #afa3bf; text-align:left; color:#000; padding:0.2em 0.4em"><br />
<br />
Links<br />
<br />
</h2><br />
|-<br />
| style="color:#000; padding:2px;" | <div id="mp-tfp"> <br />
* [//www.aros.org AROS Research Operating System]<br />
* [//www.aros-exec.org AROS discussion forum]<br />
* [//www.arosworld.org AROSWorld discussion forum]<br />
* [//www.amigacoding.de Amiga Development] discussion forum (AROS, Amiga and MorphOS)<br />
<br />
<br />
* [//www.freepascal.org Free Pascal compiler], [//www.freepascal.org/docs-html/3.0.0/ Free Pascal manuals], [//wiki.freepascal.org/Category:Tutorials Free Pascal tutorials]<br />
* Free Pascal wiki-pages: [//wiki.freepascal.org/Amiga Amiga], [//wiki.freepascal.org/AmigaOS AmigaOS], [//wiki.freepascal.org/AROS AROS], [//wiki.freepascal.org/MorphOS MorphOS] and [//wiki.freepascal.org/Category:AmigaOS category AmigaOS].<br />
* [//www.alb42.de/fpc-docu/ AROS RTL reference manual]<br />
* [//fpgui.sourceforge.net fpGUI Toolkit], [//fpgui.sourceforge.net/apidocs/index.html fpGUI reference], [//github.com/graemeg/fpgui/ fpGUI GIT]<br />
* [//michalis.ii.uni.wroc.pl/~michalis/modern_pascal_introduction/modern_pascal_introduction.html Introduction to modern Pascal]<br />
<br />
<br />
* [//blog.alb42.de Blog of the porter] with download of binary package and source<br />
<br />
<br />
* [//www.amigacoding.de/index.php?board=239.0 Dedicated Free Pascal sub-forum] for all your Free Pascal related questions whether generic Pascal question(s) and/or questions about Free Pascal with regards to Amiga(OS), AROS and/or MorphOS <br />
</div><br />
|}<br />
|}</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Main_Page&diff=834Main Page2017-08-20T20:10:52Z<p>Molly: /* Free Pascal on Amiga, AROS and MorphOS */ Add link to AROS reference manual</p>
<hr />
<div>== Free Pascal on Amiga, AROS and MorphOS ==<br />
<br />
__NOTOC__<br />
<br />
This Page is meant as an documentation project for the Amiga systems implementation of Free Pascal and related topics (like Lazarus, LCL, fpgui).<br />
Freepascal is available for all Amiga systems (Amiga classic, AmigaOS4, AROS and MorphOS). This should not be a complete Free Pascal manual, but a compendium for the Amiga systems specialities of Free Pascal.<br />
<br />
{| id="mp-upper" style="width: 100%; margin:4px 0 0 0; background:none; border-spacing: 0px;"<br />
| class="MainPageBG" style="width:50%; border:1px solid #cef2e0; background:#f5fffa; vertical-align:top; color:#000;" |<br />
{| id="mp-left" style="width:100%; vertical-align:top; background:#f5fffa;"<br />
| style="padding:2px;" | <h2 id="mp-tfa-h2" style="margin:3px; background:#cef2e0; font-family:inherit; font-size:120%; font-weight:bold; border:1px solid #a3bfb1; text-align:left; color:#000; padding:0.2em 0.4em;"><br />
<br />
Introduction<br />
<br />
</h2><br />
|-<br />
| style="color:#000;" | <div id="mp-tfa" style="padding:2px 5px"><br />
<br />
* [[Introduction to Amiga|Introduction to Amiga systems]] - Short FAQ for those unfamiliar with Amiga Systems<br />
** [[Introduction to Amiga68k|Amiga 68k]]<br />
** [[Introduction to AmigaOS4|Amiga OS 4]]<br />
** [[Introduction to AROS|AROS]]<br />
** [[Introduction to MorphOS|MorphOS]]<br />
<br />
</div><br />
|-<br />
| style="padding:2px;" | <h2 id="mp-dyk-h2" style="margin:3px; background:#cef2e0; font-family:inherit; font-size:120%; font-weight:bold; border:1px solid #a3bfb1; text-align:left; color:#000; padding:0.2em 0.4em;"> <br />
<br />
Installation and Configuration<br />
<br />
</h2><br />
|-<br />
| style="color:#000; padding:2px 5px 5px;" | <div id="mp-dyk"><br />
<br />
How to install Free Pascal on Amiga systems<br />
* [[Installation Classic|Amiga 68k]]<br />
* [[Installation OS4|Amiga OS4]]<br />
* [[Installation|AROS]]<br />
* [[Installation MorphOS|MorphOS]]<br />
* [[Configuration]] - How to configure the compiler<br />
<br />
<br />
</div><br />
|}<br />
| style="border:1px solid transparent;" |<br />
| class="MainPageBG" style="width:50%; border:1px solid #cedff2; background:#f5faff; vertical-align:top;"|<br />
{| id="mp-right" style="width:100%; vertical-align:top; background:#f5faff;"<br />
| style="padding:2px;" | <h2 id="mp-itn-h2" style="margin:3px; background:#cedff2; font-family:inherit; font-size:120%; font-weight:bold; border:1px solid #a3b0bf; text-align:left; color:#000; padding:0.2em 0.4em;"><br />
<br />
Infos and Status<br />
<br />
</h2><br />
|-<br />
| style="color:#000; padding:2px 5px;" | <div id="mp-itn"><br />
<br />
* [[Specifics]] - Amiga Systems Specifics in Free Pascal.<br />
* [[Status]] - Status and known Bugs of Implementation and how to avoid<br />
** [[AROS Libraries]] - Overview of included AROS library<br />
** [[FPC trunk status| FPC RTL/Packages]] - Status of the FreePascal RTL and Packages<br />
** [[Library units]] - Status of the FreePascal Library units for Amiga systems<br />
** [[LCL status]] - Status of the LCL MUI/Zune implementation<br />
<br />
</div><br />
|-<br />
| style="padding:2px;" | <h2 id="mp-otd-h2" style="margin:3px; background:#cedff2; font-family:inherit; font-size:120%; font-weight:bold; border:1px solid #a3b0bf; text-align:left; color:#000; padding:0.2em 0.4em;"><br />
<br />
Coding with FreePascal for Amiga systems<br />
<br />
</h2><br />
|-<br />
| style="color:#000; padding:2px 5px 5px;" | <div id="mp-otd"><br />
<br />
* [[Source Examples]] - examples to use on Amiga systems<br />
* [[AROS Programs]] - programs use Free Pascal on AROS or tested to work on.<br />
* [[AROS compatible projects]] - Projects written in Free Pascal that are compatible with AROS<br />
<br />
</div><br />
|}<br />
|}<br />
<br />
{| id="mp-lower" style="margin:4px 0 0 0; width:100%; background:none; border-spacing: 0px;"<br />
| class="MainPageBG" style="width:100%; border:1px solid #ddcef2; background:#faf5ff; vertical-align:top; color:#000;" |<br />
{| id="mp-bottom" style="width:100%; vertical-align:top; background:#faf5ff; color:#000;"<br />
| style="padding:2px;" | <h2 id="mp-tfp-h2" style="margin:3px; background:#ddcef2; font-family:inherit; font-size:120%; font-weight:bold; border:1px solid #afa3bf; text-align:left; color:#000; padding:0.2em 0.4em"><br />
<br />
Links<br />
<br />
</h2><br />
|-<br />
| style="color:#000; padding:2px;" | <div id="mp-tfp"> <br />
* [//www.aros.org AROS Research Operating System]<br />
* [//www.aros-exec.org AROS discussion forum]<br />
* [//www.arosworld.org AROSWorld discussion forum]<br />
* [//www.amigacoding.de Amiga Development] discussion forum (AROS, Amiga and MorphOS)<br />
<br />
<br />
* [//www.freepascal.org Free Pascal compiler], [//www.freepascal.org/docs-html/3.0.0/ Free Pascal manuals], [//wiki.freepascal.org/Category:Tutorials Free Pascal tutorials]<br />
* [//www.alb42.de/fpc-docu/ AROS RTL reference manual]<br />
* [//fpgui.sourceforge.net fpGUI Toolkit], [//fpgui.sourceforge.net/apidocs/index.html fpGUI reference], [//github.com/graemeg/fpgui/ fpGUI GIT]<br />
* [//michalis.ii.uni.wroc.pl/~michalis/modern_pascal_introduction/modern_pascal_introduction.html Introduction to modern Pascal]<br />
<br />
<br />
* [//blog.alb42.de Blog of the porter] with download of binary package and source<br />
<br />
<br />
* [//www.amigacoding.de/index.php?board=239.0 Dedicated Free Pascal sub-forum] for all your Free Pascal related questions whether generic Pascal question(s) and/or questions about Free Pascal with regards to Amiga(OS), AROS and/or MorphOS <br />
</div><br />
|}<br />
|}</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Specifics&diff=833Specifics2017-08-19T19:26:36Z<p>Molly: /* Changed constants/function Names */ Someimes mistakes are made. this was on of them. removed rntry DosGetLocalizedString</p>
<hr />
<div>== Specific implementations/changes for Free Pascal on Amiga systems==<br />
<br />
=== in Relation to other Free Pascal implementations ===<br />
----<br />
==== Compiler defines ====<br />
There are several [//www.freepascal.org/docs-html/prog/progap7.html#x329-344000G compiler defines (table G.4)] available for Free Pascal. More on multiplatform programming can be found [//wiki.freepascal.org/Multiplatform_Programming_Guide here].<br />
{| class="wikitable"<br />
|-<br />
!Definition HASAMIGA<br />
|With this definition you can can determine if you are on any of the Amiga platforms (also future proof for future Amiga platforms). Use it to encapsulate code parts (or uses) for all Amiga-style systems {$ifdef HASAMIGA} {$endif} or disable parts which does not work on all Amiga-style systems via {$ifndef HASAMIGA} {$endif}<br />
|-<br />
!Definition AMIGA<br />
|This definition exists in FreePascal for Amiga m68k and Amiga PowerPC (OS4) only. Do separate Amiga m68k and OS4 you can use the processor defines '''M68K''' and '''POWERPC''' or the specialized defines '''AMIGA68k''' and '''AMIGAOS4'''.<br />
|-<br />
!Definition MORPHOS<br />
|This definition exists in FreePascal for MorphOS.<br />
|-<br />
!Definition AROS<br />
| This definition exists in FreePascal for AROS. This enables you to determine if your program is compiled/targeted for AROS or not. You can use {$IFDEF AROS} or {$IFNDEF AROS} to make use of this define. ABI define '''AROS_ABIv0''' and '''AROS_ABIv1'''<br />
NOTE: There is no dedicated AROS m68k Version, the released Version is the Amiga m68k version because AROS m68k is binary compatible to Amiga classic.<br />
|}<br />
<br />
==== Additional System unit contents ====<br />
'''System''' unit has some additional functions and variables:<br />
<br />
Be warned if you change any of the variables then your program will crash and most likely your computer as well.<br />
<br />
{| class="wikitable"<br />
|-<br />
!SysDebug(Msg: ShortString); SysDebugLn(Msg: ShortString);<br />
|Write a message to debug output of the system. To read this debug messages you need a special tool to catch the messages (AROS: sashimi, hosted AROS: consoleoutput; MorphOS/AmigaOS4: LogTool) SysDebugLn() adds a return after the message, SysDebug() does not. '''NOTE''': The ''Msg'' parameter is a ShortString, so must not be longer than 255 chars.<br />
|-<br />
!AOS_WbMsg: Pointer;<br />
|Real Type is PWBStartup (from "Workbench" unit) can be used to determine if the program was started from WB (if this pointer is Nil then it was started from CLI)<br />
|-<br />
!AOS_ExecBase: Pointer;<br />
|Real type is: PExecBase (from "Exec" Unit) can be used to call all exec.library calls, or to inspect system basic features<br />
|-<br />
!AOS_DOSBase: Pointer;<br />
|Real type is: PDOSBase (from "AmigaDos" Unit) can be used to call all dos.library calls<br />
|-<br />
!AOS_UtilityBase: Pointer;<br />
|Real type is: PUtilityBase (from "Utility" Unit) can be used to call all utility.library calls<br />
|-<br />
!AOS_heapPool: Pointer;<br />
|Its a Pool Header created with (CreatePool from exec), all memory allocations are created in this pool, could be used to AllocPooled memory as well (even AllocMem, New and so on do it automatically)<br />
|}<br />
<br />
==== Threading ====<br />
{| class="wikitable"<br />
|-<br />
!athreads<br />
|This unit is needed if the program wants to use threads. It must be added as very first uses in the main source file. it is comparable to the ''cthreads'' units of freepascal for linux.<br />
|}<br />
<br />
==== Syscalls ====<br />
<br />
For all Amiga systems a special calling convention for calls into a Amiga-style (.library) with the keyword ''syscall''. A typical Library call looks like this:<br />
<source lang="pascal"><br />
procedure FktName(parameters); syscall Base Offset;<br />
</source><br />
{| class="wikitable"<br />
|-<br />
|''Parameter''<br />
|'''AROS(i386, x86_64, ARM), AmigaOS4''': Parameter of the function with type: '''param: type'''<br />
'''AmigaOS3, MorphOS''': Parameter of the function with type and location: '''param: type location 'register' ''' register is a standard m68k register 'd0'-'d7','a0'-'a6'<br />
|-<br />
|''Base''<br />
|'''AmigaOS3, AROS, MorphOS''': LibraryBase from OpenLibrary();<br />
'''AmigaOS4:''' Interface of the Library from GetInterface();<br />
|-<br />
|''Offset''<br />
|'''AmigaOS3, MorphOS, AmigaOS4''': Offset relative to the ''Base''<br />
'''AROS''': Offset relative to the ''Base'' divided by the size of a Pointer (therefore its the same value for 64 bit and 32bit)<br />
|}<br />
[[Call Library]] shows how to create syscalls for the different platforms and how to gather the needed informations<br />
<br />
Further informations about syscalls: [http://wiki.freepascal.org/Amiga#SysCalls AmigaOS3], [http://wiki.freepascal.org/AmigaOS#Library_interfaces_and_Syscalls AmigaOS4], [http://wiki.freepascal.org/MorphOS#SysCalls MorphOS]<br />
<br />
=== Relation to other languages on AROS/Amiga ===<br />
----<br />
Not really compiler related, but more about how ones program should behave, look and act on an amiganoid system.<br />
<br />
<br />
There is a document called "Amiga User Interface Style Guide" [http://amigaos.pl/amiga_user_interface_style_guide/index.html] that cover some of the basic techniques that should be practised when programming for amiganoid systems. Of course, it would be impossible to follow them all (if even for the GUI differences) but there are a couple of interesting topics in there:<br />
{| class="wikitable"<br />
|-<br />
!Embedded version IDs: [http://amigaos.pl/amiga_user_interface_style_guide/embedded_version_ids.html]<br />
|Gives the programmer the ability to let the system 'interrogate' your program (executable) and displays version information (from either commandline tool version or by using the icon information menu from the workbench).<br><br />
To put it simply, add a const string in generic version format:<br />
$VER: <name> <version>.<revision> (<d>.<m>.<y>) <br />
|}<br />
----<br />
==== Changed Unit Names ====<br />
{| class="wikitable"<br />
|+<br />
!Library name in AROS !! corresponding Free Pascal Unit !! Collision with<br />
|-<br />
|graphics.libray || agraphics || Graphics unit from LCL<br />
|-<br />
|dos.library || amigados || Dos unit from RTL<br />
|}<br />
<br />
Also note that the usage of unit amigalib is deprecated. The functions that resided in this unit can now be found in their respective counterpart units e.g. graphic related function can now be found inside unit AGraphics, Intuition related functions can now be found inside unit Intuition, etc.<br />
<br />
==== Changed constants/function Names ====<br />
{| class="wikitable"<br />
|+ <br />
!type !! Original Name !! Free Pascal Name !! Collision with<br />
|-<br />
| colspan=4 | '''Exec.Library (exec) '''<br />
|-<br />
|function || AllocMem || ExecAllocMem || AllocMem() in System<br />
|-<br />
|procedure || FreeMem || ExecFreeMem || FreeMem() in System<br />
|-<br />
|procedure || Insert || ExecInsert || Insert() in System<br />
|-<br />
|procedure || Exception || ExecException || Exception keyword<br />
|-<br />
| colspan=4 | '''dos.Library (amigados)'''<br />
|-<br />
|procedure || Close || DOSClose || Collides with Close() function in unit System<br />
|-<br />
| function || CreateDir || DOSCreateDir || Collides with CreateDir() function in unit SysUtils<br />
|-<br />
| function || DateToStr || DOSDateToStr || Collides with DateToStr() function in unit SysUtils<br />
|-<br />
|procedure || Delay || DOSDelay || Collides with Delay() function in unit CRT<br />
|-<br />
|procedure || DeleteFile || DOSDeleteFile || Collides with DeleteFile() function in unit SysUtils<br />
|-<br />
|procedure || Exit || DOSExit || Collides with Exit() function in unit System<br />
|-<br />
|procedure || Flush || DOSFlush || Collides with Flush() function in unit System<br />
|-<br />
| function || Format || DOSFormat || Collides with Format() function in unit SysUtils<br />
|-<br />
|variable || Input || DOSInput || Collides with Input() variable in unit System<br />
|-<br />
|procedure || Open || DOSOpen || For consistency with other dos functions<br />
|-<br />
|variable || Output || DOSOutput || Collides with Output() variable in unit System<br />
|-<br />
|procedure || Read || DOSRead || Collides with Read() function in unit System<br />
|-<br />
|procedure || Rename || DOSRename || Collides with Rename() function in unit System<br />
|-<br />
|procedure || Seek || DOSSeek || Collides with Seek() function in unit System<br />
|-<br />
| function || StrToDate || DOSStrToDate || Collides with StrToDate() function in unit SysUtils<br />
|-<br />
| function || System || DOSSystem || For consistency with other dos functions<br />
|-<br />
|procedure || Write || DOSWrite || Collides with Write() function in unit System<br />
|-<br />
| colspan=4 | '''Graphics.Library (agraphics)'''<br />
|-<br />
|procedure || Move || GfxMove || Move() in System<br />
|-<br />
|procedure || Text || GfxText || Text type<br />
|-<br />
| colspan=4 | '''Intuition.Library (intuition)'''<br />
|-<br />
|const || SINGLE || SINGLE_PT || Single type<br />
|-<br />
|const || FANFOLD || FANFOLD_PT || (renamed because of Single renaming)<br />
|-<br />
|const || WBENCHSCREEN || WBENCHSCREEN_f ||<br />
|-<br />
|const || PUBLICSCREEN || PUBLICSCREEN_f ||<br />
|-<br />
|const || CUSTOMSCREEN || CUSTOMSCREEN_f ||<br />
|-<br />
|const || SCREENTYPE || SCREENTYPE_f ||<br />
|-<br />
|const || SHOWTITLE || SHOTITLE_f ||<br />
|-<br />
|const || BEEPING || BEEPING_f ||<br />
|-<br />
|const || CUSTOMBITMAP || CUSTOMBITMAP_f ||<br />
|-<br />
|const || SCREENBEHIND || SCREENBEHIND_f ||<br />
|}</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=Library_units&diff=832Library units2017-08-19T19:04:13Z<p>Molly: add deprecation note for unit amigalib</p>
<hr />
<div><br />
{| class="wikitable"<br />
|+ List of available units per platform:<br />
! Unit !! Category !! OS3.x !! OS4.x !! AROS !! MorphOS !! Remark(s)<br />
|-<br />
| agraphics || graphics.library || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| ahi || ahi.device || class="working" | yes || class="unknown" | n/a || class="progress" | MAG || class="working" | yes || <br />
|-<br />
| ahi_sub || ahi_sub.library || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| akeyboard || keyboard.device || class="working" | yes || class="unknown" | n/a || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/akeyboard.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/akeyboard.pas test] || <br />
|-<br />
| amarquee || amarquee.library || class="working" | yes || class="unknown" | n/a || class="unknown" | n/a || class="unknown" | n/a || <br />
|-<br />
| amigados || dos.library || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| amigaguide || amigaguide.library || class="working" | yes || class="unknown" | n/a || class="progress" | MAG || class="not" | no || No use though, AROS' lib functions are not implemented<br />
|-<br />
| amigalib || amigalib || class="working" | yes || class="unknown" | n/a || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/amigalib.pas test] || class="working" | yes || Unit amigalib has it's own status page, [[AmigaLib]] (deprectaed fpc 3.1.1, revision 36777/36778<br />
|-<br />
| amigaprinter || printer.device || class="working" | yes || class="unknown" | n/a || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/amigaprinter.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/amigaprinter.pas test] || <br />
|-<br />
| aroslib || aros.library || class="unknown" | n/a || class="unknown" | n/a || class="working" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/aroslib.pas yes] || class="unknown" | n/a || <br />
|-<br />
| asl || asl.library || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| audio || audio.device || class="working" | yes || class="unknown" | n/a || class="progress" | MAG || class="not" | no || <br />
|-<br />
| bootblock || bootblock.device || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| bullet || bullet.library || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| cd || cd.device || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| clipboard || clipboard.device || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| colorwheel || colorwheel.gadget || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| commodities || commodities.library || class="working" | yes || class="unknown" | n/a || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/commodities.pas test] || <br />
|-<br />
| configregs || see expansion || class="working" | yes || class="unknown" | n/a || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/configregs.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/configregs.pas test] || <br />
|-<br />
| configvars || see expansion || class="working" | yes || class="unknown" | n/a || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/configvars.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/configvars.pas test] || <br />
|-<br />
| console || console.device || class="working" | yes || class="unknown" | n/a || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/console.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/console.pas test] || <br />
|-<br />
| conunit || console.device || class="working" | yes || class="unknown" | n/a || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/conunit.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/conunit.pas test] || <br />
|-<br />
| cgxvideo || || class="not" | no || class="unknown" | n/a || class="not" | no || class="working" | [//svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32343 yes] || Recently added to MorphOS by Chain-Q<br />
|-<br />
| cybergraphics || cybergraphics.library || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | [//svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=32343 yes] || Recently added to MorphOS by Chain-Q<br />
|-<br />
| datatypes || datatypes.library || class="working" | yes || class="unknown" | n/a || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/datatypes.pas test] || class="working" | yes || Unit source (AROS) is still a bit of a mess. Amiga version needs an overhaul (no PObject_ being used where it should -> concerns most if not all of declared functions).<br />
|-<br />
| diskfont || diskfont.library || class="working" | yes || class="working" | yes || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/diskfont.pas test] || A diskfont unit was added to MorphOS by Chain-Q<br />
|-<br />
| exec || exec.library || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || MorphOS: noticed some things missing in comparison to SDK 3.9 (this is meant as a reminder to verify this unit)<br />
|-<br />
| expansion || expansion.library || class="working" | yes || class="unknown" | n/a || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/expansion.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/expansion.pas test] || <br />
|-<br />
| expansionbase || see expansion || class="working" | yes || class="unknown" | n/a || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/expansionbase.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/expansionbase.pas test] || <br />
|-<br />
| gadtools || gadtools.library || class="working" | yes || class="unknown" | n/a || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/gadtools.pas test] || <br />
|-<br />
| gameport || gameport.device || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| get9 || ? || class="unknown" | n/a || class="unknown" | n/a || class="unknown" | n/a || class="working" | yes || silly MorphOS-only historic .library, Pascal interface unit exist as a joke, ignore this :)<br />
|-<br />
| gradientslider || gradientslider.gadget || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| gtlayout || gtlayout.library || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| guigfx || guigfx.library || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| hardblocks || hardblocks.device || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| hardware || hardware.resource || class="working" | yes || class="unknown" | n/a || class="working" | yes || class="working" | yes || <br />
|-<br />
| icon || icon.library || class="working" | yes || class="working" | yes || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/icon.pas test] || <br />
|-<br />
| identify || identify.lbrary || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| iffparse || iffparse.library || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| input || input.device || class="working" | yes || class="unknown" | n/a || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/input.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/input.pas test] || <br />
|-<br />
| inputevent || see input.device || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| intuition || intuition.library || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| keymap || keymap.library || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| kvm || ? || class="unknown" | n/a || class="unknown" | n/a || class="unknown" | n/a || class="unknown" | n/a || This unit is now dropped. It was a helper unit for the KVM stuff, but it's no longer used, and it doesn't provide any other useful functionality. It's "API" was never meant for public use either. The idea was, mouse unit could be used w/o the video and keyboard, and doesn't depend on each other. But it doesn't really matter any more. I removed it from trunk.<br />
|-<br />
| layers || layers.library || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| locale || locale.library || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| lowlevel || lowlevel.library || class="working" | yes || class="unknown" | n/a || class="progress" | MAG || class="not" | no || <br />
|-<br />
| lucyplay || lucyplay.library || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| mui || muimaster.library || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || Also for Zune.<br />
|-<br />
| mysticview || mysticview.library || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| nonvolatile || nonvolatile.library || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| parallel || parallel.device || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| picasso96api || picasso library || class="working" | yes || class="working" | yes || class="unknown" | n/a || class="unknown" | n/a || MorphOS and AROS don't have Picasso96 support.<br />
|-<br />
| preferences || preferences.library || class="working" | yes || class="unknown" | n/a || class="not" | n/a || class="not" | no || <br />
|-<br />
| prefs || see preferences || class="working" | yes || class="unknown" | n/a || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/prefs.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/prefs.pas test] || <br />
|-<br />
| prtbase || printer.device || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| prtgfx || ? || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| ptreplay || ptreplay.library || class="working" | yes || class="unknown" | n/a || class="progress" | MAG || class="not" | no || <br />
|-<br />
| realtime || realtime.library || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| render || render.library || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| reqtools || reqtools.library || class="working" | yes || class="unknown" | n/a || class="progress" | MAG || class="not" | no || <br />
|-<br />
| rexx || rexxsyslib.library || class="working" | yes || class="unknown" | n/a || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/AROS/rexx.pas test] || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/rexx.pas test] || <br />
|-<br />
| romboot_base || || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| scsidisk || scscidisk.device || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| serial || serial.device || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| tapedeck || tapedeck.gadget || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| timer || timer.device || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| tinygl || tinygl library || class="unknown" | n/a || class="unknown" | n/a || class="unknown" | n/a || class="working" | yes || TinyGL is MorphOS specific and the unit there is only used to get the OpenGL package of FPC running.<br />
|-<br />
| trackdisk || trackdisk.device || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| translator || translator.library || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| triton || triton.library || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| tritonmacros || macros for triton || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| ttengine || ttengine.library || class="working" | yes || class="unknown" | n/a || class="progress" | MAG || class="not" | no || <br />
|-<br />
| utility || utility.library || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || <br />
|-<br />
| workbench || workbench.library || class="working" | yes || class="working" | yes || class="working" | yes || class="progress" | [//github.com/magorium/fpc-triforce/blob/master/Sys/MorphOS/workbench.pas test] || <br />
|-<br />
| xadmaster || xadmaster.library || class="working" | yes || class="unknown" | n/a || class="progress" | MAG || class="not" | no || <br />
|-<br />
| zlib || zlib.library || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
! Unit !! Category !! OS3.x !! OS4.x !! AROS !! MorphOS !! Remark(s)<br />
|-<br />
| amigautils || amigautils || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || Should be moved to ami-extra when it's verified it works elsewhere and contains no deprecated code.<br />
|-<br />
| amsgbox || msgbox || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes ||<br />
|-<br />
| consoleio || crt using console || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| deadkeys || console deadkeys || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| doublebuffer || || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| easyasl || easyasl.library || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| hisoft || || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || Should be moved to ami-extra when it's verified it works elsewhere and contains no deprecated code.<br />
|-<br />
| linklist || || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || Should be moved to ami-extra when it's verified it works elsewhere and contains no deprecated code.<br />
|-<br />
| longarray || || class="working" | yes || class="unknown" | n/a || class="working" | yes || class="not" | no || <br />
|-<br />
| muihelper || see mui.library || class="working" | yes || class="working" | yes || class="working" | yes || class="working" | yes || MUIHelper contains some Pascal syntax-sugar and helpers for writing MUI code.<br />
|-<br />
| pastoc || || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || This is deprecated, and must *not* be ported to other platforms.<br />
|-<br />
| pcq || || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || Should be moved to ami-extra when it's verified it works elsewhere and contains no deprecated code.<br />
|-<br />
| tagsarray || || class="working" | yes || class="unknown" | n/a || class="working" | yes || class="not" | no || Tagsarray implementation is not thread safe, at least on classic.<br />
|-<br />
| timerutils || || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || Should be moved to ami-extra when it's verified it works elsewhere.<br />
|-<br />
| vartags || || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|-<br />
| wbargs || || class="working" | yes || class="unknown" | n/a || class="not" | no || class="not" | no || <br />
|}</div>Mollyhttp://fpcamigawiki.alb42.de/index.php?title=AmigaLib&diff=831AmigaLib2017-08-19T19:01:49Z<p>Molly: add deprecated note</p>
<hr />
<div>'''As of FPC 3.1.1 revision 36777, the use of unit Amigalib is deprecated.'''<br />
<br />
== Introduction ==<br />
<br />
Unit amigalib is a unit that has its origin in Classic Amiga amiga_lib headers.<br />
<br />
<br />
== The Problem ==<br />
<br />
Functions declared inside unit AmigaLib are primarily dictated by their c counter part on the underlying platform (plus some additional helpful routines that we can use ?).<br />
<br />
On Classic there are c headers for amiga_lib, on AROS there are the alib headers, while on MorphOS we have something similar also using alib headers.<br />
<br />
As usual, none of the available c headers offer any kind of unification, making a big mess of things when attempting to transfer to Pascal. AROS alib complicate things even further because alib was heavily expanded there, overlapping most of the functions declared in classic Amiga unit sysvartags.<br />
<br />
Since (currently) none of the available AmigaLib units contain all function implementations, there is room to toy around a little and attempting to organize the big messy pile.<br />
<br />
In order to do so, you can find a very large table, that contains all functions declared in their c counterparts that originates from their c-headers and which theoretically all belong to unit AmigaLib. The fact that some functions are not available on all supported platforms does not really matter as much.<br />
<br />
The goal of the table is to provide information on where the functions originate from, and be able to decide into which Pascal unit they need to end up.<br />
<br />
Note that implementing all functions inside the same unit poses the same difficulties encountered with classic Amiga unit sysvartags. There are some questions that requires answering before able to continue with this unit (See Below).<br />
<br />
== AmigaLib Table ==<br />
<br />
In the table below the left subtable contains the original c location, while the right subtable displays the pascal unit location in which the function (is) located).<br />
<br />
{| class="wikitable"<br />
|+ List of amiga lib functions and their location/status (c/Pascal) per platform<br />
|-<br />
! Function !! OS3.x !! AROS !! MorphOS !! !! OS3.x !! AROS !! MorphOS !! Remark(s)<br />
|-<br />
| ACrypt || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| AddAmigaguideHost || amigaguide || alib || || || systemvartags || n/a || || AmigaGuide<br />
|-<br />
| AddAppIcon || workbench || alib || || || n/i || workbench || || workbench<br />
|-<br />
| AddAppMenuItem || workbench || alib || || || systemvartags || workbench || || workbench<br />
|-<br />
| AddAppWindow || workbench || alib || || || systemvartags || workbench || || workbench<br />
|-<br />
| AddAppWindowDropZone || workbench || alib || || || n/i || workbench || || workbench<br />
|-<br />
| AllocAslRequestTags || asl || alib || || || systemvartags || || || asl<br />
|-<br />
| AllocDosObjectTags || dos || alib || || || systemvartags || amigados || || dos<br />
|-<br />
| AllocNamedObject || utility || alib || || || systemvartags || utility || || utility<br />
|-<br />
| AllocSpriteData || graphics || alib || || || systemvartags || agraphics || || graphics<br />
|-<br />
| AndRectRect || n/a || alib || || || n/a || agraphics || || MAG<br />
|-<br />
| AddTOF || amiga_lib || n/i || alib || || n/i || n/i || n/i || MAG<br />
|-<br />
| afp || amiga_lib || n/i || alib || || n/i || n/i || n/i || MAG<br />
|-<br />
| ArgArrayDone || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| ArgArrayInit || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| ArgInt || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| ArgString || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| ArosInquire || n/a || alib || n/a || || n/a || amigalib || n/a || MAG<br />
|-<br />
| arnd || amiga_lib || n/i || alib || || n/i || n/i || n/i || MAG<br />
|-<br />
| AslRequestTags || asl || alib || || || systemvartags || || || asl<br />
|-<br />
| asmallocpooled || n/a || alib || n/a || || n/a || amigalib || || MAG<br />
|-<br />
| asmcreatepool || n/a || alib || n/a || || n/a || amigalib || || MAG<br />
|-<br />
| asmdeletepool || n/a || alib || n/a || || n/a || amigalib || || MAG<br />
|- <br />
| asmfreepooled || n/a || alib || n/a || || n/a || amigalib || || MAG<br />
|-<br />
| BeginIO || amiga_lib || alib || alib || || amigalib || amigalib || n/i || MAG<br />
|-<br />
| BestCModeIDTags || n/a || alib || || || cybergraphics || cybergraphics || || CyberGfx<br />
|-<br />
| BestModeID || graphics || alib || || || systemvartags || agraphics || || Gfx<br />
|-<br />
| BuildEasyRequest || intuition || alib || || || n/i || intuition || || intuition<br />
|-<br />
| CallHook || amiga_lib || alib || alib || || n/i || amigalib/utility || n/i || MAG<br />
|-<br />
| CallHookA || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| ChangeExtSprite || graphics || alib || || || systemvartags || agraphics || || Gfx<br />
|-<br />
| CheckRexxMsg || amiga_lib || alib || alib || || n/i || amigalib || n/i || ???<br />
|-<br />
| CloseWorkbenchObject || workbench || alib || || || n/i || workbench || || workbench<br />
|-<br />
| CoerceMethod || amiga_lib || alib || alib || || n/i || amigalib/intuition || n/i || MAG<br />
|-<br />
| CoerceMethodA || amiga_lib || alib || alib || || amigalib || amigalib/intuition || n/i || MAG<br />
|-<br />
| CopyRegion || n/a || alib || n/a || || n/a || amigalib/agraphics || || MAG<br />
|-<br />
| CreateExtIO || amiga_lib || alib || alib || || amigalib || amigalib || n/i || MAG<br />
|-<br />
| CreateGadget || gadtools || alib || || || systemvartags || n/i || || gadtools<br />
|-<br />
| CreateMenus || gadtools || alib || || || systemvartags || n/i || || gadtools<br />
|-<br />
| CreateNewProcTags || dos || alib || || || systemvartags || amigados || || dos<br />
|-<br />
| CreatePort || amiga_lib || alib || alib || || amigalib || amigalib || n/i || MAG<br />
|-<br />
| CreateStdIO || amiga_lib || alib || alib || || amigalib || amigalib || n/i || MAG<br />
|-<br />
| CreateTask || amiga_lib || alib || alib || || amigalib || amigalib || n/i || MAG<br />
|-<br />
| CxCustom || amiga_lib || commodities || || || amigalib || amigalib || n/i || ???<br />
|-<br />
| CxDebug || amiga_lib || commodities || || || amigalib || amigalib || n/i || ???<br />
|-<br />
| CxFilter || amiga_lib || commodities || || || amigalib || amigalib || n/i || ???<br />
|-<br />
| CxSender || amiga_lib || commodoties || || || amigalib || amigalib || n/i || ???<br />
|-<br />
| CxSignal || amiga_lib || commodities || || || amigalib || amigalib || n/i || ???<br />
|-<br />
| CxTranslate || amiga_lib || commodities || || || amigalib || amigalib || n/i || ???<br />
|-<br />
| dbf || amiga_lib || n/i || alib || || n/i || n/i || n/i || MAG<br />
|-<br />
| DISPATCHERARG || n/a || n/a || n/a || || n/a || n/a || amigalib/purple || ???<br />
|-<br />
| DeleteExtIO || amiga_lib || alib || alib || || amigalib || amigalib || n/i || MAG<br />
|-<br />
| DeletePort || amiga_lib || alib || alib || || amigalib || amigalib || n/i || MAG<br />
|-<br />
| DeleteStdIO || amiga_lib || alib || alib || || amigalib || amigalib || n/i || MAG<br />
|-<br />
| DeleteTask || amiga_lib || alib || alib || || amigalib || amigalib || n/i || MAG<br />
|-<br />
| DoDTMethod || datatypes || alib || || || n/i || datatypes || || datatypes<br />
|-<br />
| DoGadgetMethod || intuition || alib || || || n/i || || || intuition<br />
|-<br />
| DoMethod || amiga_lib || alib || alib || || n/i || amigalib || amigalib || MAG<br />
|-<br />
| DoMethodA || amiga_lib || alib || alib || || amigalib || amigalib || amigalib || MAG<br />
|-<br />
| DoSuperMethod || amiga_lib || alib || alib || || n/i || amigalib || amigalib || MAG<br />
|-<br />
| DoSuperMethodA || amiga_lib || alib || alib || || amigalib || amigalib || amigalib || MAG<br />
|-<br />
| DoSuperNew || n/a || alib || alib || || n/a || amigalib || n/i || MAG<br />
|-<br />
| DoTimer || n/a || n/a || alib || || n/a || n/a || || ???<br />
|-<br />
| DrawBevelBox || gadtools || alib || || || systemvartags || n/i || || gadtools<br />
|-<br />
| EasyRequest || intuition || alib || || || n/i || intuition || || intuition<br />
|-<br />
| ErrorOutput || n/a || alib || || || n/a || amigalib || || MAG<br />
|-<br />
| ExtendFontTags || graphics || alib || || || systemvartags || agraphics || || gfx<br />
|-<br />
| FastRand || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| fpa || amiga_lib || n/a || alib || || n/i || n/i || n/i || MAG<br />
|-<br />
| fpbcd || n/a || n/a || alib || || n/a || n/a || || MAG<br />
|-<br />
| fprintf || amiga_lib || alib || || || n/i || n/i || || ???<br />
|-<br />
| FreeIEvents || amiga_lib || alib || alib || || amigalib || amigalib || n/i || MAG<br />
|-<br />
| fwritef || n/a || alib || || || n/i || n/i || n/i || ???<br />
|-<br />
| GetDTAttrs || datatypes || alib || || || systemvartags || datatypes || || datatypes<br />
|-<br />
| GetExtSprite || graphics || alib || || || systemvartags || agraphics || || gfx<br />
|-<br />
| GetRexxVar || amiga_lib || alib || alib || || n/i || amigalib || n/i || ???<br />
|-<br />
| GetRPAttrs || graphics || alib || || || systemvartags || agraphics || || gfx<br />
|-<br />
| GetVisualInfo || gadtools || alib || || || systemvartags || n/i || || gadtools<br />
|-<br />
| GT_GetGadgetAttrs || gadtools || alib || || || systemvartags || n/i || || gadtools<br />
|-<br />
| GT_SetGadgetAttrs || gadtools || alib || || || systemvartags || n/i || || gadtools<br />
|-<br />
| HookEntry || amiga_lib || alib || alib || || amigalib || amigalib/purple || amigalib/purple || MAG<br />
|-<br />
| HotKey || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| InvertString || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| InvertStringForward || n/a || alib || || || n/a || amigalib || || MAG<br />
|-<br />
| LayoutMenuItems || gadtools || alib || || || systemvartags || n/i || || gadtools<br />
|-<br />
| LayoutMenus || gadtools || alib || || || systemvartags || n/i || || gadtools<br />
|-<br />
| LibAllocAligned || n/a || alib || || || n/a || amigalib || || MAG<br />
|-<br />
| LibAllocPooled || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| LibCreatePool || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| LibDeletePool || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| LibFreePooled || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| LockBitmapTags || n/a || alib || || || cybergraphics || cybergraphics/purple || || cgfx<br />
|-<br />
| MakeDirAll || n/a || n/a || alib || || n/a || n/a || || ???<br />
|-<br />
| MakeWorkbenchObjectVisible || workbench || alib || || || n/i || workbench || || woprkbench<br />
|-<br />
| MergeSortList || n/a || alib || || || n/a || n/i || || ???<br />
|-<br />
| NewDTObject || datatypes || alib || || || systemvartags || datatypes || || datatypes<br />
|-<br />
| NewList || amiga_lib || alib || alib || || amigalib || amigalib/exec || n/i || ???<br />
|-<br />
| NewLoadSegTags || dos || alib || || || systemvartags || amigados || || amigados<br />
|-<br />
| NewObject || intuition || alib || || || systemvartags || intuition || || intuition<br />
|-<br />
| NewRawDoFmt || n/a || alib || || || n/a || n/i || || ???<br />
|-<br />
| NewRectRegion || n/a || alib || || || n/a || amigalib/agraphics || || ???<br />
|-<br />
| ObtainBestPen || graphics || alib || || || systemvartags || agraphics || || gfx<br />
|-<br />
| ObtainInfo || bullet || alib || || || systemvartags || n/a || || ???<br />
|-<br />
| OpenAmigaGuide || amigaguide || alib || || || systemvartags || n/a || || amigaguide<br />
|-<br />
| OpenAmigaGuideAsync || amigaguide || alib || || || systemvartags || n/a || || amigaguide<br />
|-<br />
| OpenCatalog || locale || alib || || || systemvartags || locale || || locale<br />
|-<br />
| OpenMakeDir || n/a || n/a || alib || || n/a || n/a || || ???<br />
|-<br />
| OpenScreenTags || intuition || alib || || || systemvartags || intuition || || intuition<br />
|-<br />
| OpenWindowTags || intuition || alib || || || systemvartags || intuition || || intuition<br />
|-<br />
| OpenWorkbenchObject || workbench || alib || || || n/i || workbench || || workbench<br />
|-<br />
| printf || amiga_lib || alib || || || amigalib || n/i || n/i || ???<br />
|-<br />
| RangeRand || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| RefreshDTObject || datatypes || alib || || || systemvartags || datatypes || || datatypes<br />
|-<br />
| RefreshDTObjects || n/a || alib || || || n/a || n/i || || ???<br />
|-<br />
| ReleaseInfo || bullet || alib || || || systemvartags || n/a || || ???<br />
|-<br />
| RemTOF || amiga_lib || n/a || alib || || n/i || n/i || n/i || ???<br />
|-<br />
| RemoveAmigaGuideHost || amigaguide || alib || || || systemvartags || n/a || || amigaguide<br />
|-<br />
| SelectErrorOutput || n/a || alib || || || n/a || amigalib || || MAG<br />
|-<br />
| SendAmigaGuideCmd || amigaguide || alib || || || systemvartags || n/a || || amigaguide<br />
|-<br />
| SendAmigaGuideContext || amigaguide || alib || || || systemvartags || n/a || || amigaguide<br />
|-<br />
| SetAmigaGuideAttrs || amigaguide || alib || || || systemvartags || n/a || || amigaguide<br />
|-<br />
| SetAmigaGuideContext || amigaguide || alib || || || systemvartags || n/a || || amigaguide<br />
|-<br />
| SetAttrs || intuition || alib || || || n/i || || || intuition<br />
|-<br />
| SetDTAttrs || datatypes || alib || || || systemvartags || datatypes || || datatypes<br />
|-<br />
| SetGadgetAttrs || intuition || alib || || || systemvartags || intuition || || intuition<br />
|-<br />
| SetInfo || bullet || alib || || || systemvartags || n/a || || ???<br />
|-<br />
| SetRexxVar || amiga_lib || alib || alib || || n/i || amigalib || n/i || ???<br />
|-<br />
| SetRPAttrs || graphics || alib || || || systemvartags || agraphics || || gfx<br />
|-<br />
| SetSuperAttrs || amiga_lib || alib || || || n/i || amigalib/intuition || n/i || MAG<br />
|-<br />
| SetSuperAttrsA || || alib || alib || || amigalib || amigalib/purple || n/i || MAG<br />
|-<br />
| SetWindowPointer || intuition || alib || || || n/i || intuition || || intuition<br />
|-<br />
| sprintf || amiga_lib || alib || libc? || || n/i || n/i || n/i || ???<br />
|-<br />
| strdup || amiga_lib || alib || libc? || || n/i || n/i || n/i || ???<br />
|-<br />
| SystemTags || dos || alib || || || systemvartags || amigados || || amigados<br />
|-<br />
| TimeDelay || amiga_lib || alib || alib || || n/i || amigalib || n/i || MAG<br />
|-<br />
| UnlockBitmapTags || n/a || alib || || || cybergraphics || cybergraphics/purple || || cybergfx<br />
|-<br />
| VideoControlTags || graphics || alib || || || systemvartags || agraphics || || gfx<br />
|-<br />
| waitbeam || n/a || n/a || alib || || n/a || n/a || n/i || ???<br />
|-<br />
| WorkbenchControl || workbench || alib || || || n/i || workbench || || workbench<br />
|}<br />
<br />
legend:<br />
: red = unimplemented<br />
: red + n/a = unimplemented, no other implementation available (e.g. no c, no pascal meaning implement from scratch).<br />
: blue = implemented, untested<br />
: purple = difficulty, discuss first<br />
: Green = implemented, tested, and having correct (pascal) location mentioned<br />
<br />
== Questions ==<br />
<br />
* Initially, what (official) funcs/procs should go in amigalib ?<br />
* do we want ^^ those ^^ for all 3 platforms ?<br />
* What additional funcs/procs need to go in ?<br />
* In case related functions included, how to solve the fact that libs get auto-opened (or don't we care) ?<br />
<br />
== Remarks ==<br />
<br />
After some work done on the actual implementation of unit amigalib, some practical issues presented itself.<br />
<br />
In order to not fall into the same pitfall of systemvartags unit, some (if not most) functions that originally are located inside amigalib actually need to be located inside their respective units.<br />
<br />
But, when applying that rule a full 100%, unit amigalib ends up with the following 3 functions:<br />
* function ACrypt(buffer: PChar; password: PChar; username: PChar): PChar;<br />
* function FastRand(seed: ULONG): ULONG;<br />
* function RangeRand(maxValue: ULONG): ULONG;<br />
<br />
The two random related functions have no meaning for Pascal whatsoever, as it offers its own random functions.<br />
<br />
Which leaves the ACrypt function.<br />
<br />
There was talk about wanting to add some helper functions inside unit AmigaLib, such as SetHook(), but that actually belongs to unit utility (when applying the same rule for not falling for the systemvartags pitfall).<br />
<br />
Other then what was mentioned in this paragraph, we need to be fair. Amigalib also adds a couple of clib function such as printf(). Do we want a pascal implementation for that (and inside unit amigalib) ?<br />
<br />
To make things complete, there are a couple of other function inside the original AmigaLib, such as dbf, but there isn't an actual implementation available in source.<br />
<br />
<br />
So... "what again was the purpose of unit amigalib", i started wondering.</div>Molly