475. SDL_GPU is bound for :core and :gpu, and tested on the first thread
Date: 2026-09-24
Status
Accepted. The first decisions of M4 (docs/gpu-plan.md): D6 as built, and
what phase 1 found about where a GPU device can exist. Amends
ADR-0280 as
ADR-0461 did.
Context
M4 needs SDL_GPU from Java. SDL_GPU is already inside libgoldberry,
because SDL 3.4.16 is built with its defaults (ADR-0003). But none of its 97
functions was exported, and :natives had no binding for any of them.
Phase 1 of the plan binds the first surface: a device, textures, transfer buffers, command buffers, a render pass that clears, a copy pass that uploads and downloads, and fences. Its exit criterion is a texture cleared to a known colour that downloads byte for byte.
Three facts were found in SDL’s source on the way, and each changes something:
- A device needs the video subsystem, and a video driver that can make a
Metal view or a Vulkan surface.
SDL_CreateGPUDeviceWithPropertiesasks the current video device (SDL_gpu.c). The Metal driver’sPrepareDriverneedsMetal_CreateViewand the Vulkan driver’s needsVulkan_CreateSurface. SDL’sdummyvideo driver, which every headless test in the project runs under, has neither, so under it there is no device at all. Theoffscreendriver has a headless Vulkan surface, so lavapipe works under it with no display. On macOS, onlycocoahas a Metal view. cocoastarts only on the process’s first thread (Cocoa_CreateDevicereturns NULL otherwise). That is ADR-0039’s failure, “No available video device”, met again in a place where the flag cannot be passed: Gradle’sTesttask runs tests on a worker thread however its JVM is started.- The exports cost almost nothing.
libgoldberryfor macos-aarch64 was 6,100,832 bytes without the 29 new symbols and 6,102,848 with them: 2 KB. The GPU drivers were already linked, reached through SDL’s renderer, which backs the window surface where a platform has no framebuffer (ADR-0046). The size risk in the plan’s phase 0 is answered for this target.
Decision
Bind SDL_GPU in :natives by the rules every other library follows, and
seal it to the two modules M4 builds on it.
- The functions are holders in
natives.sdl.calls(ADR-0173):SdlGpuDeviceCalls,SdlGpuResourceCallsandSdlGpuCommandCalls, plusSdlPropertiesCallsfor the property groups a device is configured with. They are exported ingoldberry.symbolsonly as they gain a caller, soExportListTestholds. - Their six structs and 23 enumerators are on the shim’s layout table and in
Layouts/NativeConstants, so the C compiler checks every offset and value. The ABI version is 16. - The wrappers are in
natives.sdl.gpu:SdlGpuDevice,SdlGpuTexture,SdlGpuTransferBuffer,SdlGpuCommandBufferwith itsCopyPass, andSdlGpuFence.- No public signature carries a
MemorySegment. - SDL’s rules are checked in Java before SDL is called: one pass at a time, a command buffer used once, unmapped buffers in a copy pass, regions inside their texture, and resources of the same device.
- A closed resource fails in Java, naming itself. A device releases whatever is still open when it closes.
- Mapped memory is a
ByteBufferscoped to an arena thatunmapcloses, so holding it too long throws rather than reading freed memory.
- No public signature carries a
natives.sdl.gpuis exported to:coreand:gpu, and to nobody else.:corewill claim windows and present;:gpuis the public API. It is the first package sealed to two readers, andExportedSurfaceTestnow holds a sealed package to a set of readers.
Run the GPU tests on the first thread. They are tagged gpu, and the
ordinary test task leaves them out. :natives:gpuTest runs them instead: a
JavaExec with -XstartOnFirstThread on macOS, whose main
(GpuTestLauncher) asks JUnit to run them on the calling thread. It is part
of check.
- Without a device they skip.
-Pgoldberry.gpu.required=truemakes that a failure, which is ADR-0016’s rule applied to a device. -Pgoldberry.gpu.videoDrivernames the video driver:offscreenfor lavapipe on a runner with no display.- A launch that finds no tests fails.
Alternatives considered
- Vulkan through MoltenVK under
offscreenon macOS. It needs no first thread, but it tests a driver no Mac user runs, and needs MoltenVK installed. Rejected: the macOS path is Metal. - Dispatching SDL’s video initialisation to the main queue from the test
thread. The
javalauncher parks the first thread in a run loop, so a block sent there would run. But SDL’s video would then belong to a thread the tests never run on, and every later call would cross threads. Rejected for what it would teach the code under test. - One export per SDL_GPU function up front (the plan’s ~60). Rejected by
ExportListTest’s rule: a symbol nothing binds is dead weight, and the rest arrive with the phases that call them.
Consequences
- Phase 1’s exit is met on Metal: 16 device tests pass under Metal’s API
validation. They cover the clear and download, a region uploaded and read
back, 16-bit planes, and every rule above. Layouts and constants are
verified by
LayoutVerificationTest. Lavapipe waits for the GPU lane. - A readback in a headless test needs
offscreenorcocoa, notdummy. The plan’s “headlessGpuSurfacein readback mode” (D5) is corrected: the golden tests that render GPU content run in agpuTest-style task under a GPU-capable driver. The CPU goldens keepdummyandOffscreen, unchanged. - The size gate needs no allowance for GPU exports on macOS. Linux and Windows are measured when their builds run.