Porting to PlayStation1 Guide
Overview
This is a step by step tutorial based on the example of my FPC port to the PlayStation 1.
To see the port in action look at the PlayStation 1 compilation manual.
This Guide assumes we are working under Linux.
Forking and Merging
The first step is to create an GitLab account and Fork the compiler sources from
https://gitlab.com/freepascal.org/fpc/source
when done, create a new branch. I called it PS1.
Now when Your are done with the work, just do a merge request. The FPC Team will review Your changes and merge them into the main branch.
Preparing the Assembler and Linker
As we don't going to write our own internal Assembler and Linker we use GNU BinUtils. In our case we need the MIPSEL one.
Download a recent version from https://ftp.gnu.org/gnu/binutils/ . Then run shell commands:
./tar -xf <binutils.tar.gz> ./cd <binutils> ./mkdir build ./cd build ../configure --prefix=/usr/local/mipsel-unknown-elf --target=mipsel-unknown-elf --with-float=soft make -j$(nproc) sudo make install export PATH="/usr/local/mipsel-unknown-elf/bin:$PATH"
Now you can go two ways:
1) Let the binaries as they are and always use the compiler option
-XP/usr/local/mipsel-unknown-elf/bin/mipsel-unknown-elf-
this means we going to build the compiler with the option CROSSOPT="-XP/usr/local/mipsel-unknown-elf/bin/mipsel-unknown-elf-"
2) Create symlinks
sudo ln -s /usr/local/mipsel-unknown-elf/bin/mipsel-unknown-elf-as /usr/bin/mipsel-ps1-as sudo ln -s /usr/local/mipsel-unknown-elf/bin/mipsel-unknown-elf-ld /usr/bin/mipsel-ps1-ld sudo ln -s /usr/local/mipsel-unknown-elf/bin/mipsel-unknown-elf-objcopy /usr/bin/mipsel-ps1-objcopy
this means we don't need any extra compile options
Setup the make files
1) add the new build target to fpcmake:
- You need to extend the ./utils/fpcm/fpcmmain.pp:
insert the new Target in
type TOS=(..., ps1); (about line 100)
section.
don't forget to insert it also to OSStr(about line 135) and OSSufix(about line 144) constants.
add at the end of the table OSCpuPossible a new os/cpu entry (about line 207)
- now expand the ./packages/fpmkunit/src/fpmkunit.pp
TOS=(.., ps1) (about line 166)
and in the OSCPUSupported table like with the fpcmmain above
2) rebuild your native compiler:
make all sudo make install
now fpcmake can generate make files.
3) add to ./compiler/Makefile.fpc:
NO_NATIVE_COMPILER_OS_LIST (about line 44)
your target system, ps1 this indicates, there is no native compiler for your target
4) time to rebuild all make files:
you have to run the fpcmake in all folders of the fpc sources after that to build the rtl make files you have to execute ./regenmakefiles.sh in ./rtl to build the packages you have to execute ./regenmakefiles.sh in ./packages
to simplify all those steps execute my script:
#!/bin/sh set -e FPCMAKE=/usr/local/bin/fpcmake find . -name Makefile.fpc | xargs $FPCMAKE -Tall cd ./rtl ./regenmakefiles.sh cd ./.. cd ./packages ./regenmakefiles.sh cd ./.. cd ./utils/build/ $FPCMAKE -Tall -s -o Makefile.pkg Makefile.fpc cd ./../.. cd ./packages/build/ $FPCMAKE -Tall -s -o Makefile.pkg Makefile.fpc cd ./../..
Inform the compiler about the new Target
1) ppu signature
./compiler/utils/pputils/ppudump.pp (about line 252)
2) info screen of the compiler
./compiler/msg/errore.msg under option_help_pages (about line 4369)
BTW. this file:
I included an error message in the section about linking (about line 3224)
link_f_executable_too_big_exceeds_X_by_Y_bytes=09224_F_Executable image size is too big for $1 target (exceeds $2 by $3 bytes). % The result File is too big
I mentioned this because You have to keep track of the message number. In this case it is 09224. You can display this message by invoking:
Message3(link_f_executable_too_big_exceeds_X_by_Y_bytes, 'PS1', '2097152', tostr(filesize(f) - $200000));
3) to get the set of systems what support the new target add in
./compiler/systems.pas (about line 430, in our case after systems_jvm) the ps1 target
./compiler/systems.inc: (about line 218) in type tsystem
4) register new linker in ./compiler/systems.inc (about line 339) in tlink
ld_ps1
5) set to your target CPU (in this case MIPS) the new platform:
./compiler/mips/cputarg.pas (about line 58):
{$ifndef NOTARGETPS1}
,t_ps1
{$endif}
6) set the Target CPU to MIPS1
./compiler/options.pas (about line 5555):
system_mipsel_PS1:
begin
{ set default cpu type to MIPS1 with no FPU }
if not option.CPUSetExplicitly then
init_settings.cputype:=cpu_mips1;
if not option.OptCPUSetExplicitly then
init_settings.optimizecputype:=cpu_mips1;
if not option.FPUSetExplicitly then
init_settings.fputype:=fpu_none;
end;
pay attention that we going with no FPU for now. Later we will add the Software FPU support. We are using the the -Cfnone option.
So we comment out in ./compiler/options.pas (about line 4656):
def_system_macro('FPC_HAS_TYPE_DOUBLE');
def_system_macro('FPC_HAS_TYPE_SINGLE');
def_system_macro('FPC_INCLUDE_SOFTWARE_INT64_TO_DOUBLE');
7) As we support only external assembler (GNU)
./compiler/mips/cpugas.pas (about line 283)
add to as_MIPSEL_as_info the new target : system_mipsel_ps1
Create ./compiler/systems/i_ps1.pas
This unit implements support information structures for ps1.
unit i_ps1;
{$i fpcdefs.inc}
interface
uses systems;
const
system_mipsel_ps1_info : tsysteminfo =
(
system : system_mipsel_ps1;
name : 'PlayStation 1 for MIPSEL';
shortname : 'ps1';
flags : [tf_no_pic_supported, tf_smartlink_sections, tf_files_case_sensitive, tf_requires_proper_alignment, tf_emit_stklen];
cpu : cpu_mipsel;
unit_env : '';
extradefines : '';
exeext : '';
defext : '.def';
scriptext : '.sh';
smartext : '.sl';
unitext : '.ppu';
unitlibext : '.ppl';
asmext : '.s';
objext : '.o';
resext : '.res';
resobjext : '.or';
sharedlibext : '.so';
staticlibext : '.a';
staticlibprefix : 'libp';
sharedlibprefix : 'lib';
sharedClibext : '.so';
staticClibext : '.a';
staticClibprefix : 'lib';
sharedClibprefix : 'lib';
importlibprefix : 'libimp';
importlibext : '.a';
Cprefix : '';
newline : #10;
dirsep : '/';
assem : as_gas;
assemextern : as_gas;
link : ld_none;
linkextern : ld_ps1;
ar : ar_gnu_ar;
res : res_elf;
dbg : dbg_none;
script : script_unix;
endian : endian_little;
alignment :
(
procalign : 8;
loopalign : 8;
jumpalign : 8;
jumpalignskipmax : 8;
coalescealign : 0;
coalescealignskipmax: 8;
constalignmin : 0;
constalignmax : 8;
varalignmin : 0;
varalignmax : 8;
localalignmin : 0;
localalignmax : 8;
recordalignmin : 0;
recordalignmax : 8;
maxCrecordalign : 8
);
first_parm_offset : 0;
stacksize : 262144;
stackalign : 8;
abi : abi_default;
llvmdatalayout : 'e-p:32:32:32-i1:8:8-i8:8:32-i16:16:32-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-n32-S64';
);
implementation
initialization
{$IFDEF CPUMIPSEL}
{$IFDEF PS1}
set_source_info(system_mipsel_ps1_info);
{$ENDIF}
{$ENDIF}
end.
Create ./compiler/systems/t_ps1.pas
For now we going with the minimal implementation
unit t_ps1;
{$i fpcdefs.inc}
interface
uses link;
type
TLinkerPS1=class(TExternalLinker)
private
Function WriteResponseFile(isdll:boolean) : Boolean;
public
constructor Create;override;
procedure SetDefaultInfo;override;
function MakeExecutable:boolean;override;
procedure InitSysInitUnitName; override;
end;
implementation
uses
SysUtils,
cutils,cfileutl,cclasses,
verbose,systems,globtype,globals,
fmodule,
aasmbase,
ogelf,owar,
i_ps1
;
Constructor TLinkerPS1.Create;
begin
Inherited Create;
end;
procedure TLinkerPS1.SetDefaultInfo;
begin
with Info do
begin
ExeCmd[1]:= 'ld $OPT $RES';
end;
end;
Function TLinkerPS1.WriteResponseFile(isdll: boolean): Boolean;
begin
result:= True;
end;
function TLinkerPS1.MakeExecutable: boolean;
begin
result:= true;
end;
procedure TLinkerPS1.InitSysInitUnitName;
begin
sysinitunit:='si_prc';
end;
initialization
RegisterLinker(ld_ps1, TLinkerPS1);
RegisterTarget(system_mipsel_ps1_info);
end.
Create a minimal RTL
In this step we are using the minimal RTL setup.
./rtl/ps1/system.pas:
unit system;
interface
const
MaxLongint = $7fffffff;
type
TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat, tkSet,
tkMethod, tkSString, tkLString, tkAString, tkWString, tkVariant, tkArray,
tkRecord, tkInterface, tkClass, tkObject, tkWChar, tkBool, tkInt64, tkQWord,
tkDynArray, tkInterfaceRaw, tkProcVar, tkUString, tkUChar, tkHelper, tkFile,
tkClassRef, tkPointer);
{$I typedefs.inc}
{$I ../mips/setjumph.inc}
type
PExceptAddr = ^TExceptAddr;
TExceptAddr = record
buf: Pjmp_buf;
next: PExceptAddr;
{$IFDEF CPU16}
frametype: SmallInt;
{$ELSE CPU16}
frametype: LongInt;
{$ENDIF CPU16}
end;
PGuid = ^TGuid;
TGuid = packed record
case Integer of
1:
(Data1: DWord;
Data2: word;
Data3: word;
Data4: array [0 .. 7] of byte;
);
2:
(D1: DWord;
D2: word;
D3: word;
D4: array [0 .. 7] of byte;
);
3:
( { uuid fields according to RFC4122 }
time_low: DWord; // The low field of the timestamp
time_mid: word; // The middle field of the timestamp
time_hi_and_version: word;
// The high field of the timestamp multiplexed with the version number
clock_seq_hi_and_reserved: byte;
// The high field of the clock sequence multiplexed with the variant
clock_seq_low: byte; // The low field of the clock sequence
node: array [0 .. 5] of byte; // The spatially unique node identifier
);
end;
var
ExitCode: hresult = 0; export name 'operatingsystem_result';
procedure fpc_initializeunits; compilerproc;
procedure fpc_do_exit; compilerproc;
implementation
{$I ../mips/setjump.inc}
procedure fpc_initializeunits;
begin
end;
procedure fpc_do_exit;
begin
end;
end.
./rtl/ps1/typedefs.inc:
Type
{ The compiler has all integer types defined internally. Here
we define only aliases }
DWord = LongWord;
Cardinal = LongWord;
Integer = SmallInt;
UInt64 = QWord;
SizeInt = Longint;
SizeUInt = DWord;
PtrInt = Longint;
PtrUInt = DWord;
ValSInt = Longint;
ValUInt = Cardinal;
CodePointer = Pointer;
CodePtrInt = PtrInt;
CodePtrUInt = PtrUInt;
TExitCode = Longint;
{ NativeInt and NativeUInt are Delphi compatibility types. Even though Delphi
has IntPtr and UIntPtr, the Delphi documentation for NativeInt states that
'The size of NativeInt is equivalent to the size of the pointer on the
current platform'. Because of the misleading names, these types shouldn't be
used in the FPC RTL. Note that on i8086 their size changes between 16-bit
and 32-bit according to the memory model, so they're not really a 'native
int' type there at all. }
NativeInt = Type PtrInt;
NativeUInt = Type PtrUInt;
Int8 = ShortInt;
Int16 = SmallInt;
Int32 = Longint;
IntPtr = PtrInt;
UInt8 = Byte;
UInt16 = Word;
UInt32 = Cardinal;
UIntPtr = PtrUInt;
{ Zero - terminated strings }
{$IF DECLARED(AnsiChar)}
// Compiler defines AnsiChar and WideChar, not AnsiChar
{$IFDEF UNICODERTL}
Char = WideChar;
{$ElSE}
Char = AnsiChar;
{$ENDIF}
{$ELSE}
// Compiler defines Char, we make AnsiChar an alias
AnsiChar = char;
{$ENDIF}
PChar = ^Char;
PPChar = ^PChar;
PPPChar = ^PPChar;
TAnsiChar = AnsiChar;
PAnsiChar = ^AnsiChar;
PPAnsiChar = ^PAnsiChar;
PPPAnsiChar = ^PPAnsiChar;
UTF8Char = AnsiChar;
PUTF8Char = PAnsiChar;
UCS4Char = 0..$10ffff;
PUCS4Char = ^UCS4Char;
{$ifdef CPU16}
TUCS4CharArray = array[0..32767 div sizeof(UCS4Char)-1] of UCS4Char;
{$else CPU16}
TUCS4CharArray = array[0..$effffff] of UCS4Char;
{$endif CPU16}
PUCS4CharArray = ^TUCS4CharArray;
{$ifdef FPC_HAS_FEATURE_DYNARRAYS}
UCS4String = array of UCS4Char;
{$endif}
Comp = type Int64;
PCurrency = ^Currency;
UTF8String = type ansistring;
PUTF8String = ^UTF8String;
RawByteString = ansistring;
HRESULT = type Longint;
TError = type Longint;
PSmallInt = ^Smallint;
PShortInt = ^Shortint;
PInteger = ^Integer;
PByte = ^Byte;
PWord = ^word;
PDWord = ^DWord;
PLongWord = ^LongWord;
PLongint = ^Longint;
PCardinal = ^Cardinal;
PQWord = ^QWord;
PInt64 = ^Int64;
PUInt64 = ^UInt64;
PPtrInt = ^PtrInt;
PPtrUInt = ^PtrUInt;
PSizeInt = ^SizeInt;
PSizeUInt = ^SizeUInt;
PPByte = ^PByte;
PPLongint = ^PLongint;
PPointer = ^Pointer;
PPPointer = ^PPointer;
PCodePointer = ^CodePointer;
PPCodePointer = ^PCodePointer;
PBoolean = ^Boolean;
{$IFNDEF VER3_0}
PBoolean8 = ^Boolean8;
{$ENDIF VER3_0}
PBoolean16 = ^Boolean16;
PBoolean32 = ^Boolean32;
PBoolean64 = ^Boolean64;
PByteBool = ^ByteBool;
PWordBool = ^WordBool;
PLongBool = ^LongBool;
PQWordBool = ^QWordBool;
PNativeInt = ^NativeInt;
PNativeUInt = ^NativeUint;
PInt8 = PShortInt;
PInt16 = PSmallint;
PInt32 = PLongint;
PIntPtr = PPtrInt;
PUInt8 = PByte;
PUInt16 = PWord;
PUInt32 = PDWord;
PUintPtr = PPtrUInt;
PShortString = ^ShortString;
PAnsiString = ^AnsiString;
PRawByteString = ^RawByteString;
{$ifndef FPUNONE}
PDate = ^TDateTime;
PDateTime = ^TDateTime;
{$endif}
PError = ^TError;
{$ifdef FPC_HAS_FEATURE_VARIANTS}
PVariant = ^Variant;
POleVariant = ^OleVariant;
{$endif FPC_HAS_FEATURE_VARIANTS}
PWideChar = ^WideChar;
PPWideChar = ^PWideChar;
PPPWideChar = ^PPWideChar;
WChar = Widechar;
UCS2Char = WideChar;
PUCS2Char = PWideChar;
PWideString = ^WideString;
UnicodeChar = WideChar;
PUnicodeChar = ^UnicodeChar;
PUnicodeString = ^UnicodeString;
PMarshaledString = ^PWideChar;
PMarshaledAString = ^PAnsiChar;
MarshaledString = PWideChar;
MarshaledAString = PAnsiChar;
TSystemCodePage = Word;
TFileTextRecChar = {$if defined(FPC_ANSI_TEXTFILEREC) or not(defined(FPC_HAS_FEATURE_WIDESTRINGS))}AnsiChar{$else}UnicodeChar{$endif};
PFileTextRecChar = ^TFileTextRecChar;
TTextLineBreakStyle = (tlbsLF,tlbsCRLF,tlbsCR);
{ opaque data type and related opaque pointer }
TOpaqueData = record end;
POpaqueData = ^TOpaqueData;
OpaquePointer = type POpaqueData;
{ procedure type }
TProcedure = Procedure;
./rtl/ps1/objpas.pp:
{$Mode OBJFPC}
{$I-,S-}
unit objpas;
interface
{ first, in object pascal, the integer type must be redefined }
const
MaxInt = MaxLongint;
type
Integer = longint;
implementation
end.
./rtl/ps1/si_prc.pp:
{$SMARTLINK OFF}
unit si_prc;
interface
implementation
procedure PascalMain; external name 'PASCALMAIN';
function _FPC_proc_start: longint; cdecl; public name '_start';
begin
PascalMain;
end;
begin
end.
Build and Compile the first time
1) Now we can build the compiler itself:
make compiler_cycle CPU_TARGET=mipsel OS_TARGET=ps1 OPT="-O- -CR -glv" CROSSOPT="-O- -g-" ALLOW_WARNINGS=1
2) Build the minimal RTL:
./compiler/ppcrossmipsel ./rtl/ps1/system.pp -O- -Tps1 -Fi./rtl/mipsel -Fi./rtl/inc -Fi./rtl/ps1 -a -XP/usr/local/mipsel-unknown-elf/bin/mipsel-unknown-elf- -Cfnone ./compiler/ppcrossmipsel ./rtl/inc/fpintres.pp -O- -Tps1 -Fu./rtl/ps1 -Fi./rtl/mipsel -Fi./rtl/inc -Fi./rtl/ps1 -a -XP/usr/local/mipsel-unknown-elf/bin/mipsel-unknown-elf- -Cfnone
3) Create the first test.pas:
begin
asm
li $a1, 123
end;
end.
4) Compile it:
./compiler/ppcrossmipsel test.pas -Cn -O- -Fu./rtl/ps1 -Fu./rtl/inc -Tps1 -Cfnone -a -XP/usr/local/mipsel-unknown-elf/bin/mipsel-unknown-elf-
Congratulations!
Building the full RTL
1) Add requered RTL units to the makefile:
./rtl/ps1/Makefile.fpc
...
units=$(SYSTEMUNIT) $(OBJPASUNIT) $(FPINTRESUNIT) $(ISO7185UNIT) $(CTYPESUNIT) \
$(SYSCONSTUNIT) $(UUCHARUNIT) $(STRINGSUNIT) \
libstd libetc libgte libgpu libapi libcd libcomb libgs libsnd libspu libds libgun libmcrd libtap libpad libmcx libpress libsn libmcgui libhmd \
si_prc
...
# PSY-Q-SDK units
libstd$(PPUEXT) : psy-q-sdk/libstd.pas $(OBJPASUNIT)$(PPUEXT) $(SYSTEMUNIT)$(PPUEXT)
$(COMPILER) $<
libetc$(PPUEXT) : psy-q-sdk/libetc.pas $(OBJPASUNIT)$(PPUEXT) $(SYSTEMUNIT)$(PPUEXT)
$(COMPILER) $<
libgte$(PPUEXT) : psy-q-sdk/libgte.pas $(OBJPASUNIT)$(PPUEXT) $(SYSTEMUNIT)$(PPUEXT)
$(COMPILER) $<
...
# Loaders
si_prc$(PPUEXT) : $(SYSTEMUNIT)$(PPUEXT)
$(COMPILER) si_prc.pp
the listed units gonna be compiled an put automatically into the units folder of the fpc installation.