darwin: native Cocoa 2D graphics driver (no XQuartz, no OpenGL) - #480
Draft
agorangetek wants to merge 13 commits into
Draft
agorangetek wants to merge 13 commits into
agorangetek wants to merge 13 commits into
Conversation
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.
agorangetek
force-pushed
the
darwin-cocoa-gfx
branch
from
September 19, 2026 09:04
700a656 to
e50361d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes a plain
ScreenResopen 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-aarch64build 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/*, themakefile
.mrules, 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 fbcno longer links
-lX11/-lXext/-lXpm/-lXrandr/-lXrender. The X11 driversource 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,
NSWindowhandling and thefb_private_scancodes_cocoa.hkeyboard table — is reused here, credited in thecommit. But it cannot replace X11 on its own: its
initstarts withso it only claims screens that pass
FB.GFX_OPENGL, and itsflipgoesthrough
fb_hGL_SetupProjection()+flushBuffer. A plainScreenResrenders 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 CoreGraphicsbitmap 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 processmain thread and there is no
NSApplicationrun loop. I did not take #448'scomputed-goto rewrite (rversteegen's review called the label→function-pointer
cast unsafe; the
-ecodegen fix in #479 addresses that instead) or itsprofiler change.
Validation
On macOS 27 / Apple M5, with
/opt/X11absent andDISPLAYunset:ScreenRes 640,480,32succeeds (err = 0, validScreenPtr);Line,Circle,PSet/Point,Paint(flood fill),Draw(graphics language),Draw String,Get/Putimage blitting,rgba()alpha blending (verifiednumerically: 50% red over green gives
&h807F6400) and the built-in font allrender correctly, and the window is pixel-identical to the framebuffer
(
BSave)ScreenSync/ScreenUnlockInKey(log showsisActive=1 isKeyWindow=1,keyDown keyCode=4 scancode=35 translated=104),mouse motion and clicks reach
GetMouse, and closing the window postsEVENT_WINDOW_CLOSE+KEY_QUITpass with this driver compiled in (the 2 failures are the pre-existing
acos(-1.0f)constant-folding ULP difference)-lfb -lfbgfx -lncurses -framework Cocoa -framework QuartzCore -framework CoreGraphicsand has zero X11 dylib dependenciesFBCOCOA_DEBUG=<file>logs the raw AppKit events and the app/window activationstate, which is what pinned down the input bugs.
Known limitations
FB.GFX_OPENGLscreens and no fullscreen on Darwin any more (XQuartz/GLXused to provide them);
ScreenReswithFB.GFX_OPENGLfails with illegalfunction call. A GL/Metal path would be a separate, larger piece.
InKey/GetMouse(GetKey,MultiKey,ScreenEvent,ScreenControlqueries) are untested.CGEventPost) cannot be used to test this: macOS requiresAccessibility permission for it. The verification above used real input.