3D: <glarea> and the three ways underneath it
react-x11 gives you a GL surface in the layout and nothing above it. A
scene graph — meshes, materials, lights, post-processing — is
@react-x11/components/three, which brings its own
reconciler and renders through the surface described here.
<glarea
style={{ flexGrow: 1 }}
clearColor="#0b1021"
frameLoop="always"
onCreated={(gl) => gl.Enable(gl.DEPTH_TEST)}
onDraw={(gl, { width, height }) => {
/* one frame */
}}
/>
examples/viewer3d.jsx is the worked example: a model viewer that orbits,
and the display-list discipline the indirect backend demands.
The three ways
<glarea> and onDraw are the same everywhere. What is underneath is not:
these are different APIs, not three spellings of one, and which one a
connection has decides what onDraw can do.
| direct (X11) | indirect (X11) | Cocoa | |
|---|---|---|---|
| how it draws | OpenGL ES 2 on the GPU, in two flavors: on Linux frames reach the server as dma-buf descriptors over DRI3 + Present, and on macOS/XQuartz the server exports the window's surface over Apple-DRI and CGL draws into it | GL commands encoded into the X connection | CGL — OpenGL 4.1 core on Metal — into IOSurface-backed framebuffers, presented as a sublayer |
| shaders | yes, GLSL ES 1.00 | none; the protocol encodes no shader objects | yes |
| render targets | yes — framebuffer objects | none; the protocol encodes no framebuffer objects | yes — it draws into one |
| geometry | vertex buffers on the GPU | immediate mode compiled into display lists | vertex buffers on the GPU |
| lighting | per fragment | per vertex | per fragment |
| cost per frame | one Present request | matrices, material state and one CallList per mesh | a swap between two targets; the WindowServer composites |
| where it runs | a local connection, plus ntk's optional x11-dri addon: a Linux server with DRI3, or macOS/XQuartz with Apple-DRI (ntk 8.4.0) | any server that allows indirect contexts, including over a network | any Mac, on the Cocoa backend — it needs the same optional x11-dri addon and nothing else |
The Cocoa column is the X11 direct path with the X-specific half swapped
out: the same x11-dri CGL context and the same WebGL-shaped gl table,
but instead of attaching to a surface an X server exported, frames render
into IOSurfaces and present by handing the surface's id to the area's own
CALayer. glPolicy: 'indirect' is the one setting that is an error there
and says so — GLX is a protocol, and there is no server to speak it to.
On X11 the default is indirect, because it is what react-x11 has always used. Turn the other on per app:
const root = await createRoot({ glPolicy: 'auto' });
'auto' uses direct where it is available and indirect otherwise, which is
usually what you want: most modern desktops refuse indirect GLX — Xorg
1.17 and later, and Xwayland, ship with it off — and those are exactly the
machines where direct works. 'direct' and 'off' are the strict forms, and
'indirect' is the default. One run can be switched without touching code:
NTK_GL_POLICY=direct npm start
Which flavor a connection got is app.glCapabilities().flavor — 'dri3' or
'appledri'. Both spell the context the same way, so nothing above the
policy has to branch on it; it is worth reading when a machine that should
have direct does not.
Everything about how the backend is chosen and why it might be unavailable —
the GLError codes, app.glCapabilities(), the addon — is ntk's, and is
documented in ntk's context-gles.md.
npm run labs:direct-gl is the shader path on a real display, and reports
both the flavor and which backend actually drew.
Raw GL through onDraw
onDraw(gl, { width, height, node }) hands you the context itself, and
there are two spellings of GL to write against — camelCase ES 2 against
PascalCase OpenGL 1.x. Nothing translates between them. Branch on
gl.backend, which is 'direct' or 'indirect'; the Cocoa path reports
'direct', because that is the API it hands you:
<glarea
onDraw={(gl) => {
if (gl.backend === 'direct') gl.clear(gl.COLOR_BUFFER_BIT);
else gl.Clear(gl.COLOR_BUFFER_BIT);
}}
/>
Code written against one spelling will not run on the other, which is why
the default X11 policy does not switch under an app that never asked for it
— and why examples/viewer3d.jsx reports which one it got rather than
pretending it can draw on both. It also means a scene written for the direct
API runs unchanged on the Cocoa backend, and one written for indirect GLX
does not run there at all.
On the indirect backend, geometry belongs in a display list. Every
immediate-mode vertex is a command on the wire, so a mesh re-sent per frame
costs kilobytes per frame while a compiled list costs one CallList. Names
are yours to choose — GenLists is a round trip, and this is the backend
where round trips are the thing to avoid.
Input
The pointer over a surface is the tree's, on every backend: onMouseDown,
onMouseMove, onMouseUp, onClick, onWheel and the rest fire at the
<glarea> — or at a child drawn over it (below) — and bubble, with
ev.localX/localY measured from the target's corner, which is what a
scene picks with. Nothing needs to listen on the surface's own window, and
nothing should: on X11 a listener there takes the event away from the tree
(elements.md says why).
2D over the surface
A <glarea>'s children are drawn above it — a legend, a toolbar, a label
over the scene — laid out in its box and hit before it. They are ordinary
elements. Core paints them on panes stacked over the surface, from the
window's own frame. On the Cocoa backend Core Animation composites the
panes with the GL frame, translucency and all. On X11 a pane is an opaque
child window, so what a child leaves unpainted shows the surface's
clearColor (elements.md has the table).
useSupports('glOverlay') asks whether a connection draws them;
examples/labs/gl-overlay.jsx runs one on both backends.
So a HUD is not GL's business: its text is set by the app's own text engine, its controls take their own input, and nothing is rasterized into a texture.
When there is no surface at all
onError(err) fires when no GL context could be made: no GLX, indirect
disabled, no matching visual. err.code is one of ntk's GLXError values,
GLX_INDIRECT_DISABLED being the usual one. Without a handler the failure is
a console warning and the element draws nothing, which looks like a bug in
your scene rather than a fact about the machine — so handle it and say what
the reader can do.
Testing
A GL app is tested on what it emits, not on pixels — which is also the
only option, because GL renders where GetImage cannot read it (see
glx.md):
test/glarea.test.jsdrives node-x11's in-process X server with its GLX emulator registered, so a frame's GL calls land on aRecordingBackend. No display, no GPU, no addon.test/viewer3d.test.jsdoes the same for the example, and says in its header which claim that harness cannot see and where it is measured instead.
See also glx.md for the indirect backend's design, and what its transport can never do.