Skip to content

Run SpecBAS on macOS and Linux, through SDL2 - #26

Open
Xalior wants to merge 12 commits into
ZXDunny:InDevfrom
Xalior:feat/SDL2
Open

Run SpecBAS on macOS and Linux, through SDL2#26
Xalior wants to merge 12 commits into
ZXDunny:InDevfrom
Xalior:feat/SDL2

Conversation

@Xalior

@Xalior Xalior commented Aug 11, 2026

Copy link
Copy Markdown

What this does

SpecBAS builds and runs on macOS and Linux, with the same editor, console,
windows, dialogs and graphics as the Windows build.

SpecBAS draws its screen into PixArray and needs four things from the host:
a window, a way to present that buffer, an event queue and a clock. Lazarus
supplies them on Windows. This branch adds an SDL2 implementation, selected by
the SDL2 compiler symbol.

Unit What it is
SP_SDL2Backend.pas window, renderer, texture, present
SP_SDL2Host.pas event loop, frame clock, mouse and keyboard delivery
SP_SDL2Keys.pas scancode mapping, shared by every SDL2 platform
SP_SDL2Compat.pas the Lazarus calls the SDL2 build would otherwise link
SpecBAS_SDL2.dpr the program file

The Windows build is untouched

Every change to an existing file is behind {$IFDEF SDL2} or an existing
platform conditional. A build without that symbol compiles the same text as
before.

Windows was not built or tested.

Where an existing file had to change, the commit message names the compile
error that forced it:

  • bass.pas -- {$IFDEF MACOS} is Delphi's symbol. Free Pascal defines
    DARWIN, leaving a const block dangling.
  • RunTimeCompiler.pas -- unconditional import of ActiveX for a GUID
    comparison SysUtils provides on every target.
  • Twelve units naming an LCL unit that does not exist without Lazarus. The
    imports are made conditional, not removed.
  • SP_Display.pas -- Windows display-mode management outside any conditional,
    and a uses branch missing units used unconditionally.
  • SP_Sockets.pas -- written against AddrInfo, fpGetAddrInfo, a bare
    socket() and FIONREAD, none of which Free Pascal 3.2.2 has where the
    file looks.

Building

Free Pascal 3.2.2, and SDL2 2.0.5 or later. No Lazarus, no lazbuild.

cd build-sdl2
make deps
make app

make deps fills vendor/, which is not tracked. It clones the Pascal
bindings at a fixed commit. On macOS it also downloads the SDL2 framework,
checks its SHA-256, and leaves it for make app to copy into the bundle.

On Linux SDL2 comes from the distribution: libsdl2-dev on Debian, Ubuntu and
Raspberry Pi OS, SDL2-devel on Fedora, sdl2 on Arch. make deps checks it
is installed and names the package if it is not.

make app produces SpecBAS.app on macOS and the executable on Linux.

The bundled framework is a fixed version because it must be universal, run on
Apple Silicon and pass codesign. Homebrew's sdl2 cannot do that job: it is
sdl2-compat, which reaches SDL3 by dlopen at run time, and it is built for
the host's own macOS version.

What has been run

  • macOS on Apple Silicon: builds, runs, runs the demos.
  • Linux on Raspberry Pi OS, aarch64, under labwc: builds, runs windowed and
    headless under SDL_VIDEODRIVER=dummy. A program was typed, run, and its
    output seen.

The demo set from the v0.980 release was used, the 3D and Graphics folders in
particular.

What is not done

  • Sound is silent. SpecBAS loads BASS by name at run time and sets
    SoundEnabled from the result, so it runs without one. No BASS library
    ships for macOS or Linux.
  • SAVE ... GIF writes nothing. fcl-image has a GIF reader and no writer.
    Loading works. A GIF save target takes the same do-nothing branch an
    unrecognised extension already takes.
  • MIDI on macOS takes the empty branch already there for platforms that are
    neither Windows nor Linux.
  • CFBundleIdentifier in the macOS bundle is a placeholder.
  • Windows is unverified.

The commits

Each builds, and each is one topic.

  1. Name Darwin where only MACOS was named
  2. Add the SDL2 symbol and the backend it selects
  3. Supply the two Lazarus calls the SDL2 build cannot link
  4. Do not reach for Lazarus where the SDL2 build has none
  5. Give every SDL2 platform one keyboard mapping
  6. Run SpecBAS on the SDL2 backend
  7. Give SP_Sockets the POSIX names Free Pascal has
  8. Build the SDL2 backend without Lazarus
  9. Release the display lock only where it was taken
  10. Take the version from the file the Windows build takes it from
  11. Answer one mouse position, in the space the events use
  12. Repaint the pointer and window shadows correctly under SDL2

Commit 12 is the only one that changes drawing code shared with Windows, and
it is inside {$IFDEF SDL2}. It fixes two things seen while dragging the
pointer over a window with a drop shadow:

  • A shadow is blurred from the buffer it is blended into, so a dirty rectangle
    covering part of one gives a different result from one covering all of it,
    and the difference accumulates.
  • The pointer's dirty rectangle started at MOUSEX, but the image is drawn at
    MOUSEX - MOUSEHSX. With a hotspot, set by MOUSE GRAPHIC ... POINT, the
    strip above and left of the pointer was never repainted.

Xalior added 12 commits August 11, 2026 12:13
Two units stop a Free Pascal compile before it reaches anything else.

bass.pas guards its macOS library name with {$IFDEF MACOS}. That is
Delphi's symbol; Free Pascal defines DARWIN and never MACOS, so on a Free
Pascal Apple target the const block ends up empty and bassdll is
undeclared for every import below it:

  bass.pas(849,1) Fatal: Syntax error, "identifier" expected but
  "FUNCTION" found

RunTimeCompiler.pas imports ActiveX unconditionally, for IsEqualGUID.
ActiveX is a Windows unit:

  RunTimeCompiler.pas(32,13) Fatal: Can't find unit ActiveX used by
  RunTimeCompiler

Free Pascal's SysUtils declares IsEqualGUID on every target, so off
Windows the import is all that has to go. Windows keeps it, and keeps
the ActiveX implementation with it.
SpecBAS.inc auto-defines OPENGL for Free Pascal on Windows. SDL2 is the
same shape for every other Free Pascal target: it says which host layer
this build gets. Delphi and Windows are untouched, so the three-way split
the source already carries stays as it is.

SP_SDL2Backend is that host layer. It owns the window, the renderer and
the streaming texture SpecBAS's frame buffer is uploaded into, and
answers the clipboard, the display geometry and the clock. It knows
nothing about SpecBAS beyond the address and pitch of the buffer it
presents.

Nothing selects it yet.
The SDL2 build links no Lazarus, so ClipBrd's Clipboard.AsText and
LazUtils' CopyFile are not there to be called. SP_EditUnit, SP_MemoUnit,
SP_FileIO and SP_Interpret_PostFix each reach for one of them by name.

Both are answered here under the names and shapes those units already
use, so nothing about the calls has to change: the clipboard from SDL2,
and the copy over TFileStream, replacing an existing destination and
carrying the source's modification time across when asked to.

Nothing selects it yet.
The SDL2 build links no Lazarus, so a unit that names an LCL unit in its
uses clause stops the compile there. Free Pascal on Windows keeps every
one of them, and so does Delphi.

  SP_Interpret_PostFix.pas(28,16) Fatal: Can't find unit Forms used by
  SP_Interpret_PostFix

Three of them are replaced, by SP_SDL2Compat where the call has to keep
working and by nothing where the import was never used:

  ClipBrd, in SP_EditUnit, SP_MemoUnit and SP_Interpret_PostFix, for
  Clipboard.AsText.
  FileUtil, in SP_FileIO and SP_Interpret_PostFix, for CopyFile. The one
  qualified call site needs the qualifier to follow.
  Forms, LCLType, LclIntf, GraphUtil and Dialogs, in the remaining files
  here, are imported and never called.

SP_DebugPanel and SP_Components carry ClipBrd on both sides of an
existing {$IFNDEF FPC}, so the comma moves inside the conditional where
the item can now be absent.
SpecBAS names keys by the K_ constants in SP_Input.pas, which the editor,
the widget set, KEYSTATE and INKEY$ all index by. SDL2 reports a scancode
instead, so one of the two has to be turned into the other, and it is
done in one place for every platform this backend covers.

The mapping runs from the scancode rather than the keycode because a
scancode names the physical key, which is what a K_ constant means as
well. Which character that key produces is the layout's business and
arrives separately, on the SDL_TEXTINPUT event.

SpecBAS reads K_CONTROL for copy, cut, paste and select-all. macOS puts
those on Command, so MAC_COMMAND_IS_CONTROL has Command deliver
K_CONTROL as well and both keys work; it is a conditional inside the one
mapping and Linux and Windows are unchanged by it.

Nothing selects it yet.
SP_SDL2Host is what MainForm.pas is to the Lazarus build: the one
platform file. It creates the window, starts the interpreter thread,
fills in the CB_ callback table, turns SDL2 events into SpecBAS's own
input, and owns the main loop that Application.OnIdle drives on Windows.
It exports the names MainForm.pas exports, so a unit that wants the host
picks between the two in its uses clause and changes nothing else.
SpecBAS_SDL2.dpr is its entry point, as SpecBAS.dpr is Lazarus's.

Pictures load and save through Free Pascal's own fcl-image, because
there is no LCL here to supply TBitmap, TPortableNetworkGraphic,
TJPEGImage or TGIFImage. The clock is SDL2's performance counter: Free
Pascal 3.2.2 declares no clock_gettime on these targets, which is what
MainForm.pas's UNIX branch calls, and SDL_GetTicks counts whole
milliseconds, which is too coarse to pace a frame with.

SP_Display.pas is the file that would not compile at all. Every error in
it is Windows display-mode management outside any conditional:

  SP_Display.pas(1010) StretchBlt
  SP_Display.pas(1132) SystemParametersInfo, SPI_GETWORKAREA
  SP_Display.pas(1185..1341) EnumDisplaySettings, ChangeDisplaySettings,
                             ChangeDisplaySettingsEx, GetMonitorInfo,
                             EnumDisplayDevices, TDeviceMode, TMonitor

Those become SDL_RenderCopy, SDL_GetDisplayUsableBounds,
SDL_GetDisplayBounds, SDL_GetCurrentDisplayMode and
SDL_SetWindowFullscreen. Its UNIX uses branch also omitted SysUtils,
SyncObjs and Types, which the file uses unconditionally for
TCriticalSection, TRect and PtInRect, and the frame buffer and the
window size were declared for the OpenGL path alone.

Fullscreen is desktop fullscreen. The renderer already stretches the
logical screen to whatever it is given, so there is nothing to gain from
switching the monitor's mode.

SP_BankManager and SP_UITools name MainForm in a uses clause and follow
it here.

Building the whole environment needs -O1. Free Pascal 3.2.2 for aarch64
raises internal error 200510011 on SP_FPEditor.pas at -O2.
The POSIX half of the unit is written against calls Free Pascal 3.2.2
does not have:

  SP_Sockets.pas(181,12) Error: Identifier not found "AddrInfo"
  SP_Sockets.pas(193,6)  Error: Identifier not found "fpGetAddrInfo"
  SP_Sockets.pas(271,11) Error: Identifier not found "socket"
  SP_Sockets.pas(718,45) Error: Identifier not found "FIONREAD"

There is no fpGetAddrInfo, no AddrInfo type and no fpFreeAddrInfo, so
name resolution goes through netdb's ResolveHostByName, with
StrToNetAddr answering a dotted quad without a lookup. StrToNetAddr is
the network-order helper, which is the order netdb answers in; the
host-order one beside it would reverse the octets and connect to the
wrong machine.

The socket entry points are named fpSocket, fpBind and so on. Only
socket() is named in the code shared with WinSock, so it gets a name of
its own here, as InitWinSock already does.

FIONREAD is declared in termio.

Nothing here is reached by the Windows build, which keeps WinSock2.
src/build.bat drives Delphi, and SpecBAS.lpi drives Lazarus. Neither
builds this, so build-sdl2/ does: fetch-deps.sh puts the Pascal bindings
and, on macOS, the SDL2 release framework into a gitignored vendor/, and
the Makefile calls fpc directly.

make app assembles a macOS application bundle with SDL2 inside it, which
runs on a machine carrying no toolchain and no system SDL2. That is how
SpecBAS already ships BASS on Windows, and it keeps a proprietary
library alongside a GPLv3 application without linking it in. Every
comment in these three files is there because the fact it records is not
visible from the command it sits above.

Free Pascal 3.2.2 needs no -k-ld_classic here: with Lazarus gone it
emits no Objective-C, so there is no metadata for Xcode 26 to reject.
HandleMouseMotion takes DisplaySection with TryEnter and released it after
the block that acquires it rather than inside, so a failed acquire was still
followed by a release. TryEnter only fails while another thread holds the
lock, which is why it killed the program on roughly half of the attempts and
looked like a crash on a mouse click.

Releasing a mutex this thread does not own is an error pthreads reports,
which Free Pascal turns into runtime error 236 and SysUtils raises as
EThreadError. Nothing on the path catches it, so the process printed two
"EThreadError: Thread error" traces and stopped.
BUILDSTR was the literal '0.0.0.0', so the title bar read "SpecBAS v 0.0.0.0"
where the Windows one reads the real version. Windows gets it from the
executable's own VERSIONINFO resource at run time; there is no such resource
here, and no way to read one back.

build-sdl2/Makefile now reads src/SpecBAS.rc, which is where that resource is
written, and generates SpecBAS_Version.inc for the unit to include. The file
stays the project's single statement of its own version, and both builds
report the same number: FILEVERSION 0,0,0,1511 shows as 0.1511 on each.
SDLB_GetMousePos derived the pointer from the global mouse position minus the
window position, while motion events arrive in SDL's window-relative space.
SP_Display.HandleMouse takes MOUSEX and MOUSEY from the first every frame and
the host marks the area to repaint from the second, so where the two disagreed
the pointer was drawn outside the rectangle redrawn for it.

The global position is still what decides whether the pointer is inside the
window, because HandleMouse needs to notice it leaving and SDL_GetMouseState
cannot report that. Once inside, the position answered is SDL's own.
Two drawing faults showed up while dragging the mouse pointer over a window
with a drop shadow.

A shadow is blurred from the pixels around it and blended into the same buffer
it reads, so a dirty rectangle covering part of one gives a different result
from one covering all of it. The buffer keeps the previous frame, so the
difference accumulates and the area darkens and smears. Any rectangle that
touches a shadow is now widened to that whole shadow before the fragments are
generated.

The pointer's dirty rectangle started at MOUSEX, but the image is drawn at
MOUSEX - MOUSEHSX. With a hotspot, set by MOUSE GRAPHIC ... POINT, the strip
above and to the left of the pointer was drawn and never repainted. The
rectangle now starts where the image does.

Both are in code the Windows build compiles, so both changes are inside
{$IFDEF SDL2} and the Windows path selects what it did before.
@what-the-diff

what-the-diff Bot commented Aug 11, 2026

Copy link
Copy Markdown

PR Summary

  • Improved Source Code Organization

    • Introduced entries in .gitignore to prevent tracking of build artifacts for the SDL2 backend to keep the repository clean.
    • Added a Makefile that serves as a tool for building the SpecBAS project and helps in managing dependencies, building executable files, and cleaning up.
    • New scripts like bundle.sh and fetch-deps.sh have been added for setting up macOS application bundle and downloading necessary SDL2 dependencies respectively.
  • Code Modifications for SDL2 Backend

    • Several files such as RunTimeCompiler.pas, SP_BankManager.pas, SP_CheckBoxUnit.pas, SP_Components.pas and SP_DebugPanel.pas have been modified to adopt conditional logic based on the SDL2 flag, enabling the SDL2 backend.
    • New files like SP_SDL2Backend.pas, SP_SDL2Compat.pas, and SP_SDL2Keys.pas have been added to support functionalities, compatibility, and keyboard mapping for SDL2 backend.
    • SP_Sockets.pas was updated for better POSIX compatibility and host resolution functionality, which eliminates the dependence on fpGetAddrInfo.
  • Enhanced Platform-Specific Support

    • Multiple files including SP_Tokenise.pas and SP_UITools.pas were updated to conditionally include certain features based on SDL2 definition to facilitate platform-specific behaviors.
    • New definitions for platform-specific behaviors have been added in SpecBAS.inc.
    • A new file named SpecBAS_SDL2.dpr has been introduced as an entry point for the SpecBAS interpreter with an SDL2 backend, which offers a custom main loop without a GUI.
    • bass.pas has been tweaked to ensure correct definition of bassdll for macOS, providing better support for this platform.

These changes contribute to improving the robustness of the SpecBAS project by enabling a new SDL2 backend, improving the organization of source code, enhancing platform compatibility, and refining application build and setup processes.

@Xalior Xalior mentioned this pull request Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant