Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5ca712e
Add experimental Linux direct display and touch backends
monofuel Oct 22, 2026
e9ed10a
Speed up X11 CPU pixel uploads with packed row writes
monofuel Oct 28, 2026
83699dd
Speed up standard 32-bit framebuffer uploads
monofuel Oct 28, 2026
0c34bd7
Use Xlib int return types for event predicates
monofuel Oct 28, 2026
799e062
Support vblank page flips for DRM pixel windows
monofuel Oct 29, 2026
1f1ca15
Add standalone GBM EGL paired DRM presentation probe
monofuel Oct 30, 2026
389055d
Enumerate all EGL configs for the requested scanout format
monofuel Oct 30, 2026
0047e15
Finish GPU work after EGL resolves the presentation buffer
monofuel Oct 30, 2026
d260ef8
Keep synthetic quad vertices in a GLES buffer
monofuel Oct 30, 2026
627c7aa
Release unused CPU scanout buffers before the GPU proof
monofuel Oct 30, 2026
ad67736
Match the CPU reference scaler loop specialization
monofuel Oct 30, 2026
b39f864
Select unmultisampled EGL buffers and record DS Plus GPU results
monofuel Oct 30, 2026
014cc16
Confirm manual acceptance of both GPU display layouts
monofuel Oct 30, 2026
37f8d12
Emit X11 wheel scrolling only on button press
monofuel Oct 29, 2026
1db2ef8
Label Linux cpuTime measurements as main-thread time
monofuel Oct 30, 2026
24d59a5
Add explicit paired DRM GPU pixel presentation
monofuel Oct 30, 2026
488b614
Add opt-in X11 GPU pixel presentation with GLX vsync
monofuel Oct 31, 2026
e4280e7
Organize Linux pixel APIs and simplify display documentation
monofuel Nov 11, 2026
2e18c03
Remove separate Linux display guides
monofuel Nov 11, 2026
6eb3f45
Move fbdev backend directly under Linux
monofuel Nov 11, 2026
b39b204
Keep Linux backend internals behind their entry modules
monofuel Nov 11, 2026
1ce3db3
Merge upstream master and preserve Linux display and GPU paths
monofuel Nov 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jobs:
# Run tests.
- run: nim c tests/test.nim
- run: nim c tests/manual_inputs.nim
- name: Check 32-bit fbdev without DRM
if: matrix.os == 'ubuntu-latest'
run: WINDY_TEST_BACKENDS=fbdev WINDY_TEST_TARGET=arm nim check tests/test_linux_display.nim
- run: nim r tests/test_openurl.nim
- run: nim r tests/test_macos.nim
if: matrix.os == 'macos-latest'
Expand Down
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.PHONY: test-native test-linux-cpu test-linux-combined test-x11-pixels
test-x11-pixels:
nim r tests/test_x11_pixels.nim
test-native:
nim r tests/test_native_input.nim
nim r tests/test_linux_framebuffer.nim
nim r tests/test_linux_drm.nim
test-linux-cpu:
WINDY_TEST_BACKENDS=x11 nim r tests/test_linux_display.nim
test-linux-combined:
WINDY_TEST_BACKENDS=x11,fbdev,drm nim r tests/test_linux_display.nim
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,13 @@ while not window.closeRequested:

## CPU Pixels

On Windows, compile with `-d:useCpu` to create a plain window without an
On Windows and X11 Linux, compile with `-d:useCpu` to create a plain window without an
OpenGL context. CPU renderers can update the client area by passing a Pixie
image to `window.presentPixels(image)`.

Linux also has experimental fbdev, DRM and evdev touch backends
for applications running without a window server.


### Why not just use GLFW or SDL?

Expand Down
10 changes: 10 additions & 0 deletions src/windy/common.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import pixie, std/strutils, std/unicode
type
WindyError* = object of ValueError

TouchPhase* = enum
TouchBegin, TouchMove, TouchEnd, TouchCancel
TouchContact* = object
id*: int32
position*: Vec2
TouchEvent* = object
contact*: TouchContact
phase*: TouchPhase
TouchCallback* = proc(event: TouchEvent)

OpenGLVersion* = enum
OpenGL3Dot0
OpenGL3Dot1
Expand Down
4 changes: 4 additions & 0 deletions src/windy/platforms/emscripten/platform.nim
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type
Window* = ref object
onCloseRequest*: Callback
onFrame*: Callback
onTouch*: TouchCallback
onMove*: Callback
onResize*: Callback
onFocusChange*: Callback
Expand Down Expand Up @@ -1311,3 +1312,6 @@ proc saveFileDialog*(
discard defaultName
warn("saveFileDialog is not supported on emscripten")
result = ""

proc touches*(window: Window): seq[TouchContact] =
@[]
21 changes: 21 additions & 0 deletions src/windy/platforms/linux/backends.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import std/strutils

const
windyBackends* {.strdefine.} = "x11"
HasX11* = "x11" in windyBackends.split(',')
HasFbdev* = "fbdev" in windyBackends.split(',')
HasDrm* = "drm" in windyBackends.split(',')
HasDirect* = HasFbdev or HasDrm

type
LinuxBackend* = enum
X11Backend, FbdevBackend, DrmBackend
LinuxWindowOptions* = object
device*, output*, touchDevice*: string
rotation*, touchRotation*: int
gpuPixels*: bool
## Opt into a double-buffered X11 OpenGL drawable in CPU pixel builds.

static:
for backend in windyBackends.split(','):
doAssert backend in ["x11", "fbdev", "drm"], "Unknown Windy backend"
193 changes: 193 additions & 0 deletions src/windy/platforms/linux/direct.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import
pixie,
../../common,
./backends,
./native/[state, touch]

when HasFbdev:
import ./fbdev
when HasDrm:
import ./drm

when HasX11:
import ./x11
type Window* = x11.Window
else:
type Window* = ref object
onCloseRequest*, onFrame*, onMove*, onResize*, onFocusChange*: Callback
onMouseMove*, onScroll*, onImeChange*: Callback
onButtonPress*, onButtonRelease*: ButtonCallback
onRune*: RuneCallback
onTextInput*: TextInputCallback
onTextEdit*: TextInputEditCallback
onFileDrop*: FileDropCallback
onTouch*: TouchCallback
direct*: DirectWindowState

var windows: seq[Window]

proc close*(window: Window) =
## Release input and display resources before removing the window.
if window.direct == nil or window.direct.closed:
return
let data = window.direct
when HasDrm:
if data.drm != nil and data.drm.gpuOwned:
raise newException(WindyError, "Close the GPU pixel presenter before its windows")
data.closed = true
window.onTextInput = nil
window.onTextEdit = nil
try:
if data.touch != nil:
try:
for event in data.touch.cancel():
data.contacts.setLen(0)
if window.onTouch != nil: window.onTouch(event)
finally:
data.touch.close()
finally:
try:
when HasFbdev:
if data.fb != nil: data.fb.close()
when HasDrm:
if data.drm != nil and data.screen != nil:
data.drm.closeScreen(data.screen)
finally:
when HasDrm:
if data.drm != nil:
for index, screen in data.drm.screens:
if screen == data.screen:
data.drm.screens.delete(index)
break
if data.drm.screens.len == 0:
data.drm.close()
for index, current in windows:
if current == window:
windows.delete(index)
break

proc size*(window: Window): IVec2 =
## Return the native dimensions of the selected display.
let data = window.direct
when HasFbdev:
if data.backend == FbdevBackend:
return ivec2(data.fb.width.int32, data.fb.height.int32)
when HasDrm:
return ivec2(data.screen.width.int32, data.screen.height.int32)

proc newDirectWindow*(title: string, backend: LinuxBackend,
options: LinuxWindowOptions, vsync = false): Window =
## Open one compiled direct display backend and its optional touch device.
let path = if options.device.len > 0: options.device
elif backend == FbdevBackend: "/dev/fb0" else: "/dev/dri/card0"
result = Window(direct: DirectWindowState(title: title, device: path, backend: backend))
let data = result.direct
try:
case backend
of FbdevBackend:
when HasFbdev:
for window in windows:
if window.direct.device == path:
raise newException(WindyError, "Framebuffer is already owned: " & path)
data.fb = openFramebuffer(path, options.rotation)
else:
raise newException(WindyError, "fbdev backend was not compiled")
of DrmBackend:
when HasDrm:
if options.output.len == 0:
raise newException(WindyError, "A DRM connector must be selected")
for window in windows:
if window.direct.backend == DrmBackend and window.direct.device == path:
data.drm = window.direct.drm
break
if data.drm == nil:
data.drm = openDrm(path)
data.drm.openOutput(@[options.output], options.rotation, vsync)
data.screen = data.drm.screens[^1]
else:
raise newException(WindyError, "DRM backend was not compiled")
of X11Backend:
raise newException(WindyError, "X11 is not a direct display backend")
if options.touchDevice.len > 0:
for window in windows:
if window.direct.touch != nil and window.direct.touch.path == options.touchDevice:
raise newException(WindyError, "Touchscreen is already assigned")
let dimensions = result.size
data.touch = openTouch(options.touchDevice, dimensions.x.int, dimensions.y.int,
options.touchRotation)
windows.add result
except:
result.close()
raise

proc framebufferSize*(window: Window): IVec2 = window.size
proc closed*(window: Window): bool = window.direct.closed
proc closeRequested*(window: Window): bool = window.direct.closeRequested
proc `closeRequested=`*(window: Window, value: bool) = window.direct.closeRequested = value
proc title*(window: Window): string = window.direct.title
proc `title=`*(window: Window, value: string) = window.direct.title = value
proc contentScale*(window: Window): float32 = 1
proc fullscreen*(window: Window): bool = true
proc visible*(window: Window): bool = not window.closed
proc focused*(window: Window): bool = not window.closed
proc pos*(window: Window): IVec2 = ivec2(0, 0)
proc touches*(window: Window): seq[TouchContact] = window.direct.contacts
proc buttonDown*(window: Window): ButtonView = ButtonView({})
proc buttonPressed*(window: Window): ButtonView = ButtonView({})
proc buttonReleased*(window: Window): ButtonView = ButtonView({})

proc presentPixels*(window: Window, image: Image) =
if window.closed:
raise newException(WindyError, "Cannot present to a closed window")
let data = window.direct
when HasFbdev:
if data.fb != nil:
data.fb.present(image)
return
when HasDrm:
for index, screen in data.drm.screens:
if screen == data.screen:
data.drm.present(index, image)
return

proc pollEvents*() =
let current = windows
for window in current:
if window.closed: continue
let data = window.direct
if data.touch != nil:
for event in data.touch.poll():
var found = -1
for index, contact in data.contacts:
if contact.id == event.contact.id: found = index
if event.phase in {TouchEnd, TouchCancel}:
if found >= 0: data.contacts.delete(found)
elif found >= 0:
data.contacts[found] = event.contact
else:
data.contacts.add event.contact
if window.onTouch != nil: window.onTouch(event)
if window.closed: break
if not window.closed and window.onFrame != nil:
window.onFrame()

proc hasWindows*(): bool = windows.len > 0

proc unsupported*() =
raise newException(WindyError, "Operation is unavailable on a direct display")

proc minimized*(window: Window): bool = unsupported()
proc maximized*(window: Window): bool = unsupported()
proc style*(window: Window): WindowStyle = unsupported()
proc runeInputEnabled*(window: Window): bool = unsupported()
proc cursor*(window: Window): Cursor = unsupported()
proc mousePos*(window: Window): IVec2 = unsupported()
proc mousePrevPos*(window: Window): IVec2 = unsupported()
proc mouseDelta*(window: Window): IVec2 = unsupported()
proc mouseInside*(window: Window): bool = unsupported()
proc mouseCaptured*(window: Window): bool = unsupported()
proc scrollDelta*(window: Window): Vec2 = unsupported()
proc buttonToggle*(window: Window): ButtonView = unsupported()
proc imeCursorIndex*(window: Window): int = unsupported()
proc imeCompositionString*(window: Window): string = unsupported()
proc url*(window: Window): string = unsupported()
2 changes: 2 additions & 0 deletions src/windy/platforms/linux/drm.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include drm/display
include drm/pixels
Loading
Loading