Fortran on Playdate?

I created a command line C/Fortran interop example for Mac. Does anyone have any thoughts on what it would take to mix Fortran code into a Playdate game? The LLVM flang compiler appears to be a work in progress that delegates actual compilation to another compiler, gfortran by default. Evidently people have cross compiled Fortran for ARM, but I have not attempted it yet. (Obviously, having hardware would help here.)

In case anyone is wondering about the why, Fortran is used for heavy number crunching in mathematical and scientific computing. Libraries have been written over tha last 40 years or so. Also, Fortran can be faster than C because blazing fast execution was the focus of non-modern Fortran. The C standard makes certain guarantees that slow code down where Fortran programmers were expected to make sure the code was valid (overlapping memory regions for function IO).

I've been working in this zone since I am working on porting swift and probably have some useful things to say:

  • Clang/LLVM support for fortran is almost a must have if you want to run on simulator, since the existing make files rely on clang to create the shared lib for your host system that is embedded in the pdx. Of course, technically, any shared object which correctly exposes an unmangled eventHandler should function..
  • If you manage to get the above to work do not mistake this for proper support! It is only a first step. Once you have the semi-hosted simulator working, you will need to turn your attention to the arm-none-eabi toolchain that is used for producing the bin files that are embedded in the pdx for running on device. I am not really sure how fortran is compiled or linked (is it a lib? if so, can it be produced as a static lib only? does it have a runtime?). You can likely get away with compiling using any compiler that supports thumbv7m/em but you can't avoid using the linker provided in the arm toolchain afaik. Based on my quick perusal on google in relation to this- it seems folks are managing to get this working on android. However, most Android devices target application variants of the ARM Cortex series and these are very different processors from the thumb versions. Also be sure to look in to bare metal fortran before relying on anything that seems to require a host OS, because there isn't really an accessible host OS when you are running on device. I don't really know how memory management works in fortran, but you will not have access to an mmu - and that is a big problem if fortran requires one.
  • You will need to use the custom linker script in PlaydateSDK/C_API/buildsupport, this is because the device/PlaydateOS have certain expectations about where things are located. This can get challenging if you need things the linker script doesn't provide by default, especially without a device since you won't be able to test those changes. I have found though, that some minor changes to the linker script work completely fine on device, but if you have to make broad changes you will need to be careful.
  • Are you really sure the performance benefit is there? While Fortran has always been very fast at number crunching, what are the performance penalties for interacting between C and Fortran? Is there a toll-free bridge between the languages or a penalty? Are you doing fast and short calculations many times per second or long running jobs (probably the former)? You could very well end up with something that is slower than plain C if there is a bridge or if the fortran compiler can't take advantage of things like SIMD on the arm processor (yes, it has SIMD and the compiler will automatically use it at certain optimization levels). I would try to answer these questions up-front before continuing on. The reasons why both swift and rust support are being brought to playdate is primarily because these languages are easier to work with / safer than C itself.
  • If you can manage to produce a bin file, you can use QEMU to at least verify that it somewhat boots on the class of processor used by Playdate. You won't be using the custom linker script in this scenario since there is no OS on the other side doing the actual loading for you. But it will verify that it actually works with the architecture.

None of this is to discourage you, it is really fun to try and bring a new language to new hardware! These are all things I wished I had known in more detail up front before starting on swift support :smiley:

2 Likes

Thank you for the list of pitfalls.

A quick note on motivation. Fortran is used for a lot scientific computing. If Fortran code exists that solves a problem, then I think it makes sense to link it in as a library. Specifically, I want code for astrophysical N-body simulations.

Fortran predates C, and it makes less guarantees than C does. From that standpoint, it can run faster than C. The memory model is simple, and interoperation with C is part of the modern Fortran standard. In practice, Fortran is mainly used academics who never learned computer science but need to crunch a lot of number for scientific calculations. Fortran culture is one speed, but I lot of the code could arguably be much better.

As for the porting details, Flang (the Clang Fortran complier) is still a work in progress. Cross compilation for ARM is being done, but it might be a little quirky. I will need to dig into the rest of the issues when I circle back to this problem, but I will probably be building the Fortran code as a static library.

Looks like flang does build a runtime for fortran... in c! it also has some reliance on clib things, but if you check out the windows target they seem to have flags to work around it. If you add a new triple (which looks pretty simple, this is much easier to reason about than the cmake scripts in the swift repo lol) to the cmake and try a thumb build it would likely get pretty close to just working.

Not sure running n-body simulations on 180mhz devices with a battery is going to be too fruitful though :wink:

thumb arch is experimental in llvm still (it has been for like over 10 years), but it should work fine - other targets are using it. Just be sure to use the arm linker.

1 Like

Fortran seems to work on the simulator if I package the code as a .a archive and subsequently link to it. The SDK Makefile does not seem to be linking source files to libraries for the simulator. My testing was done by manually performing all of the steps.

Yet another screenshot of another version of my square root test program, this one linked to the following Fortran code.

src/fast_sqrt.h

// header for C interop
#ifndef FAST_SQRT_H
#define FAST_SQRT_H

extern double fast_sqrt(double);

#endif  // FAST_SQRT_H

src/fast_sqrt.f90

function fast_sqrt( x ) result( y ) bind( C, name="fast_sqrt" )
  use iso_c_binding, only: c_double
  implicit none

  real(c_double), VALUE :: x
  real(c_double) :: y

  y = sqrt(x)
end function

Officially putting this on hold. I cannot figure out how to get gfortran to compile for arm, nor was an arm-none-eabi version forthcoming. One of the other compilers can probably do it, but there are higher priority things to focus on.

In the unlikely event anyone feels like trying to figure out this tooling puzzle, the commands to build an archive for the host machine with gfortran and ar follow.

gfortran -c fast_sqrt.f90
ar r fast_sqrt.a fast_sqrt.o

This goal is to build and archive for thumb arm-none-eabi. Given that, the rest of the task should be easy.

1 Like

The hard part is hard :slight_smile:

1 Like

Btw, the version of the ARM toolchain that Panic ships with the SDK isn't the full toolchain that ARM actually distributes. If you download the toolchain directly from them you can get a bin for arm-none-eabi-gfortran. Of course, it still won't be all that useful unless you have some sort of C entry point.

You can statically link libraries to the bin that gets spit out too. I would guess you could probably set up a cmake file that can automatically figure out how to compile fortran and c together, with CMAKE_FC_COMPILER being the big hint.

your example seems a little incomplete too, if you post the corresponding C code that would be helpful!

Is gcc-arm-none-eabi-10.3-2021.10-mac.pkg "Mac OS X 64-bit Package (Signed and notarized)" the correct download?

This is just a modified version of my ASM-Playdate example. I removed the fast_sqrt.s variants, and added fast_sqrt.f90 instead. Note that the signature in fast_sqrt.h changed because Fortran uses double by default. The source code for the ASM project can be found in this post.

EDIT: Having said that, it should be easy enough to call the double fast_sqrt(double) function from any project that uses the C API. It is likely easier than fuzing around with my project, seeing as there is not a working Makefile for any of the Fortran stuff.

I believe that 10.3 of the arm toolchain doesn't include gfortran but the 11 version does.

idk about makefile, but the cmake file should be pretty simple, something like this:

cmake_minimum_required(VERSION 3.14)
set(CMAKE_C_STANDARD 11)
set(CMAKE_Fortran_COMPILER /Users/ericlewis/Compilers/ARM/11.2/bin/arm-none-eabi-gfortran)

set(ENVSDK $ENV{PLAYDATE_SDK_PATH})

if (NOT ${ENVSDK} STREQUAL "")
    # Convert path from Windows
    file(TO_CMAKE_PATH ${ENVSDK} SDK)
else()
    execute_process(
            COMMAND bash -c "egrep '^\\s*SDKRoot' $HOME/.Playdate/config"
            COMMAND head -n 1
            COMMAND cut -c9-
            OUTPUT_VARIABLE SDK
            OUTPUT_STRIP_TRAILING_WHITESPACE
    )
endif()

if (NOT EXISTS ${SDK})
    message(FATAL_ERROR "SDK Path not found; set ENV value PLAYDATE_SDK_PATH")
    return()
endif()

project(FTRAN_TEST C ASM Fortran)

add_executable(FTRAN_TEST ${SDK}/C_API/buildsupport/setup.c src/main.c test.f90)

include(${SDK}/C_API/buildsupport/playdate_game.cmake)

In that case, presumably MacOS gcc-arm-11.2-2022.02-darwin-x86_64-arm-none-eabi.pkg download from the Arm GNU Toolchain Downloads page is what I am looking for?

Figuring out the Makefile should be easy given the ability to generate a valid .o object file from a .f90 Fortran source code. I have a non-Playdate test project for basic interop testing. I guess I may as well add it.

$ cd c-fortran-interop/
$ make test
* snip build output *
Hello from C to C!
Hello from C to Fortran!
Hello from Fortran to C!
Hello from Fortran to Fortran!

c-fortran-interop.zip (2.1 KB)

Open note to self.

The Cortex-M7 uses the ARMv7E-M microarchitecture. Kegel's crosstool built for -march=armv7e-m is a possible solution for running on hardware (crosstool howto).

I finally got Fortran to run on hardware with the macOS Hosted Bare-Metal Target (arm-none-eabi) 11.3.rel1 toolchain. The following software versions were used.

$ uname -vm
Darwin Kernel Version 21.6.0: Mon Aug 22 20:19:52 PDT 2022; root:xnu-8020.140.49~2/RELEASE_ARM64_T6000 arm64
$ ex -s +'%s/<[^>].\{-}>//ge' +'%s/\s\+//e' +'%norm J' +'g/^$/d' +%p +q! /System/Library/CoreServices/SystemVersion.plist | grep -E 'ProductName|ProductVersion' | sed 's/^[^ ]* //g' | sed 'N; s/\n/ /g'
macOS 12.6
$ echo "${SHELL}"
/bin/bash
$ "${SHELL}" --version  | head -n 1
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin21)
$ cat "${HOME}/Developer/PlaydateSDK/VERSION.txt"
1.12.3
$ gfortran -v 2> >(tail -1)
gcc version 12.2.0 (MacPorts gcc12 12.2.0_0+stdlib_flag)
$ arm-none-eabi-gfortran -v 2> >(tail -1)
gcc version 11.3.1 20220712 (Arm GNU Toolchain 11.3.Rel1)

First, source files need to be listed in Makefile.

Makefile Partial Listing

PRODUCT = FortranTest.pdx

# List C source files here
SRC = src/main.c

# List Fortran source files here
FSRC = src/fast_sqrt.f90

# last line of Makefile
include common.mk

I copied common.mk into the project before modifying it.

cp "${PLAYDATE_SDK_PATH}/C_API/buildsupport/common.mk" common.mk

The following diff lists the changes.

$ diff "${PLAYDATE_SDK_PATH}/C_API/buildsupport/common.mk" common.mk
84c84
< _OBJS	= $(SRC:.c=.o)
---
> _OBJS	= $(SRC:.c=.o) $(FSRC:.f90=.o)
106a107,111
> SIMFC=$(shell which gfortran)
> SIMFCFLAGS = -gdwarf-2 -Wall
> FC=$(shell which $(TRGT)gfortran)
> FCFLAGS = $(MCFLAGS) $(OPT) -gdwarf-2 -Wall
>
131c136
< pdc: simulator
---
> pdc: device simulator
140a146,153
> $(OBJDIR)/%.o : %.f90 | OBJDIR DEPDIR
> 	mkdir -p `dirname $@`
> 	$(FC) $(FCFLAGS) -c $< -o $@
>
> $(OBJDIR)/%_simulator.o : %.f90 | OBJDIR DEPDIR
> 	mkdir -p $(dir $@)
> 	$(SIMFC) $(SIMFCFLAGS) -c $< -o $@
>
153,154c166,167
< $(OBJDIR)/pdex.${DYLIB_EXT}: OBJDIR
< 	$(SIMCOMPILER) $(DYLIB_FLAGS) -lm -DTARGET_SIMULATOR=1 -DTARGET_EXTENSION=1 $(INCDIR) -o $(OBJDIR)/pdex.${DYLIB_EXT} $(SRC)
---
> $(OBJDIR)/pdex.${DYLIB_EXT}: OBJDIR $(FSRC:%.f90=$(OBJDIR)/%_simulator.o)
> 	$(SIMCOMPILER) $(DYLIB_FLAGS) -lm -DTARGET_SIMULATOR=1 -DTARGET_EXTENSION=1 $(INCDIR) -o $(OBJDIR)/pdex.${DYLIB_EXT} $(SRC) $(FSRC:%.f90=$(OBJDIR)/%_simulator.o)

At that point, the PDX can be built and run. It can be uploaded to hardware from the simulator.

# add alias for simulator
echo 'alias playdate_simulator="open ${HOME}/Developer/PlaydateSDK/bin/Playdate\ Simulator.app"' >> "${HOME}/.bash_profile"

# reload profile, if necessary
. "${HOME}/.bash_profile"

# build and run
PRODUCT="$(cat Source/pdxinfo | grep name | cut -d "=" -f 2-).pdx"
make
playdate_simulator "${PRODUCT}"

I wrote a "Fortran Playdate Development" blog post that contains a full guide. A complete copy of my project has also been included as a ZIP file.

FortranTest.zip (8.7 KB)

1 Like