Skip to content

darwin: native Cocoa 2D graphics driver (no XQuartz, no OpenGL) - #480

Draft
agorangetek wants to merge 13 commits into
freebasic:masterfrom
agorangetek:darwin-cocoa-gfx
Draft

agorangetek wants to merge 13 commits into
freebasic:masterfrom
agorangetek:darwin-cocoa-gfx

Conversation

@agorangetek

@agorangetek agorangetek commented Sep 18, 2026

Copy link
Copy Markdown

Makes a plain ScreenRes open a real macOS window with the 2D primitives,
font, palette modes and input working — with no XQuartz install and no OpenGL
(deprecated on macOS since 10.14).

Stacked on #479 — the first 8 commits are that PR (the toolchain fixes that
make a darwin-aarch64 build possible at all); the Cocoa work is the last 4.
I'll rebase onto master once #479 lands; until then the extra commits are
unavoidable here. Only src/gfxlib2/darwin/*, src/rtlib/darwin/*, the
makefile .m rules, the framework linking and the driver registration are new.

Why not X11

Upstream's only Darwin graphics backend is XQuartz/X11. It requires an extra
~100 MB install, its GLX path is OpenGL, and it was only ever the legacy route
on this platform, so this branch removes it from the Darwin build entirely:
the makefile always builds Darwin with DISABLE_X11/DISABLE_OPENGL, and fbc
no longer links -lX11/-lXext/-lXpm/-lXrandr/-lXrender. The X11 driver
source is untouched, since it is the main backend on Linux/BSD.

Relationship to #448

#448 (Markos-Th09) adds a Cocoa OpenGL driver, and its scaffolding —
window/application setup, event pump, NSWindow handling and the
fb_private_scancodes_cocoa.h keyboard table — is reused here, credited in the
commit. But it cannot replace X11 on its own: its init starts with

if (!(flags & DRIVER_OPENGL))
    return -1;

so it only claims screens that pass FB.GFX_OPENGL, and its flip goes
through fb_hGL_SetupProjection() + flushBuffer. A plain ScreenRes
renders into FreeBASIC's software framebuffer and is never presented — which is
exactly the open question in that PR ("am I supposed to see nothing for Line or
Circle?").

This branch presents the software framebuffer instead: a refresh thread
converts it with the gfxlib2 blitter (fb_hGetBlitter) into a CoreGraphics
bitmap and publishes the image to the window's layer. AppKit is only touched on
the main thread, from the driver hooks the core already calls
(flip/unlock/poll_events/wait_vsync) — an FB program owns the process
main thread and there is no NSApplication run loop. I did not take #448's
computed-goto rewrite (rversteegen's review called the label→function-pointer
cast unsafe; the -e codegen fix in #479 addresses that instead) or its
profiler change.

Validation

On macOS 27 / Apple M5, with /opt/X11 absent and DISPLAY unset:

  • ScreenRes 640,480,32 succeeds (err = 0, valid ScreenPtr); Line,
    Circle, PSet/Point, Paint (flood fill), Draw (graphics language),
    Draw String, Get/Put image blitting, rgba() alpha blending (verified
    numerically: 50% red over green gives &h807F6400) and the built-in font all
    render correctly, and the window is pixel-identical to the framebuffer
    (BSave)
  • 16bpp and 8bpp-with-palette screens work too
  • animation updates continuously with no ScreenSync/ScreenUnlock
  • real input works: a key press reaches InKey (log shows
    isActive=1 isKeyWindow=1, keyDown keyCode=4 scancode=35 translated=104),
    mouse motion and clicks reach GetMouse, and closing the window posts
    EVENT_WINDOW_CLOSE + KEY_QUIT
  • the full fbcunit suite is unchanged: 1,151,705 of 1,151,707 assertions
    pass with this driver compiled in (the 2 failures are the pre-existing
    acos(-1.0f) constant-folding ULP difference)
  • a gfx program links -lfb -lfbgfx -lncurses -framework Cocoa -framework QuartzCore -framework CoreGraphics and has zero X11 dylib dependencies

FBCOCOA_DEBUG=<file> logs the raw AppKit events and the app/window activation
state, which is what pinned down the input bugs.

Known limitations

  • No FB.GFX_OPENGL screens and no fullscreen on Darwin any more (XQuartz/GLX
    used to provide them); ScreenRes with FB.GFX_OPENGL fails with illegal
    function call. A GL/Metal path would be a separate, larger piece.
  • Window resize while a program runs is not handled.
  • Event-queue paths beyond InKey/GetMouse (GetKey, MultiKey,
    ScreenEvent, ScreenControl queries) are untested.
  • Synthetic input (CGEventPost) cannot be used to test this: macOS requires
    Accessibility permission for it. The verification above used real input.

FB's OpenGL support is implemented in the X11 gfx driver.  Without
ENABLE_XQUARTZ that driver is compiled out, but gfx_opengl.c is still
built and calls fb_hGL_GetProcAddress(), which only the driver defines.
libfbgfx.a therefore ends up with an undefined symbol and every link
fails:

    Undefined symbols for architecture arm64:
      "_fb_hGL_GetProcAddress", referenced from:
          _fb_GfxGetGLProcAddress in libfbgfxmt.a[31](gfx_opengl.o)

gfx_opengl.c already provides a stub for exactly this situation; define
DISABLE_OPENGL alongside DISABLE_X11 so it is used.
rtlErrorCheck() and rtlErrorThrow() only have a resume label to hand to
the error throw when -ex/-exx enabled RESUME support.  Without it the
label arguments are NULL, fb_ErrorThrowAt() can never return a usable
jump target, and `fb_ErrorThrow(); goto *result;` is equivalent to a
plain call.

Emitting the call also keeps the generated C compilable by clang, which
rejects `goto *ptr;` in a function that contains no address-of-label
expression ("indirect goto in function with no address-of-label
expressions").

Code built with -ex/-exx is unaffected: there the label addresses are
taken anyway (&&label), so the indirect jump is still emitted.
DATA items are emitted as a packed { short, void* } table, 10 bytes per
entry, so the embedded string/link pointers sit at 2-byte offsets.
ELF linkers accept that, but Mach-O's ld64 refuses to relocate a
pointer that is not pointer-aligned and fails the link:

    ld: pointer not aligned in '_label$N'+0x16

Give the descriptor the natural pointer alignment when targeting
Darwin, and make fb_data.h use the matching, non-packed layout.  Other
targets keep the packed 10-byte layout so their ABI is unchanged.
AArch64 Darwin uses a plain pointer-like va_list (sizeof(va_list) == 8),
not the AAPCS64 __va_list_tag struct used by Linux/BSD aarch64.  The
compiler assumed the struct, so cva_start()/cva_arg() walked the wrong
memory and any program using cva_list crashed immediately.
Mach-O has no __start_/__stop_ section boundary symbols and limits
section names to 16 characters, so the ELF-style section scan cannot
work there.  Guard it for Darwin and fall back to the single version
record (the report is then simply empty) instead of emitting an invalid
section attribute and failing to build.
The Darwin link command was built for a GNU ld: it adds crt1.o/crti.o/
crtbegin.o/crtend.o/crtn.o, `-macosx_version_min 10.4`, `--eh-frame-hdr`,
`--export-dynamic` and `-lgcc`, and uses `-shared -h<name>` for shared
libraries.  Modern ld64 rejects --eh-frame-hdr outright, the crt objects
cannot be linked that way on macOS, and libgcc does not exist unless a
GCC toolchain happens to be installed.

Invoke the C compiler driver (clang) for linking Darwin targets instead:
it supplies the startup object and libSystem itself.  Drop the options
it does not accept, use -dynamiclib/-install_name for -dylib, and add
-arch arm64 so the architecture is explicit.

Executables and dynamic libraries now link with the system toolchain
alone.
Xfuncproto.h defines NeedWidePrototypes to 1 unless the application
defines NARROWPROTO, which makes Xlib declare XGetKeyboardMapping()'s
first_keycode parameter as unsigned int instead of KeyCode (unsigned
char).  FB's XGETKEYBOARDMAPPING typedef hard-codes KeyCode, so passing
XGetKeyboardMapping to fb_hInitX11KeycodeToScancodeTb() no longer
matches the parameter type:

    gfx_x11.c:588:68: error: incompatible function pointer types
      passing 'KeySym *(Display *, unsigned int, int, int *)' to
      parameter of type 'XGETKEYBOARDMAPPING' (aka
      'unsigned long *(*)(struct _XDisplay *, unsigned char, int, int *)')

GCC only warns about the mismatch, which is why this went unnoticed, but
clang (and GCC 14+, where it is an error too) rejects it -- so the X11
rtlib/gfxlib2 code cannot be built by clang at all.  That makes the
XQuartz/X11 graphics path unbuildable on Darwin, where clang is the
system compiler.

Mirror Xlib's own conditional in the typedef so it matches either way.
uname -m reports "arm64" on Apple Silicon, which matched the arm% pattern
and was normalized to "arm" -- the 32-bit family.  A plain "make" on macOS
therefore resolved FBTARGET to darwin-arm instead of darwin-aarch64, so
objects and libraries landed in directories that do not match the ones fbc
looks for at run time (lib/freebasic/darwin-aarch64).

Normalize aarch64/arm64 first, then let the remaining 32-bit spellings
match armv%/arm.  Verified that arm-linux-gnueabihf and
armv7-linux-gnueabihf still resolve to linux-arm, and that
arm64-apple-darwin resolves to darwin-aarch64.

Based on the corresponding change in freebasic#470 by @metaneutrons.
Two defects on the -dylib path, both found while cross-checking metaneutrons'
freebasic-ng#160, which reviewed this branch against their fork:

- Shared libraries were named .so on every unix target, including Darwin.
  Mach-O uses .dylib -- .so is the loadable bundle extension. fb_DylibLoad()
  looks for the .dylib name first on Darwin, so it worked, but only by falling
  through to the .so entry in its candidate list, and the file was named wrongly
  for every other tool on the platform. Darwin now has its own case in the
  output naming, leaving the ELF targets on .so.

- The link line skipped --export-dynamic on Darwin entirely rather than
  translating it. The clang driver wants it spelled -Wl,-export_dynamic.
  Symbols resolved without it -- Mach-O exports global symbols by default --
  but leaving it out was a silent difference from every other unix target, and
  -export therefore did nothing on Darwin.

Verified by building a library and loading it back:

    fbc -dylib foo.bas                -> libfoo.dylib, install name "libfoo"
    linking: clang ... -dynamiclib -install_name "libfoo" -Wl,-export_dynamic
    DyLibLoad("foo") -> handle, DyLibSymbol(handle, "FOO_ADD") -> 5
Plain ScreenRes now opens a real macOS window with all the 2D primitives,
without XQuartz and without OpenGL (which is deprecated on macOS).

Backends and the missing piece:
- Upstream's only Darwin backend is X11 via XQuartz, and the Cocoa driver in
  freebasic#448 is OpenGL-only: its init returns -1 unless DRIVER_OPENGL is set, so a
  plain ScreenRes renders into the software framebuffer and never presents it
  (the author's open question about Line/Circle showing nothing).
- This driver presents that software framebuffer instead: a refresh thread
  converts it with the gfxlib2 blitter (fb_hGetBlitter) into a CoreGraphics
  bitmap and publishes the image to the window's layer, and AppKit itself is
  only touched on the main thread, from the driver hooks the core calls
  (flip/unlock/poll_events/wait_vsync) since an FB program owns the main
  thread and there is no NSApplication run loop.

Window/event/scancode scaffolding is adapted from freebasic#448 by Markos-Th09,
including src/rtlib/darwin/fb_private_scancodes_cocoa.h; the software present
path and the driver hooks are new.

Supporting changes:
- makefile: compile .m sources (OBJC, LIBFBGFX_M and its PIC/MT variants)
- fbc: link -framework Cocoa -framework QuartzCore -framework CoreGraphics
  for Darwin programs that use gfx
- gfx_unix.c: register the driver after X11, so a build with XQuartz keeps
  using X11 and falls back to Cocoa when no X server is reachable, while a
  build without XQuartz gets Cocoa as its only driver

Verified on macOS 27 / Apple M5: ScreenRes 640,480,32 succeeds (err = 0,
ScreenPtr valid), Point() readback matches the drawn colours, BSave is
correct, and the window renders Circle/Line/Draw String in the right colours.
- InKey() was dead: it reads the key buffer filled by fb_hPostKey(), which
  this driver never called (only fb_hPostEvent, which GetKey() and the event
  queue use).  Feed both, and translate NSEvents the way the X11 driver does:
  plain characters as-is, 0x7F remapped to FB's extended KEY_DEL, and
  everything else through fb_hScancodeToExtendedKey().
- GetMouse() no longer returns -1 when another application is frontmost.
  Gating it on the window being key meant a program polling the mouse while
  unattended saw nothing at all.
- Report window close (NSWindowWillCloseNotification) as EVENT_WINDOW_CLOSE
  plus KEY_QUIT, matching what ALT+F4/CLOSE does on other platforms.
- Only report mouse motion that is inside the window: unlike X11, this driver
  receives motion for the whole application, so out-of-window coordinates
  (often negative) were being reported as if they were in-window.

Verified so far: real mouse motion and clicks reach GetMouse() and are
correctly converted; animation updates continuously with no ScreenSync/
ScreenUnlock; 8bpp palette and 16bpp modes render correctly.
Setting FBCOCOA_DEBUG=<file> logs the raw AppKit events the driver receives
plus the NSApplication/window activation state, which is the quickest way to
tell an input *delivery* problem from a *translation* problem.  Costs nothing
when the variable is unset.

It immediately answered the open question here: with a real key press the log
shows

    state isActive=1 isKeyWindow=1
    event type=10
      key keyCode=4 scancode=35 down=1
      translated key=104

and InKey() returns it.  (Synthetic CGEventPost input cannot be used to test
this: macOS requires Accessibility permission for it.)
The native Cocoa driver supersedes it: XQuartz means an extra ~100 MB install
for users, its GLX path is OpenGL (deprecated on macOS since 10.14), and X11
was only ever the legacy compatibility route on this platform.

- makefile: Darwin always builds with DISABLE_X11 and DISABLE_OPENGL, so the
  X11 and GLX drivers are compiled out and /opt/X11 is no longer needed at
  build time either
- fbc: no longer adds /opt/X11/lib, nor -lX11/-lXext/-lXpm/-lXrandr/-lXrender,
  for Darwin programs; the other unix targets keep their X11 libraries

Verified after a clean rebuild: libfbgfx contains no X11/XOpenDisplay symbols,
a gfx program links as

    -lfb -lfbgfx -lncurses -framework Cocoa -framework QuartzCore -framework CoreGraphics

has zero X11 dylib dependencies, and renders correctly with no DISPLAY and no
XQuartz installed.

Note: FB.GFX_OPENGL screens are no longer available on Darwin (XQuartz/GLX used
to provide them); ScreenRes with that flag now fails as it does on a build with
no GL driver.
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