From 5ca712ee16f1df77e4f927ed5cd7cea007efec19 Mon Sep 17 00:00:00 2001 From: monofuel Date: Wed, 21 Oct 2026 23:26:57 -0400 Subject: [PATCH 1/5] Add experimental Linux direct display and touch backends --- Makefile | 7 + README.md | 5 +- docs/linux-direct.md | 68 +++ src/windy/common.nim | 10 + src/windy/platforms/emscripten/platform.nim | 4 + src/windy/platforms/linux/backends.nim | 19 + src/windy/platforms/linux/direct.nim | 162 ++++++ src/windy/platforms/linux/native/drm.nim | 468 ++++++++++++++++++ .../platforms/linux/native/framebuffer.nim | 148 ++++++ src/windy/platforms/linux/native/pixels.nim | 12 + src/windy/platforms/linux/native/state.nim | 15 + src/windy/platforms/linux/native/touch.nim | 190 +++++++ src/windy/platforms/linux/platform.nim | 422 +++++++++++++++- src/windy/platforms/linux/x11.nim | 100 +++- src/windy/platforms/macos/platform.nim | 4 + src/windy/platforms/win32/platform.nim | 4 + tests/bench_native_upload.nim | 18 + tests/config.nims | 12 + tests/manual_direct_displays.nim | 36 ++ tests/test_linux_display.nim | 52 ++ tests/test_linux_glx.nim | 13 + tests/test_native_input.nim | 59 +++ 22 files changed, 1805 insertions(+), 23 deletions(-) create mode 100644 Makefile create mode 100644 docs/linux-direct.md create mode 100644 src/windy/platforms/linux/backends.nim create mode 100644 src/windy/platforms/linux/direct.nim create mode 100644 src/windy/platforms/linux/native/drm.nim create mode 100644 src/windy/platforms/linux/native/framebuffer.nim create mode 100644 src/windy/platforms/linux/native/pixels.nim create mode 100644 src/windy/platforms/linux/native/state.nim create mode 100644 src/windy/platforms/linux/native/touch.nim create mode 100644 tests/bench_native_upload.nim create mode 100644 tests/manual_direct_displays.nim create mode 100644 tests/test_linux_display.nim create mode 100644 tests/test_linux_glx.nim create mode 100644 tests/test_native_input.nim diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..39570ac5 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index f7c4d721..37809bc9 100644 --- a/README.md +++ b/README.md @@ -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? diff --git a/docs/linux-direct.md b/docs/linux-direct.md new file mode 100644 index 00000000..779dd1f0 --- /dev/null +++ b/docs/linux-direct.md @@ -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. diff --git a/src/windy/common.nim b/src/windy/common.nim index 5d71a502..6d4790e8 100644 --- a/src/windy/common.nim +++ b/src/windy/common.nim @@ -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 diff --git a/src/windy/platforms/emscripten/platform.nim b/src/windy/platforms/emscripten/platform.nim index cfd36f35..857479a5 100644 --- a/src/windy/platforms/emscripten/platform.nim +++ b/src/windy/platforms/emscripten/platform.nim @@ -13,6 +13,7 @@ type Window* = ref object onCloseRequest*: Callback onFrame*: Callback + onTouch*: TouchCallback onMove*: Callback onResize*: Callback onFocusChange*: Callback @@ -1245,3 +1246,6 @@ proc saveFileDialog*( discard defaultName warn("saveFileDialog is not supported on emscripten") result = "" + +proc touches*(window: Window): seq[TouchContact] = + @[] diff --git a/src/windy/platforms/linux/backends.nim b/src/windy/platforms/linux/backends.nim new file mode 100644 index 00000000..70f13de1 --- /dev/null +++ b/src/windy/platforms/linux/backends.nim @@ -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" diff --git a/src/windy/platforms/linux/direct.nim b/src/windy/platforms/linux/direct.nim new file mode 100644 index 00000000..24eef9bd --- /dev/null +++ b/src/windy/platforms/linux/direct.nim @@ -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() diff --git a/src/windy/platforms/linux/native/drm.nim b/src/windy/platforms/linux/native/drm.nim new file mode 100644 index 00000000..15fe4257 --- /dev/null +++ b/src/windy/platforms/linux/native/drm.nim @@ -0,0 +1,468 @@ +import + std/[dynlib, os, posix, strformat, strutils], + pixie, + ./[framebuffer, pixels] + +type + ModeInfo = object + clock: uint32 + hdisplay, hsyncStart, hsyncEnd, htotal, hskew: uint16 + vdisplay, vsyncStart, vsyncEnd, vtotal, vscan: uint16 + refresh, flags, modeType: uint32 + name: array[32, char] + Resources = object + countFbs: cint + fbs: ptr UncheckedArray[uint32] + countCrtcs: cint + crtcs: ptr UncheckedArray[uint32] + countConnectors: cint + connectors: ptr UncheckedArray[uint32] + countEncoders: cint + encoders: ptr UncheckedArray[uint32] + minWidth, maxWidth, minHeight, maxHeight: uint32 + Connector = object + id, encoderId, connectorType, typeId: uint32 + connection: cint + mmWidth, mmHeight: uint32 + subpixel, countModes: cint + modes: ptr UncheckedArray[ModeInfo] + countProperties: cint + properties: ptr UncheckedArray[uint32] + values: ptr UncheckedArray[uint64] + countEncoders: cint + encoders: ptr UncheckedArray[uint32] + Encoder = object + id, encoderType, crtcId, possibleCrtcs, possibleClones: uint32 + Crtc = object + id, bufferId, x, y, width, height: uint32 + modeValid: cint + mode: ModeInfo + gammaSize: cint + DumbBuffer = object + height, width, bpp, flags, handle, pitch: uint32 + size: uint64 + MapBuffer = object + handle, padding: uint32 + offset: uint64 + FramebufferInfo = object + id, width, height, pitch, bpp, depth, handle: uint32 + GemClose = object + handle, padding: uint32 + PlaneResources = object + count: uint32 + planes: ptr UncheckedArray[uint32] + Plane = object + countFormats: uint32 + formats: ptr UncheckedArray[uint32] + id, crtc, framebuffer, crtcX, crtcY, x, y, possibleCrtcs, gammaSize: uint32 + ObjectProperties = object + count: uint32 + ids: ptr UncheckedArray[uint32] + values: ptr UncheckedArray[uint64] + Property = object + id, flags: uint32 + name: array[32, char] + countValues: cint + values: pointer + countEnums: cint + enums: pointer + countBlobs: cint + blobs: pointer + DrmApi = object + library: LibHandle + getResources: proc(fd: cint): ptr Resources {.cdecl.} + freeResources: proc(value: ptr Resources) {.cdecl.} + getConnector: proc(fd: cint, id: uint32): ptr Connector {.cdecl.} + freeConnector: proc(value: ptr Connector) {.cdecl.} + getEncoder: proc(fd: cint, id: uint32): ptr Encoder {.cdecl.} + freeEncoder: proc(value: ptr Encoder) {.cdecl.} + getCrtc: proc(fd: cint, id: uint32): ptr Crtc {.cdecl.} + freeCrtc: proc(value: ptr Crtc) {.cdecl.} + setCrtc: proc(fd: cint, id, buffer, x, y: uint32, connectors: ptr uint32, + count: cint, mode: ptr ModeInfo): cint {.cdecl.} + addFb: proc(fd: cint, width, height: uint32, depth, bpp: uint8, + pitch, handle: uint32, id: ptr uint32): cint {.cdecl.} + removeFb: proc(fd: cint, id: uint32): cint {.cdecl.} + getFb: proc(fd: cint, id: uint32): ptr FramebufferInfo {.cdecl.} + freeFb: proc(value: ptr FramebufferInfo) {.cdecl.} + setMaster, dropMaster: proc(fd: cint): cint {.cdecl.} + setClientCap: proc(fd: cint, capability, value: uint64): cint {.cdecl.} + getPlaneResources: proc(fd: cint): ptr PlaneResources {.cdecl.} + freePlaneResources: proc(value: ptr PlaneResources) {.cdecl.} + getPlane: proc(fd: cint, id: uint32): ptr Plane {.cdecl.} + freePlane: proc(value: ptr Plane) {.cdecl.} + getProperties: proc(fd: cint, id, kind: uint32): ptr ObjectProperties {.cdecl.} + freeProperties: proc(value: ptr ObjectProperties) {.cdecl.} + getProperty: proc(fd: cint, id: uint32): ptr Property {.cdecl.} + freeProperty: proc(value: ptr Property) {.cdecl.} + addFb2: proc(fd: cint, width, height, format: uint32, + handles, pitches, offsets: ptr uint32, id: ptr uint32, flags: uint32): cint {.cdecl.} + DrmScreen* = ref object + name*: string + width*, height*: int + rotation: int + connector, crtc, framebuffer: uint32 + mode: ModeInfo + original: Crtc + originalConnectors: seq[uint32] + buffer: DumbBuffer + memory: pointer + pixels: seq[uint32] + rgba: bool + activated: bool + DrmDisplay* = ref object + api: DrmApi + fd: cint + master: bool + screens*: seq[DrmScreen] + +const + CreateDumb = 0xc02064b2.culong + MapDumb = 0xc01064b3.culong + DestroyDumb = 0xc00464b4.culong + CloseGem = 0x40086409.culong + Connected = 1 + PreferredMode = 1'u32 shl 3 + Xbgr8888 = 0x34324258'u32 + Abgr8888 = 0x34324241'u32 + MaximumBufferBytes = 512 * 1024 * 1024 + ConnectorNames = ["Unknown", "VGA", "DVI-I", "DVI-D", "DVI-A", "Composite", + "SVIDEO", "LVDS", "Component", "DIN", "DP", "HDMI-A", "HDMI-B", "TV", + "eDP", "Virtual", "DSI", "DPI", "Writeback", "SPI", "USB"] + +static: + doAssert sizeof(pointer) == 8 + doAssert sizeof(ModeInfo) == 68 + doAssert sizeof(Resources) == 80 + doAssert sizeof(Connector) == 88 + doAssert sizeof(Crtc) == 100 + doAssert sizeof(Encoder) == 20 + doAssert sizeof(DumbBuffer) == 32 + doAssert sizeof(MapBuffer) == 16 + doAssert sizeof(FramebufferInfo) == 28 + doAssert sizeof(GemClose) == 8 + +proc loadDrm(): DrmApi = + ## Load DRM only when a DRM display is requested. + result.library = loadLib("libdrm.so.2") + if result.library == nil: + raise newException(IOError, "Cannot load libdrm.so.2; install libdrm for DRM displays") + template bindSymbol(field: untyped, name: string) = + result.field = cast[type(result.field)](symAddr(result.library, name)) + if result.field == nil: + unloadLib(result.library) + raise newException(IOError, "Missing DRM symbol: " & name) + bindSymbol(getResources, "drmModeGetResources") + bindSymbol(freeResources, "drmModeFreeResources") + bindSymbol(getConnector, "drmModeGetConnector") + bindSymbol(freeConnector, "drmModeFreeConnector") + bindSymbol(getEncoder, "drmModeGetEncoder") + bindSymbol(freeEncoder, "drmModeFreeEncoder") + bindSymbol(getCrtc, "drmModeGetCrtc") + bindSymbol(freeCrtc, "drmModeFreeCrtc") + bindSymbol(setCrtc, "drmModeSetCrtc") + bindSymbol(addFb, "drmModeAddFB") + bindSymbol(removeFb, "drmModeRmFB") + bindSymbol(getFb, "drmModeGetFB") + bindSymbol(freeFb, "drmModeFreeFB") + bindSymbol(setMaster, "drmSetMaster") + bindSymbol(dropMaster, "drmDropMaster") + bindSymbol(setClientCap, "drmSetClientCap") + bindSymbol(getPlaneResources, "drmModeGetPlaneResources") + bindSymbol(freePlaneResources, "drmModeFreePlaneResources") + bindSymbol(getPlane, "drmModeGetPlane") + bindSymbol(freePlane, "drmModeFreePlane") + bindSymbol(getProperties, "drmModeObjectGetProperties") + bindSymbol(freeProperties, "drmModeFreeObjectProperties") + bindSymbol(getProperty, "drmModeGetProperty") + bindSymbol(freeProperty, "drmModeFreeProperty") + bindSymbol(addFb2, "drmModeAddFB2") + +proc rgbaFormat(display: DrmDisplay, crtcIndex: int): uint32 = + let api = display.api + if api.setClientCap(display.fd, 2, 1) < 0: + if osLastError() == OSErrorCode(EINVAL): return 0 + raiseOSError(osLastError(), "Enabling universal DRM planes") + let resources = api.getPlaneResources(display.fd) + if resources == nil: + raiseOSError(osLastError(), "Reading DRM planes") + defer: api.freePlaneResources(resources) + for index in 0 ..< int(resources.count): + let plane = api.getPlane(display.fd, resources.planes[index]) + if plane == nil: raiseOSError(osLastError(), "Reading DRM plane") + defer: api.freePlane(plane) + if (plane.possibleCrtcs and (1'u32 shl crtcIndex)) == 0: continue + let properties = api.getProperties(display.fd, plane.id, 0xeeeeeeee'u32) + if properties == nil: raiseOSError(osLastError(), "Reading DRM plane properties") + defer: api.freeProperties(properties) + var primary = false + for propertyIndex in 0 ..< int(properties.count): + let property = api.getProperty(display.fd, properties.ids[propertyIndex]) + if property == nil: raiseOSError(osLastError(), "Reading DRM property") + if $cast[cstring](addr property.name[0]) == "type": + primary = properties.values[propertyIndex] == 1 + api.freeProperty(property) + if primary: + for format in [Xbgr8888, Abgr8888]: + for formatIndex in 0 ..< int(plane.countFormats): + if plane.formats[formatIndex] == format: return format + +proc closeScreen*(display: DrmDisplay, screen: DrmScreen) = + var errors: seq[string] + if screen.activated: + var + original = screen.original + connectors = screen.originalConnectors + enabled = original.modeValid != 0 and original.bufferId != 0 + if enabled: + let framebuffer = display.api.getFb(display.fd, original.bufferId) + if framebuffer == nil: + let error = osLastError() + enabled = false + if error == OSErrorCode(ENOENT): + echo "Original framebuffer retired by kernel; disabling ", screen.name, " on exit" + else: + errors.add "Querying original framebuffer for " & screen.name & ": " & $error + else: + if framebuffer.handle != 0: + var handle = GemClose(handle: framebuffer.handle) + if deviceIoctl(display.fd, CloseGem, addr handle) < 0: + errors.add "Closing original GEM handle for " & screen.name + display.api.freeFb(framebuffer) + let + connectorPointer = if enabled and connectors.len > 0: addr connectors[0] else: nil + modePointer = if enabled: addr original.mode else: nil + count = if enabled: cint(connectors.len) else: 0.cint + buffer = if enabled: original.bufferId else: 0'u32 + if display.api.setCrtc(display.fd, screen.crtc, buffer, original.x, + original.y, connectorPointer, count, modePointer) < 0: + errors.add "Restoring " & screen.name + screen.activated = false + if screen.memory != nil: + if munmap(screen.memory, int(screen.buffer.size)) < 0: + errors.add "Unmapping " & screen.name + screen.memory = nil + if screen.framebuffer != 0: + if display.api.removeFb(display.fd, screen.framebuffer) < 0: + errors.add "Removing framebuffer for " & screen.name + screen.framebuffer = 0 + if screen.buffer.handle != 0: + if deviceIoctl(display.fd, DestroyDumb, addr screen.buffer.handle) < 0: + errors.add "Destroying buffer for " & screen.name + screen.buffer.handle = 0 + + if errors.len > 0: + raise newException(IOError, errors.join("; ")) + +proc close*(display: DrmDisplay) = + var errors: seq[string] + for screen in display.screens: + try: + display.closeScreen(screen) + except IOError as error: + errors.add error.msg + if display.fd >= 0: + if display.master and display.api.dropMaster(display.fd) < 0: + errors.add "Releasing DRM master" + if posix.close(display.fd) < 0: + errors.add "Closing DRM device" + display.fd = -1 + if display.api.library != nil: + unloadLib(display.api.library) + display.api.library = nil + if errors.len > 0: + raise newException(IOError, errors.join("; ")) + +proc connectorName(connector: ptr Connector): string = + ## Format the kernel connector type and instance as a stable output name. + let prefix = if connector.connectorType < uint32(ConnectorNames.len): + ConnectorNames[connector.connectorType] else: "Connector" & $connector.connectorType + prefix & "-" & $connector.typeId + +proc chooseCrtc*(ids: openArray[uint32], current, possible: uint32, + unavailable: openArray[uint32]): uint32 = + ## Prefer the current compatible controller without taking another output's controller. + for index, id in ids: + if index < 32 and (possible and (1'u32 shl index)) != 0 and + id notin unavailable and id == current: + return id + for index, id in ids: + if index < 32 and (possible and (1'u32 shl index)) != 0 and id notin unavailable: + return id + raise newException(IOError, "No independent display controller is available") + +proc openOutput*(result: DrmDisplay, outputs: seq[string], rotation: int) = + ## Prepare independent CPU scanout buffers without changing the active display. + if rotation notin [0, 90, 180, 270]: + raise newException(ValueError, "Rotation must be 0, 90, 180 or 270") + let originalCount = result.screens.len + try: + let + api = result.api + resources = api.getResources(result.fd) + if resources == nil: + raiseOSError(osLastError(), "Reading DRM resources") + defer: api.freeResources(resources) + var + connectors: seq[ptr Connector] + names: seq[string] + crtcs: seq[uint32] + used: seq[uint32] + requested = outputs + defer: + for connector in connectors: + api.freeConnector(connector) + for screen in result.screens: + used.add screen.crtc + for index in 0 ..< int(resources.countCrtcs): + crtcs.add resources.crtcs[index] + for index in 0 ..< int(resources.countConnectors): + let connector = api.getConnector(result.fd, resources.connectors[index]) + if connector == nil: + raiseOSError(osLastError(), "Reading DRM connector") + connectors.add connector + names.add connector.connectorName() + if outputs.len == 0 and connector.connection == Connected and connector.countModes > 0: + requested.add names[^1] + if requested.len == 0: + raise newException(IOError, "No connected DRM displays") + for name in requested: + for screen in result.screens: + if screen.name == name: + raise newException(IOError, "DRM output is already owned: " & name) + let index = names.find(name) + if index < 0: + raise newException(IOError, "DRM output not found: " & name) + let connector = connectors[index] + if connector.connection != Connected or connector.countModes == 0: + raise newException(IOError, "DRM output has no connected mode: " & name) + var + possible = 0'u32 + current = 0'u32 + for index in 0 ..< int(connector.countEncoders): + let encoder = api.getEncoder(result.fd, connector.encoders[index]) + if encoder == nil: + raiseOSError(osLastError(), "Reading DRM encoder") + possible = possible or encoder.possibleCrtcs + if encoder.id == connector.encoderId: + current = encoder.crtcId + api.freeEncoder(encoder) + var unavailable = used + for other in connectors: + if other.connectorName() notin requested and other.encoderId != 0: + let encoder = api.getEncoder(result.fd, other.encoderId) + if encoder == nil: + raiseOSError(osLastError(), "Reading existing DRM route") + if encoder.crtcId != 0: + unavailable.add encoder.crtcId + api.freeEncoder(encoder) + let + id = chooseCrtc(crtcs, current, possible, unavailable) + original = api.getCrtc(result.fd, id) + if original == nil: + raiseOSError(osLastError(), "Saving DRM scanout") + let screen = DrmScreen(name: name, connector: connector.id, crtc: id, + original: original[], mode: connector.modes[0], rotation: rotation) + api.freeCrtc(original) + result.screens.add screen + used.add id + for modeIndex in 0 ..< int(connector.countModes): + if (connector.modes[modeIndex].modeType and PreferredMode) != 0: + screen.mode = connector.modes[modeIndex] + break + if screen.original.modeValid != 0: + screen.mode = screen.original.mode + for other in connectors: + if other.encoderId != 0: + let encoder = api.getEncoder(result.fd, other.encoderId) + if encoder == nil: + raiseOSError(osLastError(), "Saving DRM connector routing") + if encoder.crtcId == id: + screen.originalConnectors.add other.id + api.freeEncoder(encoder) + screen.width = int(if rotation in [90, 270]: screen.mode.vdisplay else: screen.mode.hdisplay) + screen.height = int(if rotation in [90, 270]: screen.mode.hdisplay else: screen.mode.vdisplay) + screen.buffer = DumbBuffer(width: uint32(screen.mode.hdisplay), + height: uint32(screen.mode.vdisplay), bpp: 32) + if deviceIoctl(result.fd, CreateDumb, addr screen.buffer) < 0: + raiseOSError(osLastError(), "Allocating DRM scanout buffer") + if screen.buffer.pitch < screen.buffer.width * 4 or + screen.buffer.size < uint64(screen.buffer.pitch) * screen.buffer.height or + screen.buffer.size > MaximumBufferBytes: + raise newException(IOError, "Unsupported DRM buffer geometry") + let format = result.rgbaFormat(crtcs.find(id)) + screen.rgba = format != 0 + if screen.rgba: + var + handles = [screen.buffer.handle, 0'u32, 0, 0] + pitches = [screen.buffer.pitch, 0'u32, 0, 0] + offsets: array[4, uint32] + if api.addFb2(result.fd, screen.buffer.width, screen.buffer.height, format, + addr handles[0], addr pitches[0], addr offsets[0], addr screen.framebuffer, 0) < 0: + raiseOSError(osLastError(), "Registering RGBA DRM framebuffer") + elif api.addFb(result.fd, screen.buffer.width, screen.buffer.height, 24, 32, + screen.buffer.pitch, screen.buffer.handle, addr screen.framebuffer) < 0: + raiseOSError(osLastError(), "Registering DRM framebuffer") + var mapping = MapBuffer(handle: screen.buffer.handle) + if deviceIoctl(result.fd, MapDumb, addr mapping) < 0: + raiseOSError(osLastError(), "Mapping DRM buffer") + screen.memory = mmap(nil, int(screen.buffer.size), PROT_READ or PROT_WRITE, + MAP_SHARED, result.fd, Off(mapping.offset)) + if screen.memory == MAP_FAILED: + screen.memory = nil + raiseOSError(osLastError(), "Mapping DRM scanout memory") + except: + while result.screens.len > originalCount: + let screen = result.screens.pop() + result.closeScreen(screen) + raise + +proc upload*(display: DrmDisplay, index: int, image: Image) = + ## Copy pixels into scanout memory without changing the active controller. + let screen = display.screens[index] + if image.width != screen.width or image.height != screen.height: + raise newException(ValueError, "Canvas does not match DRM screen") + let destination = cast[ptr UncheckedArray[uint32]](screen.memory) + if screen.rgba and screen.rotation == 0: + for row in 0 ..< image.height: + copyMem(addr destination[row * int(screen.buffer.pitch) div 4], + unsafeAddr image.data[row * image.width], image.width * 4) + return + screen.pixels.setLen(image.data.len) + if screen.rotation == 0: + for index, pixel in image.data: + screen.pixels[index] = rgb(pixel) + else: + for y in 0 ..< image.height: + for x in 0 ..< image.width: + let point = rotatedPoint(x, y, image.width, image.height, screen.rotation) + screen.pixels[point.y * int(screen.buffer.width) + point.x] = + (if screen.rgba: cast[uint32](image.data[y * image.width + x]) + else: rgb(image.data[y * image.width + x])) + for row in 0 ..< int(screen.buffer.height): + copyMem(addr destination[row * int(screen.buffer.pitch) div 4], + addr screen.pixels[row * int(screen.buffer.width)], int(screen.buffer.width) * 4) + +proc present*(display: DrmDisplay, index: int, image: Image) = + display.upload(index, image) + let screen = display.screens[index] + if not screen.activated: + if not display.master: + if display.api.setMaster(display.fd) < 0: + raiseOSError(osLastError(), "Acquiring DRM master; another display application may be running") + display.master = true + if display.api.setCrtc(display.fd, screen.crtc, screen.framebuffer, 0, 0, + addr screen.connector, 1, addr screen.mode) < 0: + raiseOSError(osLastError(), "Displaying " & screen.name) + screen.activated = true + +proc describe*(screen: DrmScreen): string = + ## Describe the selected connector, logical geometry and controller. + &"{screen.name}: {screen.width}x{screen.height}, CRTC {screen.crtc}, rotation {screen.rotation}" +proc openDrm*(path: string): DrmDisplay = + result = DrmDisplay(fd: -1, api: loadDrm()) + result.fd = posix.open(path.cstring, O_RDWR or O_CLOEXEC) + if result.fd < 0: + let error = osLastError() + result.close() + raiseOSError(error, path) diff --git a/src/windy/platforms/linux/native/framebuffer.nim b/src/windy/platforms/linux/native/framebuffer.nim new file mode 100644 index 00000000..56f8afb2 --- /dev/null +++ b/src/windy/platforms/linux/native/framebuffer.nim @@ -0,0 +1,148 @@ +import + std/[os, posix, strformat], + pixie, + ./pixels + +type + Bitfield* {.importc: "struct fb_bitfield", header: "", bycopy.} = object + offset*, length*, msb_right*: uint32 + VariableInfo {.importc: "struct fb_var_screeninfo", header: "", bycopy.} = object + xres, yres, xres_virtual, yres_virtual, xoffset, yoffset: uint32 + bits_per_pixel, grayscale: uint32 + red, green, blue, transp: Bitfield + nonstd, activate, height, width, accel_flags: uint32 + pixclock, left_margin, right_margin, upper_margin, lower_margin: uint32 + hsync_len, vsync_len, sync, vmode, rotate, colorspace: uint32 + reserved: array[4, uint32] + FixedInfo {.importc: "struct fb_fix_screeninfo", header: "", bycopy.} = object + id: array[16, char] + smem_start: culong + smem_len, `type`, type_aux, visual: uint32 + xpanstep, ypanstep, ywrapstep: uint16 + line_length: uint32 + mmio_start: culong + mmio_len, accel: uint32 + capabilities: uint16 + reserved: array[2, uint16] + Framebuffer* = ref object + fd: cint + variable, original: VariableInfo + fixed: FixedInfo + memory: pointer + saved: seq[byte] + length: int + activated, touched: bool + rotation*: int + width*, height*: int + +const + GetVariable = 0x4600.culong + PutVariable = 0x4601.culong + GetFixed = 0x4602.culong + PanDisplay = 0x4606.culong + Blank = 0x4611.culong + ActivateForce = 128'u32 + +proc deviceIoctl*(fd: cint, request: culong, argument: pointer): cint {.importc: "ioctl", header: "".} + +proc packPixel*(color: uint32, red, green, blue, alpha: Bitfield): uint32 = + for (value, channel) in [(color shr 16 and 255, red), + (color shr 8 and 255, green), (color and 255, blue), (255'u32, alpha)]: + if channel.length > 0: + if channel.length > 8 or channel.offset + channel.length > 32 or channel.msb_right != 0: + raise newException(ValueError, "Unsupported framebuffer channel layout") + let maximum = (1'u32 shl channel.length) - 1 + result = result or ((value * maximum + 127) div 255 shl channel.offset) + +proc checkedIoctl(fd: cint, request: culong, argument: pointer) = + if deviceIoctl(fd, request, argument) < 0: + raiseOSError(osLastError()) + +proc openFramebuffer*(path: string, rotation = 0): Framebuffer = + if rotation notin [0, 90, 180, 270]: + raise newException(ValueError, "Rotation must be 0, 90, 180 or 270") + result = Framebuffer(fd: -1, rotation: rotation) + result.fd = posix.open(path.cstring, O_RDWR or O_CLOEXEC) + if result.fd < 0: + raiseOSError(osLastError(), path) + try: + checkedIoctl(result.fd, GetVariable, addr result.variable) + checkedIoctl(result.fd, GetFixed, addr result.fixed) + result.original = result.variable + if result.variable.bits_per_pixel notin [16'u32, 24, 32] or + result.variable.grayscale != 0 or result.fixed.`type` != 0 or result.fixed.visual != 2: + raise newException(ValueError, "Expected a packed truecolor framebuffer") + let + width = int(result.variable.xres) + height = int(result.variable.yres) + stride = int(result.fixed.line_length) + pixelBytes = int(result.variable.bits_per_pixel div 8) + if width notin 1..8192 or height notin 1..8192 or + stride < width * pixelBytes or stride > 131072: + raise newException(ValueError, "Invalid framebuffer geometry") + result.length = int(result.fixed.smem_len) + if result.length < stride * height or result.length > 256 * 1024 * 1024: + raise newException(ValueError, "Invalid framebuffer memory size") + discard packPixel(0xffffff'u32, result.variable.red, result.variable.green, + result.variable.blue, result.variable.transp) + result.width = if rotation in [90, 270]: height else: width + result.height = if rotation in [90, 270]: width else: height + result.memory = mmap(nil, result.length, PROT_READ or PROT_WRITE, MAP_SHARED, result.fd, 0) + if result.memory == MAP_FAILED: + result.memory = nil + raiseOSError(osLastError(), "Mapping framebuffer") + result.saved = newSeq[byte](result.length) + copyMem(addr result.saved[0], result.memory, result.length) + except: + if result.memory != nil: + discard munmap(result.memory, result.length) + discard posix.close(result.fd) + raise + +proc describe*(fb: Framebuffer): string = + let v = fb.variable + &"{v.xres}x{v.yres} visible, {v.xres_virtual}x{v.yres_virtual} virtual, " & + &"{v.bits_per_pixel}bpp, stride={fb.fixed.line_length}, offsets={v.xoffset},{v.yoffset}, " & + &"RGB={v.red.offset}:{v.red.length}/{v.green.offset}:{v.green.length}/{v.blue.offset}:{v.blue.length}" + +proc present*(fb: Framebuffer, image: Image) = + if image.width != fb.width or image.height != fb.height: + raise newException(ValueError, "Canvas dimensions differ from framebuffer") + let + destination = cast[ptr UncheckedArray[byte]](fb.memory) + bytesPerPixel = int(fb.variable.bits_per_pixel div 8) + stride = int(fb.fixed.line_length) + fb.touched = true + for y in 0 ..< image.height: + for x in 0 ..< image.width: + let + point = rotatedPoint(x, y, image.width, image.height, fb.rotation) + offset = point.y * stride + point.x * bytesPerPixel + pixel = packPixel(rgb(image.data[y * image.width + x]), fb.variable.red, + fb.variable.green, fb.variable.blue, fb.variable.transp) + for index in 0 ..< bytesPerPixel: + destination[offset + index] = byte((pixel shr (index * 8)) and 255) + if not fb.activated: + fb.variable.xoffset = 0 + fb.variable.yoffset = 0 + if deviceIoctl(fb.fd, PanDisplay, addr fb.variable) < 0: + fb.variable.activate = ActivateForce + checkedIoctl(fb.fd, PutVariable, addr fb.variable) + checkedIoctl(fb.fd, Blank, nil) + fb.activated = true + +proc close*(fb: Framebuffer) = + if fb.memory != nil: + var restoreResult = 0.cint + if fb.touched: + copyMem(fb.memory, addr fb.saved[0], fb.length) + if fb.activated: + restoreResult = deviceIoctl(fb.fd, PanDisplay, addr fb.original) + if restoreResult < 0: + restoreResult = deviceIoctl(fb.fd, PutVariable, addr fb.original) + let unmapResult = munmap(fb.memory, fb.length) + fb.memory = nil + let closeResult = posix.close(fb.fd) + fb.fd = -1 + if restoreResult < 0 or unmapResult < 0 or closeResult < 0: + raise newException(IOError, "Unable to completely restore/close framebuffer") diff --git a/src/windy/platforms/linux/native/pixels.nim b/src/windy/platforms/linux/native/pixels.nim new file mode 100644 index 00000000..85bd384d --- /dev/null +++ b/src/windy/platforms/linux/native/pixels.nim @@ -0,0 +1,12 @@ +import pixie + +proc rgb*(pixel: ColorRGBX): uint32 {.inline.} = + uint32(pixel.r) shl 16 or uint32(pixel.g) shl 8 or uint32(pixel.b) + +proc rotatedPoint*(x, y, width, height, rotation: int): tuple[x, y: int] {.inline.} = + case rotation + of 0: (x, y) + of 90: (height - 1 - y, x) + of 180: (width - 1 - x, height - 1 - y) + of 270: (y, width - 1 - x) + else: raise newException(ValueError, "Rotation must be 0, 90, 180 or 270") diff --git a/src/windy/platforms/linux/native/state.nim b/src/windy/platforms/linux/native/state.nim new file mode 100644 index 00000000..356d7199 --- /dev/null +++ b/src/windy/platforms/linux/native/state.nim @@ -0,0 +1,15 @@ +import + ../../../common, + ../backends, + ./[drm, framebuffer, touch] + +type + DirectWindowState* = ref object + fb*: Framebuffer + drm*: DrmDisplay + screen*: DrmScreen + touch*: TouchReader + device*, title*: string + backend*: LinuxBackend + closed*, closeRequested*: bool + contacts*: seq[TouchContact] diff --git a/src/windy/platforms/linux/native/touch.nim b/src/windy/platforms/linux/native/touch.nim new file mode 100644 index 00000000..6a279974 --- /dev/null +++ b/src/windy/platforms/linux/native/touch.nim @@ -0,0 +1,190 @@ +import + std/[os, posix], + pixie, + ../../../common, + ./[framebuffer, pixels] + +type + Event {.importc: "struct input_event", header: "", bycopy.} = object + time: Timeval + kind {.importc: "type".}: uint16 + code: uint16 + value: int32 + Axis {.importc: "struct input_absinfo", header: "", bycopy.} = object + value, minimum, maximum, fuzz, flat, resolution: int32 + Slot* = object + id*, x*, y*: int32 + TouchReader* = ref object + fd: cint + path*: string + minimumX*, maximumX*, minimumY*, maximumY*: int32 + slotMinimum, slot: int + multitouch, dropped: bool + pending*, previous*: seq[Slot] + width*, height*, rotation*: int + +proc getAxis(code: cint): culong {.importc: "EVIOCGABS", header: "".} +proc getSlots(length: cint): culong {.importc: "EVIOCGMTSLOTS", header: "".} +proc getKeys(length: cint): culong {.importc: "EVIOCGKEY", header: "".} +proc getBits(kind, length: cint): culong {.importc: "EVIOCGBIT", header: "".} + +proc contact(reader: TouchReader, slot: Slot): TouchContact = + let + rawWidth = if reader.rotation in [90, 270]: reader.height else: reader.width + rawHeight = if reader.rotation in [90, 270]: reader.width else: reader.height + x = int((int64(clamp(slot.x, reader.minimumX, reader.maximumX)) - reader.minimumX) * + (rawWidth - 1) div (int64(reader.maximumX) - reader.minimumX)) + y = int((int64(clamp(slot.y, reader.minimumY, reader.maximumY)) - reader.minimumY) * + (rawHeight - 1) div (int64(reader.maximumY) - reader.minimumY)) + point = rotatedPoint(x, y, rawWidth, rawHeight, reader.rotation) + TouchContact(id: slot.id, position: vec2(point.x.float32, point.y.float32)) + +proc report*(reader: TouchReader): seq[TouchEvent] = + ## Publish a coherent report, ending replaced contacts before beginning new ones. + for index, old in reader.previous: + let current = reader.pending[index] + if old.id >= 0 and old.id != current.id: + result.add TouchEvent(contact: reader.contact(old), phase: TouchEnd) + for index, current in reader.pending: + let old = reader.previous[index] + if current.id >= 0: + if old.id != current.id: + result.add TouchEvent(contact: reader.contact(current), phase: TouchBegin) + elif old.x != current.x or old.y != current.y: + result.add TouchEvent(contact: reader.contact(current), phase: TouchMove) + reader.previous = reader.pending + +proc cancel*(reader: TouchReader): seq[TouchEvent] = + for old in reader.previous: + if old.id >= 0: + result.add TouchEvent(contact: reader.contact(old), phase: TouchCancel) + for slot in reader.pending.mitems: + slot.id = -1 + reader.previous = reader.pending + +proc resync(reader: TouchReader) = + if reader.multitouch: + var axis: Axis + if deviceIoctl(reader.fd, getAxis(47), addr axis) < 0: + raiseOSError(osLastError(), reader.path) + reader.slot = int(axis.value) - reader.slotMinimum + for code in [57'i32, 53, 54]: + var values = newSeq[int32](reader.pending.len + 1) + values[0] = code + if deviceIoctl(reader.fd, getSlots(cint(values.len * sizeof(int32))), addr values[0]) < 0: + raiseOSError(osLastError(), reader.path) + for index in 0 ..< reader.pending.len: + case code + of 57: reader.pending[index].id = values[index + 1] + of 53: reader.pending[index].x = values[index + 1] + of 54: reader.pending[index].y = values[index + 1] + else: discard + else: + var + keys: array[96, byte] + x, y: Axis + if deviceIoctl(reader.fd, getKeys(cint(keys.len)), addr keys[0]) < 0 or + deviceIoctl(reader.fd, getAxis(0), addr x) < 0 or + deviceIoctl(reader.fd, getAxis(1), addr y) < 0: + raiseOSError(osLastError(), reader.path) + reader.pending[0] = Slot(id: (if (keys[330 div 8] and (1'u8 shl (330 mod 8))) != 0: 0'i32 else: -1'i32), + x: x.value, y: y.value) + +proc close*(reader: TouchReader) = + if reader.fd >= 0: + let fd = reader.fd + reader.fd = -1 + if posix.close(fd) < 0: + raiseOSError(osLastError(), reader.path) + +proc openTouch*(path: string, width, height, rotation: int): TouchReader = + discard rotatedPoint(0, 0, width, height, rotation) + result = TouchReader(fd: -1, path: path, width: width, height: height, rotation: rotation) + result.fd = posix.open(path.cstring, O_RDONLY or O_NONBLOCK or O_CLOEXEC) + if result.fd < 0: + raiseOSError(osLastError(), path) + try: + var + bits: array[8, byte] + slots, x, y: Axis + if deviceIoctl(result.fd, getBits(3, cint(bits.len)), addr bits[0]) < 0: + raiseOSError(osLastError(), path) + result.multitouch = (bits[47 div 8] and (1'u8 shl (47 mod 8))) != 0 + if result.multitouch: + if deviceIoctl(result.fd, getAxis(47), addr slots) < 0: + raiseOSError(osLastError(), path) + result.slotMinimum = int(slots.minimum) + let + count = if result.multitouch: int(slots.maximum - slots.minimum + 1) else: 1 + xCode = if result.multitouch: 53 else: 0 + yCode = if result.multitouch: 54 else: 1 + if count notin 1 .. 32: + raise newException(WindyError, "Unsupported touch slot count") + if deviceIoctl(result.fd, getAxis(cint(xCode)), addr x) < 0 or + deviceIoctl(result.fd, getAxis(cint(yCode)), addr y) < 0: + raiseOSError(osLastError(), path) + if x.maximum <= x.minimum or y.maximum <= y.minimum: + raise newException(WindyError, "Invalid touchscreen coordinate ranges") + result.minimumX = x.minimum + result.maximumX = x.maximum + result.minimumY = y.minimum + result.maximumY = y.maximum + result.pending = newSeq[Slot](count) + for slot in result.pending.mitems: + slot.id = -1 + result.previous = result.pending + result.resync() + except: + result.close() + raise + +proc poll*(reader: TouchReader): seq[TouchEvent] = + if reader.fd < 0: + return + result = reader.report() + while true: + var event: Event + let count = posix.read(reader.fd, addr event, sizeof(event)) + if count < 0: + let error = osLastError() + if error == OSErrorCode(EAGAIN): break + if error == OSErrorCode(EINTR): continue + if error == OSErrorCode(ENODEV): + result.add reader.cancel() + reader.close() + break + raiseOSError(error, reader.path) + if count == 0: + result.add reader.cancel() + reader.close() + break + if count != sizeof(event): + raise newException(WindyError, "Incomplete touchscreen event") + if event.kind == 0 and event.code == 3: + result.add reader.cancel() + reader.dropped = true + elif reader.dropped: + if event.kind == 0 and event.code == 0: + reader.resync() + reader.dropped = false + result.add reader.report() + elif event.kind == 0 and event.code == 0: + result.add reader.report() + elif event.kind == 3: + if event.code == 47 and reader.multitouch: + let slot = int(event.value) - reader.slotMinimum + if slot notin 0 ..< reader.pending.len: + raise newException(WindyError, "Invalid touchscreen slot") + reader.slot = slot + elif reader.slot in 0 ..< reader.pending.len: + case event.code + of 57: reader.pending[reader.slot].id = event.value + of 53: reader.pending[reader.slot].x = event.value + of 54: reader.pending[reader.slot].y = event.value + of 0: + if not reader.multitouch: reader.pending[0].x = event.value + of 1: + if not reader.multitouch: reader.pending[0].y = event.value + else: discard + elif event.kind == 1 and event.code == 330 and not reader.multitouch: + reader.pending[0].id = if event.value == 0: -1 else: 0 diff --git a/src/windy/platforms/linux/platform.nim b/src/windy/platforms/linux/platform.nim index d581b16d..7a2fbaa1 100644 --- a/src/windy/platforms/linux/platform.nim +++ b/src/windy/platforms/linux/platform.nim @@ -1,5 +1,417 @@ -# when defined(wayland): -# include wayland -# else: -include x11 -# include x11_2 +import ./backends +export backends + +when not HasDirect: + include x11 + + proc setLinuxBackend*(backend: LinuxBackend) = + if backend != X11Backend: + raise newException(WindyError, "Backend was not compiled into Windy") +else: + import pixie, ../../common, ./direct + import ../../http + export http + when HasX11: + import ./x11 + export x11.quitRequested, x11.onQuitRequest, x11.multiClickInterval, x11.multiClickRadius + else: + var + quitRequested*: bool + onQuitRequest*: Callback + type Window* = direct.Window + var selected = when HasX11: X11Backend elif HasFbdev: FbdevBackend else: DrmBackend + + proc setLinuxBackend*(backend: LinuxBackend) = + if backend == selected: return + if direct.hasWindows(): + raise newException(WindyError, "Close windows before changing backend") + when HasX11: + if x11.hasWindows(): + raise newException(WindyError, "Close windows before changing backend") + case backend + of X11Backend: + when not HasX11: raise newException(WindyError, "X11 backend was not compiled") + of FbdevBackend: + when not HasFbdev: raise newException(WindyError, "fbdev backend was not compiled") + of DrmBackend: + when not HasDrm: raise newException(WindyError, "DRM backend was not compiled") + selected = backend + + proc newWindow*(title: string, size: IVec2, style = DecoratedResizable, + visible = true, vsync = true, openglVersion = OpenGL4Dot1, + msaa = msaaDisabled, depthBits = 24, stencilBits = 8, + linuxOptions = LinuxWindowOptions()): Window = + if selected == X11Backend: + when HasX11: + return x11.newWindow(title, size, style, visible, vsync, openglVersion, + msaa, depthBits, stencilBits, linuxOptions) + else: raise newException(WindyError, "X11 backend was not compiled") + if vsync: + raise newException(WindyError, "Direct pixel presentation requires vsync=false") + direct.newDirectWindow(title, selected, linuxOptions) + + proc pollEvents*() = + if selected == X11Backend: + when HasX11: x11.pollEvents() + else: + direct.pollEvents() + pollHttp() + + proc closed*(window: Window): bool = + when HasX11: + if window.direct == nil: return x11.closed(window) + direct.closed(window) + + proc size*(window: Window): IVec2 = + when HasX11: + if window.direct == nil: return x11.size(window) + direct.size(window) + + proc framebufferSize*(window: Window): IVec2 = + when HasX11: + if window.direct == nil: return x11.framebufferSize(window) + direct.framebufferSize(window) + + proc title*(window: Window): string = + when HasX11: + if window.direct == nil: return x11.title(window) + direct.title(window) + + proc contentScale*(window: Window): float32 = + when HasX11: + if window.direct == nil: return x11.contentScale(window) + direct.contentScale(window) + + proc fullscreen*(window: Window): bool = + when HasX11: + if window.direct == nil: return x11.fullscreen(window) + direct.fullscreen(window) + + proc visible*(window: Window): bool = + when HasX11: + if window.direct == nil: return x11.visible(window) + direct.visible(window) + + proc focused*(window: Window): bool = + when HasX11: + if window.direct == nil: return x11.focused(window) + direct.focused(window) + + proc pos*(window: Window): IVec2 = + when HasX11: + if window.direct == nil: return x11.pos(window) + direct.pos(window) + + proc touches*(window: Window): seq[TouchContact] = + when HasX11: + if window.direct == nil: return x11.touches(window) + direct.touches(window) + + proc buttonDown*(window: Window): ButtonView = + when HasX11: + if window.direct == nil: return x11.buttonDown(window) + direct.buttonDown(window) + + proc buttonPressed*(window: Window): ButtonView = + when HasX11: + if window.direct == nil: return x11.buttonPressed(window) + direct.buttonPressed(window) + + proc buttonReleased*(window: Window): ButtonView = + when HasX11: + if window.direct == nil: return x11.buttonReleased(window) + direct.buttonReleased(window) + + proc closeRequested*(window: Window): bool = + when HasX11: + if window.direct == nil: return x11.closeRequested(window) + direct.closeRequested(window) + + proc close*(window: Window) = + when HasX11: + if window.direct == nil: + x11.close(window) + return + direct.close(window) + + proc `title=`*(window: Window, value: string) = + when HasX11: + if window.direct == nil: + x11.`title=`(window, value) + return + direct.`title=`(window, value) + + proc `closeRequested=`*(window: Window, value: bool) = + when HasX11: + if window.direct == nil: + x11.`closeRequested=`(window, value) + return + direct.`closeRequested=`(window, value) + + when defined(useCpu): + proc presentPixels*(window: Window, image: Image) = + when HasX11: + if window.direct == nil: + x11.presentPixels(window, image) + return + direct.presentPixels(window, image) + + proc minimized*(window: Window): bool = + when HasX11: + if window.direct == nil: return x11.minimized(window) + direct.unsupported() + + proc maximized*(window: Window): bool = + when HasX11: + if window.direct == nil: return x11.maximized(window) + direct.unsupported() + + proc style*(window: Window): WindowStyle = + when HasX11: + if window.direct == nil: return x11.style(window) + direct.unsupported() + + proc runeInputEnabled*(window: Window): bool = + when HasX11: + if window.direct == nil: return x11.runeInputEnabled(window) + direct.unsupported() + + proc cursor*(window: Window): Cursor = + when HasX11: + if window.direct == nil: return x11.cursor(window) + direct.unsupported() + + proc mousePos*(window: Window): IVec2 = + when HasX11: + if window.direct == nil: return x11.mousePos(window) + direct.unsupported() + + proc mousePrevPos*(window: Window): IVec2 = + when HasX11: + if window.direct == nil: return x11.mousePrevPos(window) + direct.unsupported() + + proc mouseDelta*(window: Window): IVec2 = + when HasX11: + if window.direct == nil: return x11.mouseDelta(window) + direct.unsupported() + + proc mouseInside*(window: Window): bool = + when HasX11: + if window.direct == nil: return x11.mouseInside(window) + direct.unsupported() + + proc mouseCaptured*(window: Window): bool = + when HasX11: + if window.direct == nil: return x11.mouseCaptured(window) + direct.unsupported() + + proc scrollDelta*(window: Window): Vec2 = + when HasX11: + if window.direct == nil: return x11.scrollDelta(window) + direct.unsupported() + + proc buttonToggle*(window: Window): ButtonView = + when HasX11: + if window.direct == nil: return x11.buttonToggle(window) + direct.unsupported() + + proc imeCursorIndex*(window: Window): int = + when HasX11: + if window.direct == nil: return x11.imeCursorIndex(window) + direct.unsupported() + + proc imeCompositionString*(window: Window): string = + when HasX11: + if window.direct == nil: return x11.imeCompositionString(window) + direct.unsupported() + + proc url*(window: Window): string = + when HasX11: + if window.direct == nil: return x11.url(window) + direct.unsupported() + + proc makeContextCurrent*(window: Window) = + when HasX11: + if window.direct == nil: + x11.makeContextCurrent(window) + return + direct.unsupported() + + proc swapBuffers*(window: Window) = + when HasX11: + if window.direct == nil: + x11.swapBuffers(window) + return + direct.unsupported() + + proc centerWindow*(window: Window) = + when HasX11: + if window.direct == nil: + x11.centerWindow(window) + return + direct.unsupported() + + proc focus*(window: Window) = + when HasX11: + if window.direct == nil: + x11.focus(window) + return + direct.unsupported() + + proc captureMouse*(window: Window) = + when HasX11: + if window.direct == nil: + x11.captureMouse(window) + return + direct.unsupported() + + proc releaseMouse*(window: Window) = + when HasX11: + if window.direct == nil: + x11.releaseMouse(window) + return + direct.unsupported() + + proc closeIme*(window: Window) = + when HasX11: + if window.direct == nil: + x11.closeIme(window) + return + direct.unsupported() + + proc `visible=`*(window: Window, value: bool) = + when HasX11: + if window.direct == nil: + x11.`visible=`(window, value) + return + direct.unsupported() + + proc `pos=`*(window: Window, value: IVec2) = + when HasX11: + if window.direct == nil: + x11.`pos=`(window, value) + return + direct.unsupported() + + proc `size=`*(window: Window, value: IVec2) = + when HasX11: + if window.direct == nil: + x11.`size=`(window, value) + return + direct.unsupported() + + proc `maximized=`*(window: Window, value: bool) = + when HasX11: + if window.direct == nil: + x11.`maximized=`(window, value) + return + direct.unsupported() + + proc `minimized=`*(window: Window, value: bool) = + when HasX11: + if window.direct == nil: + x11.`minimized=`(window, value) + return + direct.unsupported() + + proc `fullscreen=`*(window: Window, value: bool) = + when HasX11: + if window.direct == nil: + x11.`fullscreen=`(window, value) + return + direct.unsupported() + + proc `style=`*(window: Window, value: WindowStyle) = + when HasX11: + if window.direct == nil: + x11.`style=`(window, value) + return + direct.unsupported() + + proc `runeInputEnabled=`*(window: Window, value: bool) = + when HasX11: + if window.direct == nil: + x11.`runeInputEnabled=`(window, value) + return + direct.unsupported() + + proc `cursor=`*(window: Window, value: Cursor) = + when HasX11: + if window.direct == nil: + x11.`cursor=`(window, value) + return + direct.unsupported() + + proc `icon=`*(window: Window, value: Image) = + when HasX11: + if window.direct == nil: + x11.`icon=`(window, value) + return + direct.unsupported() + + proc getClipboardString*(): string = + if selected != X11Backend: direct.unsupported() + when HasX11: + return x11.getClipboardString() + else: direct.unsupported() + + proc setClipboardString*(value: string) = + if selected != X11Backend: direct.unsupported() + when HasX11: + x11.setClipboardString(value) + else: direct.unsupported() + + proc getClipboardContentKinds*(): set[ClipboardContentKind] = + if selected != X11Backend: direct.unsupported() + when HasX11: + return x11.getClipboardContentKinds() + else: direct.unsupported() + + proc getClipboardImage*(): Image = + if selected != X11Backend: direct.unsupported() + when HasX11: + return x11.getClipboardImage() + else: direct.unsupported() + + proc getConfigHome*(appName: string): string = + if selected != X11Backend: direct.unsupported() + when HasX11: + return x11.getConfigHome(appName) + else: direct.unsupported() + + proc getConfig*(appName, fileName: string): string = + if selected != X11Backend: direct.unsupported() + when HasX11: + return x11.getConfig(appName, fileName) + else: direct.unsupported() + + proc setConfig*(appName, fileName, content: string) = + if selected != X11Backend: direct.unsupported() + when HasX11: + x11.setConfig(appName, fileName, content) + else: direct.unsupported() + + proc openUrl*(url: string) = + if selected != X11Backend: direct.unsupported() + when HasX11: + x11.openUrl(url) + else: direct.unsupported() + + proc openTempTextFile*(title, text: string) = + if selected != X11Backend: direct.unsupported() + when HasX11: + x11.openTempTextFile(title, text) + else: direct.unsupported() + + proc openFileDialog*(title = "Open File", filters: seq[FileDialogFilter] = @[], defaultPath = ""): string = + if selected != X11Backend: direct.unsupported() + when HasX11: + return x11.openFileDialog(title, filters, defaultPath) + else: direct.unsupported() + + proc saveFileDialog*(title = "Save File", filters: seq[FileDialogFilter] = @[], defaultPath = "", defaultName = ""): string = + if selected != X11Backend: direct.unsupported() + when HasX11: + return x11.saveFileDialog(title, filters, defaultPath, defaultName) + else: direct.unsupported() diff --git a/src/windy/platforms/linux/x11.nim b/src/windy/platforms/linux/x11.nim index 5b29e3cd..add90913 100644 --- a/src/windy/platforms/linux/x11.nim +++ b/src/windy/platforms/linux/x11.nim @@ -7,12 +7,19 @@ import import ../../http export http +import ./backends +when HasDirect: + import ./native/state + type XWindow = x.Window Window* = ref object + when HasDirect: + direct*: DirectWindowState onCloseRequest*: Callback onFrame*: Callback + onTouch*: TouchCallback onMove*: Callback onResize*: Callback onFocusChange*: Callback @@ -734,7 +741,8 @@ proc newWindow*( openglVersion = OpenGL4Dot1, msaa = msaaDisabled, depthBits = 24, - stencilBits = 8 + stencilBits = 8, + linuxOptions = LinuxWindowOptions() ): Window = ## Creates a new window. Intitializes Windy if needed. init() @@ -802,10 +810,10 @@ proc newWindow*( var gcv: XGCValues result.gc = display.XCreateGC(result.handle, GCForeground or GCBackground, gcv.addr) - result.ctx = display.glXCreateContext(vi.addr, nil, 1) - - if result.ctx == nil: - raise WindyError.newException("Error creating OpenGL context") + when not defined(useCpu): + result.ctx = display.glXCreateContext(vi.addr, nil, 1) + if result.ctx == nil: + raise WindyError.newException("Error creating OpenGL context") result.title = title @@ -817,18 +825,17 @@ proc newWindow*( const xdndVersion = 5'u32 result.handle.setProperty(xaXdndAware, xaCardinal, 32, @[xdndVersion].asString) - makeContextCurrent result - - if vsync: - if glXSwapIntervalEXT != nil: - display.glXSwapIntervalEXT(result.handle, 1) - elif glXSwapIntervalMESA != nil: - glXSwapIntervalMESA(1) - elif glXSwapIntervalSGI != nil: - glXSwapIntervalSGI(1) - else: - raise WindyError.newException("VSync is not supported") - + when not defined(useCpu): + makeContextCurrent result + if vsync: + if glXSwapIntervalEXT != nil: + display.glXSwapIntervalEXT(result.handle, 1) + elif glXSwapIntervalMESA != nil: + glXSwapIntervalMESA(1) + elif glXSwapIntervalSGI != nil: + glXSwapIntervalSGI(1) + else: + raise WindyError.newException("VSync is not supported") if visible: result.visible = true @@ -1455,3 +1462,62 @@ proc saveFileDialog*( args.add("--file-filter=" & filter.name & " | " & filter.extensions.replace(";", " ").replace(",", " ")) result = runZenity(args) + +proc touches*(window: Window): seq[TouchContact] = + @[] +when defined(useCpu): + type CpuImage = object + width, height, xoffset, format: cint + data: cstring + byteOrder, bitmapUnit, bitmapBitOrder, bitmapPad, depth: cint + bytesPerLine, bitsPerPixel: cint + redMask, greenMask, blueMask: culong + obdata: pointer + functions: array[6, pointer] + proc createImage(d: Display, visual: ptr Visual, depth: cuint, format: cint, + offset: cint, data: cstring, width, height: cuint, pad, stride: cint): ptr CpuImage + {.cdecl, importc: "XCreateImage", dynlib: libX11.} + proc putImage(d: Display, window: XWindow, gc: GC, image: ptr CpuImage, + sx, sy, dx, dy: cint, width, height: cuint): cint + {.cdecl, importc: "XPutImage", dynlib: libX11.} + proc putPixel(image: ptr CpuImage, x, y: cint, pixel: culong): cint + {.cdecl, importc: "XPutPixel", dynlib: libX11.} + proc destroyImage(image: ptr CpuImage): cint + {.cdecl, importc: "XDestroyImage", dynlib: libX11.} + proc malloc(size: csize_t): pointer {.importc, header: "".} + + proc channel(value: uint8, mask: culong): culong = + var bits = mask + var shift = 0 + while bits != 0 and (bits and 1) == 0: + bits = bits shr 1 + inc shift + ((culong(value) * bits + 127) div 255 shl shift) and mask + + proc presentPixels*(window: Window, image: Image) = + if window.closed: + raise newException(WindyError, "Cannot present to a closed window") + if image == nil or image.width <= 0 or image.height <= 0: + raise newException(WindyError, "Invalid pixel image") + var attributes: XWindowAttributes + display.XGetWindowAttributes(window.handle, addr attributes) + let native = createImage(display, attributes.visual, cuint(attributes.depth), + 2, 0, nil, cuint(image.width), cuint(image.height), 32, 0) + if native == nil: + raise newException(WindyError, "Cannot create X11 pixel image") + defer: discard destroyImage(native) + native.data = cast[cstring](malloc(csize_t(native.bytesPerLine) * csize_t(image.height))) + if native.data == nil: + raise newException(WindyError, "Cannot allocate X11 pixel image") + for y in 0 ..< image.height: + for x in 0 ..< image.width: + let pixel = image.data[y * image.width + x] + discard putPixel(native, cint(x), cint(y), channel(pixel.r, native.redMask) or + channel(pixel.g, native.greenMask) or channel(pixel.b, native.blueMask)) + discard putImage(display, window.handle, window.gc, native, 0, 0, 0, 0, + cuint(image.width), cuint(image.height)) + display.XFlush() + +proc hasWindows*(): bool = + for window in windows: + if not window.closed: return true diff --git a/src/windy/platforms/macos/platform.nim b/src/windy/platforms/macos/platform.nim index 2e09ee29..8f9e858b 100644 --- a/src/windy/platforms/macos/platform.nim +++ b/src/windy/platforms/macos/platform.nim @@ -19,6 +19,7 @@ type Window* = ref object onCloseRequest*: Callback onFrame*: Callback + onTouch*: TouchCallback onMove*: Callback onResize*: Callback onFocusChange*: Callback @@ -1748,3 +1749,6 @@ proc saveFileDialog*( let url = panel.URL() if url.int != 0: result = $url.path() + +proc touches*(window: Window): seq[TouchContact] = + @[] diff --git a/src/windy/platforms/win32/platform.nim b/src/windy/platforms/win32/platform.nim index e790375b..0c60ff1d 100644 --- a/src/windy/platforms/win32/platform.nim +++ b/src/windy/platforms/win32/platform.nim @@ -65,6 +65,7 @@ type Window* = ref object onCloseRequest*: Callback onFrame*: Callback + onTouch*: TouchCallback onMove*: Callback onResize*: Callback onFocusChange*: Callback @@ -2835,3 +2836,6 @@ proc openTempTextFile*(title, text: string) = createDir("tmp") writeFile("tmp/" & title, text) discard execShellCmd("notepad tmp/" & title) + +proc touches*(window: Window): seq[TouchContact] = + @[] diff --git a/tests/bench_native_upload.nim b/tests/bench_native_upload.nim new file mode 100644 index 00000000..cfb68a19 --- /dev/null +++ b/tests/bench_native_upload.nim @@ -0,0 +1,18 @@ +import + std/[monotimes, os, times], + pixie, + windy/platforms/linux/native/drm + +proc main() = + doAssert paramCount() == 1, "Choose a DRM connector." + let display = openDrm("/dev/dri/card0") + defer: display.close() + display.openOutput(@[paramStr(1)], 0) + let image = newImage(display.screens[0].width, display.screens[0].height) + image.fill(color(0.2, 0.4, 0.8, 1)) + let started = getMonoTime() + for iteration in 0 ..< 20: + display.upload(0, image) + echo "Upload mean us: ", (getMonoTime() - started).inMicroseconds div 20 + +main() diff --git a/tests/config.nims b/tests/config.nims index 2105526e..e35ab193 100644 --- a/tests/config.nims +++ b/tests/config.nims @@ -2,3 +2,15 @@ when defined(emscripten): include "../examples/config.nims" + +import std/os +let testBackends = getEnv("WINDY_TEST_BACKENDS") +if testBackends.len > 0: + switch("define", "useCpu") + switch("define", "windyBackends=" & testBackends) + +if getEnv("WINDY_TEST_TARGET") == "arm64": + switch("cpu", "arm64") + switch("gcc.exe", getEnv("MONO_RETRO_CC", "aarch64-unknown-linux-gnu-gcc")) + switch("gcc.linkerexe", getEnv("MONO_RETRO_CC", "aarch64-unknown-linux-gnu-gcc")) + switch("passL", "-Wl,--dynamic-linker=/lib/ld-linux-aarch64.so.1") diff --git a/tests/manual_direct_displays.nim b/tests/manual_direct_displays.nim new file mode 100644 index 00000000..497b1213 --- /dev/null +++ b/tests/manual_direct_displays.nim @@ -0,0 +1,36 @@ +import + std/os, + pixie, windy + +proc main() = + let useDrm = paramStr(1) == "drm" + if useDrm: + doAssert paramCount() == 3, "Choose two DRM connectors." + setLinuxBackend(if useDrm: DrmBackend else: FbdevBackend) + for attempt in 0 .. 1: + let first = newWindow("First output", ivec2(0, 0), vsync = false, + linuxOptions = LinuxWindowOptions(output: paramStr(2))) + var image = newImage(first.framebufferSize.x, first.framebufferSize.y) + image.fill(color(1, 0, 0, 1)) + first.presentPixels(image) + if useDrm: + let second = newWindow("Second output", ivec2(0, 0), vsync = false, + linuxOptions = LinuxWindowOptions(output: paramStr(3))) + image = newImage(second.framebufferSize.x, second.framebufferSize.y) + image.fill(color(0, 1, 0, 1)) + second.presentPixels(image) + var rejected = false + try: + discard newWindow("Duplicate", ivec2(0, 0), vsync = false, + linuxOptions = LinuxWindowOptions(output: paramStr(3))) + except IOError: rejected = true + doAssert rejected + first.close() + image.fill(color(0, 0, 1, 1)) + second.presentPixels(image) + second.close() + else: + first.close() + doAssert first.closed + +main() diff --git a/tests/test_linux_display.nim b/tests/test_linux_display.nim new file mode 100644 index 00000000..2c0ecb79 --- /dev/null +++ b/tests/test_linux_display.nim @@ -0,0 +1,52 @@ +import + std/os, + pixie, windy + +when HasX11: + import windy/platforms/linux/x11/[xlib, x] + + proc getImage(d: xlib.Display, drawable: x.Drawable, x, y: cint, + width, height: cuint, planes: culong, format: cint): pointer + {.cdecl, importc: "XGetImage", dynlib: libX11.} + proc getPixel(image: pointer, x, y: cint): culong + {.cdecl, importc: "XGetPixel", dynlib: libX11.} + proc destroyImage(image: pointer): cint + {.cdecl, importc: "XDestroyImage", dynlib: libX11.} + proc closeDisplay(d: xlib.Display): cint + {.cdecl, importc: "XCloseDisplay", dynlib: libX11.} + + setLinuxBackend(X11Backend) + let window = newWindow("Windy CPU regression", ivec2(64, 48), vsync = false) + let image = newImage(64, 48) + image.fill(color(1, 0, 0, 1)) + window.presentPixels(image) + pollEvents() + doAssert window.framebufferSize == ivec2(64, 48) + doAssert window.touches.len == 0 + let d = XOpenDisplay(cstring(getEnv("DISPLAY"))) + doAssert d != nil + d.XSync(false) + let captured = getImage(d, d.defaultRootWindow, 0, 0, 1, 1, culong.high, 2) + doAssert captured != nil + doAssert getPixel(captured, 0, 0) == 0xff0000 + discard destroyImage(captured) + discard closeDisplay(d) + when HasDirect: + var rejected = false + try: setLinuxBackend(FbdevBackend) + except WindyError: rejected = true + doAssert rejected + window.close() + pollEvents() + doAssert window.closed + +when HasFbdev: + setLinuxBackend(FbdevBackend) + var rejectedFramebuffer = false + try: + discard newWindow("Invalid framebuffer", ivec2(0, 0), vsync = false, + linuxOptions = LinuxWindowOptions(device: "/dev/null")) + except OSError: + rejectedFramebuffer = true + doAssert rejectedFramebuffer + setLinuxBackend(FbdevBackend) diff --git a/tests/test_linux_glx.nim b/tests/test_linux_glx.nim new file mode 100644 index 00000000..80777c0d --- /dev/null +++ b/tests/test_linux_glx.nim @@ -0,0 +1,13 @@ +import opengl, windy + +let window = newWindow("Windy GLX regression", ivec2(64, 48), vsync = false) +window.makeContextCurrent() +loadExtensions() +doAssert glGetString(GL_VERSION) != nil +for frame in 0 ..< 3: + glClearColor(0.2, 0.4, 0.8, 1) + glClear(GL_COLOR_BUFFER_BIT) + window.swapBuffers() + pollEvents() +window.close() +doAssert window.closed diff --git a/tests/test_native_input.nim b/tests/test_native_input.nim new file mode 100644 index 00000000..cb3c67f6 --- /dev/null +++ b/tests/test_native_input.nim @@ -0,0 +1,59 @@ +import + pixie, + windy/common, + windy/platforms/linux/native/[touch, pixels, framebuffer, drm] + +block: + let reader = TouchReader(width: 1024, height: 768, + minimumX: 100, maximumX: 1100, minimumY: 50, maximumY: 1050, + pending: @[Slot(id: -1), Slot(id: -1)], previous: @[Slot(id: -1), Slot(id: -1)]) + reader.pending[0] = Slot(id: 7, x: 100, y: 50) + reader.pending[1] = Slot(id: 9, x: 1100, y: 1050) + let began = reader.report() + doAssert began.len == 2 + doAssert began[0].phase == TouchBegin and began[0].contact.id == 7 + doAssert began[0].contact.position == vec2(0, 0) + doAssert began[1].contact.position == vec2(1023, 767) + doAssert reader.report().len == 0 + reader.pending[0].x = 600 + let moved = reader.report() + doAssert moved.len == 1 and moved[0].phase == TouchMove + doAssert moved[0].contact.id == 7 and moved[0].contact.position.x == 511 + reader.pending[0].id = 11 + let replaced = reader.report() + doAssert replaced.len == 2 + doAssert replaced[0].phase == TouchEnd and replaced[0].contact.id == 7 + doAssert replaced[1].phase == TouchBegin and replaced[1].contact.id == 11 + let cancelled = reader.cancel() + doAssert cancelled.len == 2 + for event in cancelled: doAssert event.phase == TouchCancel + doAssert reader.cancel().len == 0 + doAssert reader.report().len == 0 + +block: + for rotation in [0, 90, 180, 270]: + let reader = TouchReader(width: 4, height: 3, maximumX: 100, maximumY: 100, + rotation: rotation, pending: @[Slot(id: 1)], previous: @[Slot(id: -1)]) + let expected = case rotation + of 0: vec2(0, 0) + of 90: vec2(3, 0) + of 180: vec2(3, 2) + else: vec2(0, 2) + doAssert reader.report()[0].contact.position == expected + +block: + doAssert rgb(rgbx(255, 0, 0, 255)) == 0xff0000'u32 + doAssert rgb(rgbx(0, 255, 0, 255)) == 0x00ff00'u32 + doAssert rgb(rgbx(0, 0, 255, 255)) == 0x0000ff'u32 + doAssert packPixel(0xffffff, Bitfield(offset: 11, length: 5), + Bitfield(offset: 5, length: 6), Bitfield(length: 5), Bitfield()) == 0xffff + doAssert chooseCrtc([90'u32, 110, 130], 130, 5, []) == 130 + doAssert chooseCrtc([90'u32, 110, 130], 0, 5, [90'u32]) == 130 + +block: + var rejected = false + try: + discard openTouch("/dev/null", 20, 20, 0) + except OSError: + rejected = true + doAssert rejected From e9ed10a56f0e0da57ed6d1e68be947d1ad8a03ae Mon Sep 17 00:00:00 2001 From: monofuel Date: Tue, 27 Oct 2026 20:05:55 -0400 Subject: [PATCH 2/5] Speed up X11 CPU pixel uploads with packed row writes --- src/windy/platforms/linux/x11.nim | 20 +++++++++++++++++--- tests/test_linux_display.nim | 15 +++++++++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/windy/platforms/linux/x11.nim b/src/windy/platforms/linux/x11.nim index add90913..2e3cb3d6 100644 --- a/src/windy/platforms/linux/x11.nim +++ b/src/windy/platforms/linux/x11.nim @@ -1466,6 +1466,8 @@ proc saveFileDialog*( proc touches*(window: Window): seq[TouchContact] = @[] when defined(useCpu): + const NativeByteOrder = when cpuEndian == littleEndian: 0 else: 1 + type CpuImage = object width, height, xoffset, format: cint data: cstring @@ -1509,11 +1511,23 @@ when defined(useCpu): native.data = cast[cstring](malloc(csize_t(native.bytesPerLine) * csize_t(image.height))) if native.data == nil: raise newException(WindyError, "Cannot allocate X11 pixel image") + let direct = native.bitsPerPixel == 32 and native.byteOrder == NativeByteOrder + var channels: array[3, array[256, culong]] + for value in 0 .. 255: + channels[0][value] = channel(uint8(value), native.redMask) + channels[1][value] = channel(uint8(value), native.greenMask) + channels[2][value] = channel(uint8(value), native.blueMask) + let destination = cast[ptr UncheckedArray[byte]](native.data) for y in 0 ..< image.height: + let row = cast[ptr UncheckedArray[uint32]](addr destination[y * int(native.bytesPerLine)]) for x in 0 ..< image.width: - let pixel = image.data[y * image.width + x] - discard putPixel(native, cint(x), cint(y), channel(pixel.r, native.redMask) or - channel(pixel.g, native.greenMask) or channel(pixel.b, native.blueMask)) + let + pixel = image.data[y * image.width + x] + packed = channels[0][pixel.r] or channels[1][pixel.g] or channels[2][pixel.b] + if direct: + row[x] = uint32(packed) + else: + discard putPixel(native, cint(x), cint(y), packed) discard putImage(display, window.handle, window.gc, native, 0, 0, 0, 0, cuint(image.width), cuint(image.height)) display.XFlush() diff --git a/tests/test_linux_display.nim b/tests/test_linux_display.nim index 2c0ecb79..bb32352b 100644 --- a/tests/test_linux_display.nim +++ b/tests/test_linux_display.nim @@ -17,8 +17,10 @@ when HasX11: setLinuxBackend(X11Backend) let window = newWindow("Windy CPU regression", ivec2(64, 48), vsync = false) - let image = newImage(64, 48) - image.fill(color(1, 0, 0, 1)) + let image = newImage(63, 47) + for index in 0 ..< image.data.len: + image.data[index] = rgbx(uint8(index mod 256), uint8((index div 63) * 5), + uint8((index mod 8) * 31), 255) window.presentPixels(image) pollEvents() doAssert window.framebufferSize == ivec2(64, 48) @@ -26,9 +28,14 @@ when HasX11: let d = XOpenDisplay(cstring(getEnv("DISPLAY"))) doAssert d != nil d.XSync(false) - let captured = getImage(d, d.defaultRootWindow, 0, 0, 1, 1, culong.high, 2) + let captured = getImage(d, d.defaultRootWindow, 0, 0, 63, 47, culong.high, 2) doAssert captured != nil - doAssert getPixel(captured, 0, 0) == 0xff0000 + for y in 0 ..< image.height: + for x in 0 ..< image.width: + let + pixel = image.data[y * image.width + x] + expected = culong(pixel.r) shl 16 or culong(pixel.g) shl 8 or culong(pixel.b) + doAssert getPixel(captured, cint(x), cint(y)) == expected discard destroyImage(captured) discard closeDisplay(d) when HasDirect: From 83699dd298ca92043255177af6abc1ff089fa6a0 Mon Sep 17 00:00:00 2001 From: monofuel Date: Tue, 27 Oct 2026 20:53:44 -0400 Subject: [PATCH 3/5] Speed up standard 32-bit framebuffer uploads --- Makefile | 1 + .../platforms/linux/native/framebuffer.nim | 31 +++++++++---- tests/test_linux_framebuffer.nim | 44 +++++++++++++++++++ 3 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 tests/test_linux_framebuffer.nim diff --git a/Makefile b/Makefile index 39570ac5..8f3470e5 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ .PHONY: test-native test-linux-cpu test-linux-combined test-native: nim r tests/test_native_input.nim + nim r tests/test_linux_framebuffer.nim test-linux-cpu: WINDY_TEST_BACKENDS=x11 nim r tests/test_linux_display.nim test-linux-combined: diff --git a/src/windy/platforms/linux/native/framebuffer.nim b/src/windy/platforms/linux/native/framebuffer.nim index 56f8afb2..05512e10 100644 --- a/src/windy/platforms/linux/native/framebuffer.nim +++ b/src/windy/platforms/linux/native/framebuffer.nim @@ -113,15 +113,30 @@ proc present*(fb: Framebuffer, image: Image) = bytesPerPixel = int(fb.variable.bits_per_pixel div 8) stride = int(fb.fixed.line_length) fb.touched = true - for y in 0 ..< image.height: - for x in 0 ..< image.width: + if cpuEndian == littleEndian and bytesPerPixel == 4 and stride mod 4 == 0 and fb.rotation == 0 and + fb.variable.red.length == 8 and fb.variable.green.length == 8 and + fb.variable.blue.length == 8: + let alpha = packPixel(0, fb.variable.red, fb.variable.green, + fb.variable.blue, fb.variable.transp) + for y in 0 ..< image.height: let - point = rotatedPoint(x, y, image.width, image.height, fb.rotation) - offset = point.y * stride + point.x * bytesPerPixel - pixel = packPixel(rgb(image.data[y * image.width + x]), fb.variable.red, - fb.variable.green, fb.variable.blue, fb.variable.transp) - for index in 0 ..< bytesPerPixel: - destination[offset + index] = byte((pixel shr (index * 8)) and 255) + source = cast[ptr UncheckedArray[ColorRGBX]](unsafeAddr image.data[y * image.width]) + row = cast[ptr UncheckedArray[uint32]](addr destination[y * stride]) + for x in 0 ..< image.width: + let pixel = source[x] + row[x] = uint32(pixel.r) shl fb.variable.red.offset or + uint32(pixel.g) shl fb.variable.green.offset or + uint32(pixel.b) shl fb.variable.blue.offset or alpha + else: + for y in 0 ..< image.height: + for x in 0 ..< image.width: + let + point = rotatedPoint(x, y, image.width, image.height, fb.rotation) + offset = point.y * stride + point.x * bytesPerPixel + pixel = packPixel(rgb(image.data[y * image.width + x]), fb.variable.red, + fb.variable.green, fb.variable.blue, fb.variable.transp) + for index in 0 ..< bytesPerPixel: + destination[offset + index] = byte((pixel shr (index * 8)) and 255) if not fb.activated: fb.variable.xoffset = 0 fb.variable.yoffset = 0 diff --git a/tests/test_linux_framebuffer.nim b/tests/test_linux_framebuffer.nim new file mode 100644 index 00000000..bfab1262 --- /dev/null +++ b/tests/test_linux_framebuffer.nim @@ -0,0 +1,44 @@ +include windy/platforms/linux/native/framebuffer + +proc checkPixels(bits: int, red, green, blue, alpha: Bitfield, rotation: int) = + ## Compare packed scanout bytes with the generic converter, including padding. + let + image = newImage(31, 17) + physicalWidth = if rotation in [90, 270]: image.height else: image.width + physicalHeight = if rotation in [90, 270]: image.width else: image.height + bytes = bits div 8 + stride = ((physicalWidth * bytes + 3) div 4 + 1) * 4 + var + memory = newSeq[byte](stride * physicalHeight + 8) + expected = newSeq[byte](memory.len) + for index in 0 ..< memory.len: + memory[index] = 0xa5 + expected[index] = 0xa5 + for y in 0 ..< image.height: + for x in 0 ..< image.width: + let + pixel = rgbx(uint8(x * 8), uint8(y * 15), uint8((x + y) * 5), 127) + packed = packPixel(rgb(pixel), red, green, blue, alpha) + point = rotatedPoint(x, y, image.width, image.height, rotation) + offset = 4 + point.y * stride + point.x * bytes + image.data[y * image.width + x] = pixel + for index in 0 ..< bytes: + expected[offset + index] = byte((packed shr (index * 8)) and 255) + let fb = Framebuffer(memory: addr memory[4], width: image.width, + height: image.height, rotation: rotation, activated: true, + variable: VariableInfo(bits_per_pixel: uint32(bits), red: red, + green: green, blue: blue, transp: alpha), + fixed: FixedInfo(line_length: uint32(stride))) + fb.present(image) + doAssert memory == expected + +for rotation in [0, 90, 180, 270]: + for redOffset in [0'u32, 16]: + for alphaBits in [0'u32, 8]: + checkPixels(32, Bitfield(offset: redOffset, length: 8), + Bitfield(offset: 8, length: 8), Bitfield(offset: 16 - redOffset, length: 8), + Bitfield(offset: 24, length: alphaBits), rotation) + checkPixels(24, Bitfield(offset: 16, length: 8), Bitfield(offset: 8, length: 8), + Bitfield(length: 8), Bitfield(), rotation) + checkPixels(16, Bitfield(offset: 11, length: 5), Bitfield(offset: 5, length: 6), + Bitfield(length: 5), Bitfield(), rotation) From 0c34bd7379f44d5b005786147013b2a036258bce Mon Sep 17 00:00:00 2001 From: monofuel Date: Tue, 27 Oct 2026 22:02:21 -0400 Subject: [PATCH 4/5] Use Xlib int return types for event predicates --- src/windy/platforms/linux/x11.nim | 12 ++++++------ src/windy/platforms/linux/x11/xevent.nim | 4 ++-- tests/test_linux_display.nim | 12 ++++++++++++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/windy/platforms/linux/x11.nim b/src/windy/platforms/linux/x11.nim index 2e3cb3d6..46c09ab0 100644 --- a/src/windy/platforms/linux/x11.nim +++ b/src/windy/platforms/linux/x11.nim @@ -872,8 +872,8 @@ proc pollEvents(window: Window) = d: Display, event: ptr XEvent, userData: pointer - ): bool {.cdecl.} = - event.any.window == cast[Window](userData).handle + ): cint {.cdecl.} = + cint(event.any.window == cast[Window](userData).handle) template pushButtonEvent( button: Button, @@ -895,7 +895,7 @@ proc pollEvents(window: Window) = proc handleRune(window: Window, rune: Rune) = handleRuneTemplate() - while display.XCheckIfEvent(ev.addr, checkEvent, cast[pointer](window)): + while display.XCheckIfEvent(ev.addr, checkEvent, cast[pointer](window)) != 0: case ev.kind of xeClientMessage: @@ -1231,12 +1231,12 @@ proc processClipboardEvents: bool = d: Display, event: ptr XEvent, userData: pointer - ): bool {.cdecl.} = - event.any.window == cast[XWindow](userData) + ): cint {.cdecl.} = + cint(event.any.window == cast[XWindow](userData)) while display.XCheckIfEvent( ev.addr, checkEvent, cast[pointer](clipboardWindow) - ): + ) != 0: case ev.kind of xeSelection: template e: untyped = ev.selection diff --git a/src/windy/platforms/linux/x11/xevent.nim b/src/windy/platforms/linux/x11/xevent.nim index 1147b566..ac0a20da 100644 --- a/src/windy/platforms/linux/x11/xevent.nim +++ b/src/windy/platforms/linux/x11/xevent.nim @@ -5,7 +5,7 @@ type d: Display, event: ptr XEvent, p: pointer - ): bool {.cdecl.} + ): cint {.cdecl.} ErrorHandleProc* = proc (d: Display, event: ptr XErrorEvent): bool {.cdecl.} XEventKind* {.size: cint.sizeof.} = enum @@ -432,7 +432,7 @@ type {.push, cdecl, dynlib: libX11, importc.} proc XCheckIfEvent*(d: Display; e: ptr XEvent, cb: IfEventProc, - userData: pointer): bool + userData: pointer): cint proc XSendEvent*(d: Display; window: Window, propogate: bool, mask: clong, e: ptr XEvent) proc Xutf8LookupString*(ic: XIC, e: ptr XKeyEvent, buffer: cstring, len: cint, diff --git a/tests/test_linux_display.nim b/tests/test_linux_display.nim index bb32352b..7aa2c00c 100644 --- a/tests/test_linux_display.nim +++ b/tests/test_linux_display.nim @@ -38,6 +38,18 @@ when HasX11: doAssert getPixel(captured, cint(x), cint(y)) == expected discard destroyImage(captured) discard closeDisplay(d) + let other = newWindow("Windy second CPU window", ivec2(64, 48), vsync = false) + other.focus() + for attempt in 0 ..< 20: + pollEvents() + sleep(1) + doAssert other.focused and not window.focused + window.focus() + for attempt in 0 ..< 20: + pollEvents() + sleep(1) + doAssert window.focused and not other.focused + other.close() when HasDirect: var rejected = false try: setLinuxBackend(FbdevBackend) From 799e062f90743a0931aae74217c19875a69be8ef Mon Sep 17 00:00:00 2001 From: monofuel Date: Thu, 29 Oct 2026 19:24:00 -0400 Subject: [PATCH 5/5] Support vblank page flips for DRM pixel windows --- Makefile | 1 + docs/linux-direct.md | 15 +- src/windy/platforms/linux/direct.nim | 4 +- src/windy/platforms/linux/native/drm.nim | 173 ++++++++++++++++------- src/windy/platforms/linux/platform.nim | 6 +- tests/manual_direct_displays.nim | 7 +- tests/test_linux_drm.nim | 135 ++++++++++++++++++ 7 files changed, 278 insertions(+), 63 deletions(-) create mode 100644 tests/test_linux_drm.nim diff --git a/Makefile b/Makefile index 8f3470e5..643eddbe 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ 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: diff --git a/docs/linux-direct.md b/docs/linux-direct.md index 779dd1f0..8d0bae26 100644 --- a/docs/linux-direct.md +++ b/docs/linux-direct.md @@ -20,7 +20,7 @@ Select the backend before creating windows: import pixie, windy setLinuxBackend(DrmBackend) -let window = newWindow("Panel", ivec2(0, 0), vsync = false, +let window = newWindow("Panel", ivec2(0, 0), vsync = true, linuxOptions = LinuxWindowOptions( device: "/dev/dri/card0", output: "DSI-1", @@ -52,8 +52,17 @@ 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 +DRM windows with `vsync=true` use two buffers. Presentation uploads into the +non-visible buffer and queues a switch at vblank. A subsequent upload waits for +the previous switch before reusing storage, with at most one pending flip per +screen. Outputs remain independent; the backend dispatches completion events for +all windows sharing the DRM device. Applications still control their frame rate. +Page-flip errors and a one-second completion timeout propagate to the caller. +Each additional buffer uses the panel's native width, pitch and height (about +3 MiB for a 1024×768 32-bit panel). + +DRM with `vsync=false` and fbdev use direct scanout writes, which can tear. +fbdev requires `vsync=false`. Normal close waits for pending DRM flips and restores the saved display state when it is still available. If the kernel retired the previous DRM framebuffer, the output is disabled instead. diff --git a/src/windy/platforms/linux/direct.nim b/src/windy/platforms/linux/direct.nim index 24eef9bd..4b099475 100644 --- a/src/windy/platforms/linux/direct.nim +++ b/src/windy/platforms/linux/direct.nim @@ -51,7 +51,7 @@ proc close*(window: Window) = break proc newDirectWindow*(title: string, backend: LinuxBackend, - options: LinuxWindowOptions): Window = + options: LinuxWindowOptions, vsync = false): 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)) @@ -71,7 +71,7 @@ proc newDirectWindow*(title: string, backend: LinuxBackend, break if data.drm == nil: data.drm = openDrm(path) - data.drm.openOutput(@[options.output], options.rotation) + data.drm.openOutput(@[options.output], options.rotation, vsync) data.screen = data.drm.screens[^1] if options.touchDevice.len > 0: for window in windows: diff --git a/src/windy/platforms/linux/native/drm.nim b/src/windy/platforms/linux/native/drm.nim index 15fe4257..9a0e13a8 100644 --- a/src/windy/platforms/linux/native/drm.nim +++ b/src/windy/platforms/linux/native/drm.nim @@ -1,5 +1,5 @@ import - std/[dynlib, os, posix, strformat, strutils], + std/[dynlib, monotimes, os, posix, strformat, strutils, times], pixie, ./[framebuffer, pixels] @@ -68,6 +68,16 @@ type enums: pointer countBlobs: cint blobs: pointer + FlipHandler = proc(fd: cint, sequence, seconds, microseconds: cuint, + data: pointer) {.cdecl.} + EventContext = object + # Version 2 uses only this prefix of libdrm's extensible event context. + version: cint + vblankHandler, pageFlipHandler: FlipHandler + Scanout = object + buffer: DumbBuffer + memory: pointer + framebuffer: uint32 DrmApi = object library: LibHandle getResources: proc(fd: cint): ptr Resources {.cdecl.} @@ -80,6 +90,9 @@ type freeCrtc: proc(value: ptr Crtc) {.cdecl.} setCrtc: proc(fd: cint, id, buffer, x, y: uint32, connectors: ptr uint32, count: cint, mode: ptr ModeInfo): cint {.cdecl.} + pageFlip: proc(fd: cint, crtc, framebuffer, flags: uint32, + data: pointer): cint {.cdecl.} + handleEvent: proc(fd: cint, context: ptr EventContext): cint {.cdecl.} addFb: proc(fd: cint, width, height: uint32, depth, bpp: uint8, pitch, handle: uint32, id: ptr uint32): cint {.cdecl.} removeFb: proc(fd: cint, id: uint32): cint {.cdecl.} @@ -101,12 +114,13 @@ type name*: string width*, height*: int rotation: int - connector, crtc, framebuffer: uint32 + connector, crtc: uint32 mode: ModeInfo original: Crtc originalConnectors: seq[uint32] - buffer: DumbBuffer - memory: pointer + buffers: seq[Scanout] + front, draw: int + vsync, pending: bool pixels: seq[uint32] rgba: bool activated: bool @@ -115,8 +129,11 @@ type fd: cint master: bool screens*: seq[DrmScreen] + retiredScreens: seq[DrmScreen] const + FlipEvent = 1'u32 + FlipTimeoutMs = 1000 CreateDumb = 0xc02064b2.culong MapDumb = 0xc01064b3.culong DestroyDumb = 0xc00464b4.culong @@ -141,6 +158,7 @@ static: doAssert sizeof(MapBuffer) == 16 doAssert sizeof(FramebufferInfo) == 28 doAssert sizeof(GemClose) == 8 + doAssert sizeof(EventContext) == 24 proc loadDrm(): DrmApi = ## Load DRM only when a DRM display is requested. @@ -161,6 +179,8 @@ proc loadDrm(): DrmApi = bindSymbol(getCrtc, "drmModeGetCrtc") bindSymbol(freeCrtc, "drmModeFreeCrtc") bindSymbol(setCrtc, "drmModeSetCrtc") + bindSymbol(pageFlip, "drmModePageFlip") + bindSymbol(handleEvent, "drmHandleEvent") bindSymbol(addFb, "drmModeAddFB") bindSymbol(removeFb, "drmModeRmFB") bindSymbol(getFb, "drmModeGetFB") @@ -207,8 +227,42 @@ proc rgbaFormat(display: DrmDisplay, crtcIndex: int): uint32 = for formatIndex in 0 ..< int(plane.countFormats): if plane.formats[formatIndex] == format: return format +proc flipComplete(fd: cint, sequence, seconds, microseconds: cuint, + data: pointer) {.cdecl.} = + ## Mark a screen's queued buffer as scanned out after its vblank event. + let screen = cast[DrmScreen](data) + screen.front = screen.draw + screen.pending = false + +proc waitForFlip(display: DrmDisplay, screen: DrmScreen) = + ## Wait before reusing scanout storage, dispatching events for every output. + if not screen.pending: return + let deadline = getMonoTime() + initDuration(milliseconds = FlipTimeoutMs) + var context = EventContext(version: 2, pageFlipHandler: flipComplete) + while screen.pending: + let remaining = (deadline - getMonoTime()).inMilliseconds + if remaining <= 0: + raise newException(IOError, "Timed out waiting for DRM page flip on " & screen.name) + var descriptor = TPollfd(fd: display.fd, events: POLLIN) + let count = poll(addr descriptor, 1, cint(remaining)) + if count < 0: + if osLastError() == OSErrorCode(EINTR): continue + raiseOSError(osLastError(), "Waiting for DRM page flip") + if count == 0: continue + if (descriptor.revents and (POLLERR or POLLHUP or POLLNVAL)) != 0: + raise newException(IOError, "DRM event descriptor became unavailable") + if display.api.handleEvent(display.fd, addr context) < 0: + if osLastError() == OSErrorCode(EINTR): continue + raiseOSError(osLastError(), "Reading DRM page flip events") + proc closeScreen*(display: DrmDisplay, screen: DrmScreen) = var errors: seq[string] + try: + display.waitForFlip(screen) + except CatchableError as error: + errors.add error.msg + # A late event still contains this screen's address until the DRM fd closes. + display.retiredScreens.add screen if screen.activated: var original = screen.original @@ -238,18 +292,19 @@ proc closeScreen*(display: DrmDisplay, screen: DrmScreen) = original.y, connectorPointer, count, modePointer) < 0: errors.add "Restoring " & screen.name screen.activated = false - if screen.memory != nil: - if munmap(screen.memory, int(screen.buffer.size)) < 0: - errors.add "Unmapping " & screen.name - screen.memory = nil - if screen.framebuffer != 0: - if display.api.removeFb(display.fd, screen.framebuffer) < 0: - errors.add "Removing framebuffer for " & screen.name - screen.framebuffer = 0 - if screen.buffer.handle != 0: - if deviceIoctl(display.fd, DestroyDumb, addr screen.buffer.handle) < 0: - errors.add "Destroying buffer for " & screen.name - screen.buffer.handle = 0 + for scanout in screen.buffers.mitems: + if scanout.memory != nil: + if munmap(scanout.memory, int(scanout.buffer.size)) < 0: + errors.add "Unmapping " & screen.name + scanout.memory = nil + if scanout.framebuffer != 0: + if display.api.removeFb(display.fd, scanout.framebuffer) < 0: + errors.add "Removing framebuffer for " & screen.name + scanout.framebuffer = 0 + if scanout.buffer.handle != 0: + if deviceIoctl(display.fd, DestroyDumb, addr scanout.buffer.handle) < 0: + errors.add "Destroying buffer for " & screen.name + scanout.buffer.handle = 0 if errors.len > 0: raise newException(IOError, errors.join("; ")) @@ -267,6 +322,7 @@ proc close*(display: DrmDisplay) = if posix.close(display.fd) < 0: errors.add "Closing DRM device" display.fd = -1 + display.retiredScreens.setLen(0) if display.api.library != nil: unloadLib(display.api.library) display.api.library = nil @@ -291,7 +347,7 @@ proc chooseCrtc*(ids: openArray[uint32], current, possible: uint32, return id raise newException(IOError, "No independent display controller is available") -proc openOutput*(result: DrmDisplay, outputs: seq[string], rotation: int) = +proc openOutput*(result: DrmDisplay, outputs: seq[string], rotation: int, vsync = false) = ## Prepare independent CPU scanout buffers without changing the active display. if rotation notin [0, 90, 180, 270]: raise newException(ValueError, "Rotation must be 0, 90, 180 or 270") @@ -362,7 +418,7 @@ proc openOutput*(result: DrmDisplay, outputs: seq[string], rotation: int) = if original == nil: raiseOSError(osLastError(), "Saving DRM scanout") let screen = DrmScreen(name: name, connector: connector.id, crtc: id, - original: original[], mode: connector.modes[0], rotation: rotation) + original: original[], mode: connector.modes[0], rotation: rotation, vsync: vsync) api.freeCrtc(original) result.screens.add screen used.add id @@ -382,35 +438,37 @@ proc openOutput*(result: DrmDisplay, outputs: seq[string], rotation: int) = api.freeEncoder(encoder) screen.width = int(if rotation in [90, 270]: screen.mode.vdisplay else: screen.mode.hdisplay) screen.height = int(if rotation in [90, 270]: screen.mode.hdisplay else: screen.mode.vdisplay) - screen.buffer = DumbBuffer(width: uint32(screen.mode.hdisplay), - height: uint32(screen.mode.vdisplay), bpp: 32) - if deviceIoctl(result.fd, CreateDumb, addr screen.buffer) < 0: - raiseOSError(osLastError(), "Allocating DRM scanout buffer") - if screen.buffer.pitch < screen.buffer.width * 4 or - screen.buffer.size < uint64(screen.buffer.pitch) * screen.buffer.height or - screen.buffer.size > MaximumBufferBytes: - raise newException(IOError, "Unsupported DRM buffer geometry") let format = result.rgbaFormat(crtcs.find(id)) screen.rgba = format != 0 - if screen.rgba: - var - handles = [screen.buffer.handle, 0'u32, 0, 0] - pitches = [screen.buffer.pitch, 0'u32, 0, 0] - offsets: array[4, uint32] - if api.addFb2(result.fd, screen.buffer.width, screen.buffer.height, format, - addr handles[0], addr pitches[0], addr offsets[0], addr screen.framebuffer, 0) < 0: - raiseOSError(osLastError(), "Registering RGBA DRM framebuffer") - elif api.addFb(result.fd, screen.buffer.width, screen.buffer.height, 24, 32, - screen.buffer.pitch, screen.buffer.handle, addr screen.framebuffer) < 0: - raiseOSError(osLastError(), "Registering DRM framebuffer") - var mapping = MapBuffer(handle: screen.buffer.handle) - if deviceIoctl(result.fd, MapDumb, addr mapping) < 0: - raiseOSError(osLastError(), "Mapping DRM buffer") - screen.memory = mmap(nil, int(screen.buffer.size), PROT_READ or PROT_WRITE, - MAP_SHARED, result.fd, Off(mapping.offset)) - if screen.memory == MAP_FAILED: - screen.memory = nil - raiseOSError(osLastError(), "Mapping DRM scanout memory") + screen.buffers.setLen(if vsync: 2 else: 1) + for scanout in screen.buffers.mitems: + scanout.buffer = DumbBuffer(width: uint32(screen.mode.hdisplay), + height: uint32(screen.mode.vdisplay), bpp: 32) + if deviceIoctl(result.fd, CreateDumb, addr scanout.buffer) < 0: + raiseOSError(osLastError(), "Allocating DRM scanout buffer") + if scanout.buffer.pitch < scanout.buffer.width * 4 or + scanout.buffer.size < uint64(scanout.buffer.pitch) * scanout.buffer.height or + scanout.buffer.size > MaximumBufferBytes: + raise newException(IOError, "Unsupported DRM buffer geometry") + if screen.rgba: + var + handles = [scanout.buffer.handle, 0'u32, 0, 0] + pitches = [scanout.buffer.pitch, 0'u32, 0, 0] + offsets: array[4, uint32] + if api.addFb2(result.fd, scanout.buffer.width, scanout.buffer.height, format, + addr handles[0], addr pitches[0], addr offsets[0], addr scanout.framebuffer, 0) < 0: + raiseOSError(osLastError(), "Registering RGBA DRM framebuffer") + elif api.addFb(result.fd, scanout.buffer.width, scanout.buffer.height, 24, 32, + scanout.buffer.pitch, scanout.buffer.handle, addr scanout.framebuffer) < 0: + raiseOSError(osLastError(), "Registering DRM framebuffer") + var mapping = MapBuffer(handle: scanout.buffer.handle) + if deviceIoctl(result.fd, MapDumb, addr mapping) < 0: + raiseOSError(osLastError(), "Mapping DRM buffer") + scanout.memory = mmap(nil, int(scanout.buffer.size), PROT_READ or PROT_WRITE, + MAP_SHARED, result.fd, Off(mapping.offset)) + if scanout.memory == MAP_FAILED: + scanout.memory = nil + raiseOSError(osLastError(), "Mapping DRM scanout memory") except: while result.screens.len > originalCount: let screen = result.screens.pop() @@ -418,14 +476,17 @@ proc openOutput*(result: DrmDisplay, outputs: seq[string], rotation: int) = raise proc upload*(display: DrmDisplay, index: int, image: Image) = - ## Copy pixels into scanout memory without changing the active controller. + ## Upload a frame, waiting before reusing storage still owned by scanout. let screen = display.screens[index] if image.width != screen.width or image.height != screen.height: raise newException(ValueError, "Canvas does not match DRM screen") - let destination = cast[ptr UncheckedArray[uint32]](screen.memory) + display.waitForFlip(screen) + screen.draw = if screen.vsync and screen.activated: 1 - screen.front else: 0 + let scanout = screen.buffers[screen.draw] + let destination = cast[ptr UncheckedArray[uint32]](scanout.memory) if screen.rgba and screen.rotation == 0: for row in 0 ..< image.height: - copyMem(addr destination[row * int(screen.buffer.pitch) div 4], + copyMem(addr destination[row * int(scanout.buffer.pitch) div 4], unsafeAddr image.data[row * image.width], image.width * 4) return screen.pixels.setLen(image.data.len) @@ -436,12 +497,12 @@ proc upload*(display: DrmDisplay, index: int, image: Image) = for y in 0 ..< image.height: for x in 0 ..< image.width: let point = rotatedPoint(x, y, image.width, image.height, screen.rotation) - screen.pixels[point.y * int(screen.buffer.width) + point.x] = + screen.pixels[point.y * int(scanout.buffer.width) + point.x] = (if screen.rgba: cast[uint32](image.data[y * image.width + x]) else: rgb(image.data[y * image.width + x])) - for row in 0 ..< int(screen.buffer.height): - copyMem(addr destination[row * int(screen.buffer.pitch) div 4], - addr screen.pixels[row * int(screen.buffer.width)], int(screen.buffer.width) * 4) + for row in 0 ..< int(scanout.buffer.height): + copyMem(addr destination[row * int(scanout.buffer.pitch) div 4], + addr screen.pixels[row * int(scanout.buffer.width)], int(scanout.buffer.width) * 4) proc present*(display: DrmDisplay, index: int, image: Image) = display.upload(index, image) @@ -451,10 +512,16 @@ proc present*(display: DrmDisplay, index: int, image: Image) = if display.api.setMaster(display.fd) < 0: raiseOSError(osLastError(), "Acquiring DRM master; another display application may be running") display.master = true - if display.api.setCrtc(display.fd, screen.crtc, screen.framebuffer, 0, 0, + if display.api.setCrtc(display.fd, screen.crtc, screen.buffers[screen.draw].framebuffer, 0, 0, addr screen.connector, 1, addr screen.mode) < 0: raiseOSError(osLastError(), "Displaying " & screen.name) screen.activated = true + screen.front = screen.draw + elif screen.vsync: + if display.api.pageFlip(display.fd, screen.crtc, + screen.buffers[screen.draw].framebuffer, FlipEvent, cast[pointer](screen)) < 0: + raiseOSError(osLastError(), "Queuing DRM page flip on " & screen.name) + screen.pending = true proc describe*(screen: DrmScreen): string = ## Describe the selected connector, logical geometry and controller. diff --git a/src/windy/platforms/linux/platform.nim b/src/windy/platforms/linux/platform.nim index 7a2fbaa1..1a86728b 100644 --- a/src/windy/platforms/linux/platform.nim +++ b/src/windy/platforms/linux/platform.nim @@ -46,9 +46,9 @@ else: return x11.newWindow(title, size, style, visible, vsync, openglVersion, msaa, depthBits, stencilBits, linuxOptions) else: raise newException(WindyError, "X11 backend was not compiled") - if vsync: - raise newException(WindyError, "Direct pixel presentation requires vsync=false") - direct.newDirectWindow(title, selected, linuxOptions) + if vsync and selected == FbdevBackend: + raise newException(WindyError, "Framebuffer pixel presentation requires vsync=false") + direct.newDirectWindow(title, selected, linuxOptions, vsync) proc pollEvents*() = if selected == X11Backend: diff --git a/tests/manual_direct_displays.nim b/tests/manual_direct_displays.nim index 497b1213..33335f20 100644 --- a/tests/manual_direct_displays.nim +++ b/tests/manual_direct_displays.nim @@ -8,23 +8,26 @@ proc main() = doAssert paramCount() == 3, "Choose two DRM connectors." setLinuxBackend(if useDrm: DrmBackend else: FbdevBackend) for attempt in 0 .. 1: - let first = newWindow("First output", ivec2(0, 0), vsync = false, + let first = newWindow("First output", ivec2(0, 0), vsync = useDrm, linuxOptions = LinuxWindowOptions(output: paramStr(2))) var image = newImage(first.framebufferSize.x, first.framebufferSize.y) image.fill(color(1, 0, 0, 1)) + let firstImage = image first.presentPixels(image) if useDrm: - let second = newWindow("Second output", ivec2(0, 0), vsync = false, + let second = newWindow("Second output", ivec2(0, 0), vsync = true, linuxOptions = LinuxWindowOptions(output: paramStr(3))) image = newImage(second.framebufferSize.x, second.framebufferSize.y) image.fill(color(0, 1, 0, 1)) second.presentPixels(image) + second.presentPixels(image) var rejected = false try: discard newWindow("Duplicate", ivec2(0, 0), vsync = false, linuxOptions = LinuxWindowOptions(output: paramStr(3))) except IOError: rejected = true doAssert rejected + first.presentPixels(firstImage) first.close() image.fill(color(0, 0, 1, 1)) second.presentPixels(image) diff --git a/tests/test_linux_drm.nim b/tests/test_linux_drm.nim new file mode 100644 index 00000000..1adc0505 --- /dev/null +++ b/tests/test_linux_drm.nim @@ -0,0 +1,135 @@ +include windy/platforms/linux/native/drm + +var + signalFd: cint + queued: seq[pointer] + modesets, flips: int + failFlip, failEvents: bool + +proc setCrtc(fd: cint, id, buffer, x, y: uint32, connectors: ptr uint32, + count: cint, mode: ptr ModeInfo): cint {.cdecl.} = + ## Capture activation and restore calls without changing a physical display. + inc modesets + +proc pageFlip(fd: cint, crtc, framebuffer, flags: uint32, data: pointer): cint {.cdecl.} = + ## Queue completion notifications while preserving the active buffer. + doAssert flags == FlipEvent + if failFlip: return -1 + inc flips + queued.add data + var signal = 'F' + doAssert posix.write(signalFd, addr signal, 1) == 1 + +proc handleEvent(fd: cint, context: ptr EventContext): cint {.cdecl.} = + ## Deliver a completion through the real callback signature after polling a pipe. + if failEvents: return -1 + doAssert context.version == 2 + var signal: char + doAssert posix.read(fd, addr signal, 1) == 1 + let data = queued[0] + queued.delete(0) + context.pageFlipHandler(fd, 0, 0, 0, data) + +proc removeFb(fd: cint, id: uint32): cint {.cdecl.} = + ## Accept cleanup of a test-owned framebuffer registration. + discard + +proc newScreen(vsync: bool): DrmScreen = + ## Allocate padded anonymous buffers with the same lifetime as DRM mappings. + result = DrmScreen(name: "Test", width: 2, height: 2, rgba: true, vsync: vsync) + result.buffers.setLen(if vsync: 2 else: 1) + for index, scanout in result.buffers.mpairs: + scanout.buffer = DumbBuffer(width: 2, height: 2, pitch: 12, size: 24) + scanout.framebuffer = uint32(index + 1) + scanout.memory = mmap(nil, 24, PROT_READ or PROT_WRITE, MAP_PRIVATE or MAP_ANONYMOUS, -1, 0) + doAssert scanout.memory != MAP_FAILED + +proc firstPixel(screen: DrmScreen, index: int): ColorRGBX = + ## Observe a buffer without retaining a mutable alias. + cast[ptr ColorRGBX](screen.buffers[index].memory)[] + +proc main() = + ## Verify independent page flips, buffer ownership, failures and pending cleanup. + var pipeFds: array[2, cint] + doAssert pipe(pipeFds) == 0 + signalFd = pipeFds[1] + defer: + doAssert posix.close(pipeFds[0]) == 0 + doAssert posix.close(pipeFds[1]) == 0 + let + top = newScreen(true) + bottom = newScreen(true) + unsynced = newScreen(false) + display = DrmDisplay(fd: pipeFds[0], master: true, screens: @[top, bottom, unsynced], + api: DrmApi(setCrtc: setCrtc, pageFlip: pageFlip, handleEvent: handleEvent, removeFb: removeFb)) + image = newImage(2, 2) + image.fill(rgbx(255, 0, 0, 255)) + display.present(0, image) + display.present(1, image) + doAssert modesets == 2 and flips == 0 + image.fill(rgbx(0, 255, 0, 255)) + display.present(0, image) + display.present(1, image) + doAssert flips == 2 and top.pending and bottom.pending + doAssert top.firstPixel(0) == rgbx(255, 0, 0, 255) + doAssert top.firstPixel(1) == rgbx(0, 255, 0, 255) + doAssert top.front == 0 + image.fill(rgbx(0, 0, 255, 255)) + display.present(1, image) + doAssert not top.pending and top.front == 1 + doAssert bottom.pending and bottom.front == 1 + doAssert bottom.firstPixel(1) == rgbx(0, 255, 0, 255) + doAssert bottom.firstPixel(0) == rgbx(0, 0, 255, 255) + display.present(0, image) + doAssert top.firstPixel(1) == rgbx(0, 255, 0, 255) + doAssert top.firstPixel(0) == rgbx(0, 0, 255, 255) + display.closeScreen(bottom) + display.closeScreen(top) + doAssert not top.pending and not bottom.pending and queued.len == 0 + doAssert modesets == 4 + + display.present(2, image) + image.fill(rgbx(12, 34, 56, 255)) + display.present(2, image) + doAssert unsynced.firstPixel(0) == rgbx(12, 34, 56, 255) + doAssert not unsynced.pending and flips == 4 + display.closeScreen(unsynced) + + let failed = newScreen(true) + display.screens.add failed + display.present(3, image) + failFlip = true + var rejected = false + try: display.present(3, image) + except OSError: rejected = true + doAssert rejected and not failed.pending + display.closeScreen(failed) + + failFlip = false + let late = newScreen(true) + display.screens.add late + display.present(4, image) + display.present(4, image) + failEvents = true + rejected = false + try: display.closeScreen(late) + except IOError: rejected = true + doAssert rejected and display.retiredScreens == @[late] + display.screens.delete(4) + failEvents = false + let next = newScreen(true) + display.screens.add next + display.present(4, image) + display.present(4, image) + display.waitForFlip(next) + doAssert not late.pending and not next.pending and queued.len == 0 + next.pending = true + rejected = false + try: display.waitForFlip(next) + except IOError as error: + rejected = error.msg.startsWith("Timed out") + doAssert rejected + next.pending = false + display.closeScreen(next) + +main()