diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..8f3470e5 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +.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: + 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..05512e10 --- /dev/null +++ b/src/windy/platforms/linux/native/framebuffer.nim @@ -0,0 +1,163 @@ +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 + 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 + 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 + 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..46c09ab0 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 @@ -865,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, @@ -888,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: @@ -1224,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 @@ -1455,3 +1462,76 @@ proc saveFileDialog*( args.add("--file-filter=" & filter.name & " | " & filter.extensions.replace(";", " ").replace(",", " ")) result = runZenity(args) + +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 + 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") + 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] + 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() + +proc hasWindows*(): bool = + for window in windows: + if not window.closed: return true 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/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..7aa2c00c --- /dev/null +++ b/tests/test_linux_display.nim @@ -0,0 +1,71 @@ +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(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) + 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, 63, 47, culong.high, 2) + doAssert captured != nil + 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) + 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) + 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_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) 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