Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.PHONY: test-native test-linux-cpu test-linux-combined
test-native:
nim r tests/test_native_input.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](docs/linux-direct.md)
for applications running without a window server.


### Why not just use GLFW or SDL?

Expand Down
68 changes: 68 additions & 0 deletions docs/linux-direct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Direct Linux displays (experimental)

These CPU pixel backends support Linux systems without X11. The default build
still uses X11/OpenGL. Put the desired settings in your application's
`config.nims`:

```nim
switch("define", "useCpu")
switch("define", "windyBackends=fbdev,drm")
```

`windyBackends` accepts `x11`, `fbdev`, `drm`, or a comma-separated combination.
Direct-only builds do not need X11 libraries. DRM loads `libdrm.so.2` when opened
and currently requires a 64-bit target. Combined builds include X11's library
dependencies even when selecting a direct display.

Select the backend before creating windows:

```nim
import pixie, windy

setLinuxBackend(DrmBackend)
let window = newWindow("Panel", ivec2(0, 0), vsync = false,
linuxOptions = LinuxWindowOptions(
device: "/dev/dri/card0",
output: "DSI-1",
touchDevice: "/dev/input/event1"))
let image = newImage(window.framebufferSize.x, window.framebufferSize.y)
image.fill(color(0.2, 0.4, 0.8, 1))
window.presentPixels(image)
while not window.closeRequested:
pollEvents()
window.close()
```

Choose connector and input paths for your system. Multiple DRM windows can own
separate connectors on the same device. For fbdev, select `FbdevBackend` and
`LinuxWindowOptions(device: "/dev/fb0")`; only one window can own that framebuffer.
Windows use the panel's native size. `rotation` and `touchRotation` accept
0, 90, 180 or 270 degrees. Device discovery and board-specific configuration
belong to the application.

`window.onTouch` receives begin/move/end/cancel events with stable contact IDs
and window-local pixel positions. `window.touches` contains active contacts.
Evdev single-touch and slot-based multitouch are supported, without smoothing,
gestures or mouse synthesis. X11, browser, Windows and macOS touch delivery is
not implemented in this draft.

The application needs permission to open its display and input devices, and
exclusive display ownership. Close windows before switching backends. Native
desktop operations such as resizing, clipboard access or OpenGL contexts raise
`WindyError`. Keyboard, mouse and controller input are not supplied by these
direct backends; use the application's input library for controllers.

Presentation copies into scanout memory. There is no page-flip queue or vblank
pacing, so `vsync` must be false and tearing is possible. Normal close restores
the saved display state when it is still available. If the kernel retired the
previous DRM framebuffer, the output is disabled instead.

Run `nim r tests/test_native_input.nim` for touch lifecycle and pixel tests.
`WINDY_TEST_BACKENDS=x11,fbdev,drm nim r tests/test_linux_display.nim` checks
combined backend selection and X11 CPU pixels; use an isolated Xvfb display.
`nim r tests/test_linux_glx.nim` checks the default OpenGL path.

On a dedicated test device, compile `tests/manual_direct_displays.nim` with
`WINDY_TEST_BACKENDS=fbdev,drm` and run it with `fbdev`, or with
`drm CONNECTOR1 CONNECTOR2`. This temporarily changes both outputs and checks
independent ownership and cleanup; stop other display applications first.
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 @@ -13,6 +13,7 @@ type
Window* = ref object
onCloseRequest*: Callback
onFrame*: Callback
onTouch*: TouchCallback
onMove*: Callback
onResize*: Callback
onFocusChange*: Callback
Expand Down Expand Up @@ -1245,3 +1246,6 @@ proc saveFileDialog*(
discard defaultName
warn("saveFileDialog is not supported on emscripten")
result = ""

proc touches*(window: Window): seq[TouchContact] =
@[]
19 changes: 19 additions & 0 deletions src/windy/platforms/linux/backends.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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

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

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
onFileDrop*: FileDropCallback
onTouch*: TouchCallback
direct*: DirectWindowState

var windows: seq[Window]

proc close*(window: Window) =
if window.direct == nil or window.direct.closed:
return
let data = window.direct
data.closed = true
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:
if data.fb != nil: data.fb.close()
if data.drm != nil and data.screen != nil:
data.drm.closeScreen(data.screen)
finally:
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 newDirectWindow*(title: string, backend: LinuxBackend,
options: LinuxWindowOptions): Window =
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:
if backend == FbdevBackend:
for window in windows:
if window.direct.device == path:
raise newException(WindyError, "Framebuffer is already owned: " & path)
data.fb = openFramebuffer(path, options.rotation)
else:
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)
data.screen = data.drm.screens[^1]
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
width = if data.fb != nil: data.fb.width else: data.screen.width
height = if data.fb != nil: data.fb.height else: data.screen.height
data.touch = openTouch(options.touchDevice, width, height, options.touchRotation)
windows.add result
except:
result.close()
raise

proc size*(window: Window): IVec2 =
let data = window.direct
if data.fb != nil: ivec2(data.fb.width.int32, data.fb.height.int32)
else: ivec2(data.screen.width.int32, data.screen.height.int32)

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
if data.fb != nil:
data.fb.present(image)
else:
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()
Loading
Loading