ArkGraphics.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. //
  2. // Copyright 2026 Aarav Ravindra Kharade
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import ark.sys.libc
  17. // -
  18. // ArkGraphics — Display Client Library
  19. // -
  20. // Client-side library that connects to the ArkOS Display Daemon
  21. // via /dev/display.sock and sends rendering commands.
  22. //
  23. // This is the rendering API used by all ArkOS UI applications.
  24. // Commands are sent as packed binary structs over a Unix domain
  25. // socket for minimal overhead.
  26. //
  27. // Usage:
  28. // let gfx = ArkGraphics()
  29. // gfx.clear(r: 30, g: 30, b: 35)
  30. // gfx.fillRect(x: 100, y: 100, w: 200, h: 50, r: 0, g: 120, b: 215)
  31. // gfx.drawLine(x1: 10, y1: 10, x2: 200, y2: 200, r: 255, g: 255, b: 255)
  32. // gfx.pan() // Present to screen
  33. //
  34. // -
  35. // - Wire Protocol -
  36. /// Packed command structure sent to the display daemon.
  37. /// Total size: 25 bytes (1 + 5*4 + 4*1).
  38. private struct DisplayCommand {
  39. var cmd: UInt8 // Command ID (1=fillRect, 2=pan, 3=clear, 4=fillRoundedRect)
  40. var x: Int32 // X coordinate (or 0 for non-spatial commands)
  41. var y: Int32 // Y coordinate
  42. var w: Int32 // Width
  43. var h: Int32 // Height
  44. var r: UInt8 // Red (0–255)
  45. var g: UInt8 // Green (0–255)
  46. var b: UInt8 // Blue (0–255)
  47. var a: UInt8 // Alpha (0–255)
  48. var radius: Int32 // Border radius
  49. }
  50. // - Graphics Client -
  51. /// Client-side handle to the display daemon. Manages the socket
  52. /// connection and provides high-level drawing primitives.
  53. // C bridge to bypass varargs issues in Swift's ioctl binding
  54. @_silgen_name("ioctl")
  55. func ioctl_fb(_ fd: Int32, _ request: UInt, _ info: UnsafeMutableRawPointer) -> Int32
  56. public class ArkGraphics {
  57. /// File descriptor for the Unix domain socket connection.
  58. private var fd: Int32 = -1
  59. /// Path to the display daemon socket.
  60. private let socketPath = "/dev/display.sock"
  61. /// Maximum connection retry attempts on initialization.
  62. private let maxRetries = 30
  63. private let retryDelay: useconds_t = 500_000 // 500ms
  64. public var screenWidth: Int = 1024
  65. public var screenHeight: Int = 768
  66. /// Creates a new graphics client and connects to the display daemon.
  67. /// Retries up to 10 times with 500ms delay (total 5s) to allow the
  68. /// daemon time to start during boot.
  69. public init() {
  70. for attempt in 1...maxRetries {
  71. if tryConnect() {
  72. break
  73. }
  74. if attempt < maxRetries {
  75. usleep(retryDelay)
  76. }
  77. }
  78. if !isConnected {
  79. print("ArkGraphics: WARNING — Failed to connect to display daemon after \(maxRetries) attempts")
  80. }
  81. // Query real resolution from fb0 (guaranteed to exist if daemon is running)
  82. let fb_fd = ark_open2("/dev/fb0", O_RDONLY)
  83. if fb_fd >= 0 {
  84. // Memory layout of fb_var_screeninfo (ABI compatible with Linux)
  85. struct fb_var_screeninfo {
  86. var xres: UInt32 = 0
  87. var yres: UInt32 = 0
  88. var xres_virtual: UInt32 = 0
  89. var yres_virtual: UInt32 = 0
  90. var xoffset: UInt32 = 0
  91. var yoffset: UInt32 = 0
  92. var bits_per_pixel: UInt32 = 0
  93. var grayscale: UInt32 = 0
  94. var red: (UInt32, UInt32, UInt32) = (0,0,0)
  95. var green: (UInt32, UInt32, UInt32) = (0,0,0)
  96. var blue: (UInt32, UInt32, UInt32) = (0,0,0)
  97. var transp: (UInt32, UInt32, UInt32) = (0,0,0)
  98. var nonstd: UInt32 = 0
  99. var activate: UInt32 = 0
  100. var height: UInt32 = 0
  101. var width: UInt32 = 0
  102. var accel_flags: UInt32 = 0
  103. var pixclock: UInt32 = 0
  104. var left_margin: UInt32 = 0
  105. var right_margin: UInt32 = 0
  106. var upper_margin: UInt32 = 0
  107. var lower_margin: UInt32 = 0
  108. var hsync_len: UInt32 = 0
  109. var vsync_len: UInt32 = 0
  110. var sync: UInt32 = 0
  111. var vmode: UInt32 = 0
  112. var reserved: (UInt32, UInt32, UInt32, UInt32, UInt32, UInt32) = (0, 0, 0, 0, 0, 0)
  113. }
  114. var vinfo = fb_var_screeninfo()
  115. let FBIOGET_VSCREENINFO: UInt = 0x4600
  116. if ioctl_fb(fb_fd, FBIOGET_VSCREENINFO, &vinfo) >= 0 {
  117. self.screenWidth = Int(vinfo.xres) > 0 ? Int(vinfo.xres) : 1920
  118. self.screenHeight = Int(vinfo.yres) > 0 ? Int(vinfo.yres) : 1080
  119. }
  120. close(fb_fd)
  121. }
  122. if let str = ArkSys.File.readString("/tmp/screen_size.txt") {
  123. var w = 0
  124. var h = 0
  125. var parsingW = true
  126. for char in str {
  127. if char == " " {
  128. parsingW = false
  129. } else if let num = char.wholeNumberValue {
  130. if parsingW {
  131. w = w * 10 + num
  132. } else {
  133. h = h * 10 + num
  134. }
  135. }
  136. }
  137. if w > 0 && h > 0 {
  138. self.screenWidth = w
  139. self.screenHeight = h
  140. }
  141. }
  142. // Final fallback if ioctl and file fail
  143. if self.screenWidth <= 0 { self.screenWidth = 1920 }
  144. if self.screenHeight <= 0 { self.screenHeight = 1080 }
  145. }
  146. deinit {
  147. if fd >= 0 { close(fd) }
  148. }
  149. // - Connection Management -
  150. /// Attempts to connect to the display daemon socket.
  151. /// Returns true on success, false on failure.
  152. private func tryConnect() -> Bool {
  153. #if canImport(Glibc)
  154. let sock = socket(AF_UNIX, Int32(SOCK_STREAM.rawValue), 0)
  155. #else
  156. let sock = socket(AF_UNIX, Int32(SOCK_STREAM), 0)
  157. #endif
  158. if sock < 0 { return false }
  159. var addr = sockaddr_un()
  160. addr.sun_family = sa_family_t(AF_UNIX)
  161. let pathBytes = socketPath.utf8CString
  162. withUnsafeMutablePointer(to: &addr.sun_path) { sunPathPtr in
  163. let rawPtr = UnsafeMutableRawPointer(sunPathPtr).assumingMemoryBound(to: CChar.self)
  164. for i in 0..<min(pathBytes.count, 108) {
  165. rawPtr[i] = pathBytes[i]
  166. }
  167. }
  168. let addrSize = MemoryLayout<sockaddr_un>.size
  169. let connectResult = withUnsafePointer(to: &addr) { addrPtr in
  170. addrPtr.withMemoryRebound(to: sockaddr.self, capacity: 1) { saPtr in
  171. connect(sock, saPtr, socklen_t(addrSize))
  172. }
  173. }
  174. if connectResult < 0 {
  175. close(sock)
  176. return false
  177. }
  178. self.fd = sock
  179. return true
  180. }
  181. /// Returns true if connected to the display daemon.
  182. public var isConnected: Bool {
  183. return fd >= 0
  184. }
  185. // - Rendering Commands -
  186. @inline(__always)
  187. private func toTopLeftY(y: Int, h: Int) -> Int32 {
  188. return Int32(screenHeight - y - h)
  189. }
  190. /// Fills a rectangle with a solid RGB color.
  191. ///
  192. /// - Parameters:
  193. /// - x: Left edge in pixels
  194. /// - y: Top edge in pixels
  195. /// - w: Width in pixels
  196. /// - h: Height in pixels
  197. /// - r: Red component (0–255)
  198. /// - g: Green component (0–255)
  199. /// - b: Blue component (0–255)
  200. public func fillRectRGB(x: Int, y: Int, w: Int, h: Int,
  201. r: UInt8, g: UInt8, b: UInt8) {
  202. sendCommand(DisplayCommand(
  203. cmd: 1, x: Int32(x), y: toTopLeftY(y: y, h: h), w: Int32(w), h: Int32(h),
  204. r: r, g: g, b: b, a: 255, radius: 0
  205. ))
  206. }
  207. /// Fills a rectangle with an RGBA color, blending it with the background.
  208. public func fillRectRGBA(x: Int, y: Int, w: Int, h: Int,
  209. r: UInt8, g: UInt8, b: UInt8, a: UInt8) {
  210. sendCommand(DisplayCommand(
  211. cmd: 1, x: Int32(x), y: toTopLeftY(y: y, h: h), w: Int32(w), h: Int32(h),
  212. r: r, g: g, b: b, a: a, radius: 0
  213. ))
  214. }
  215. /// Fills a rounded rectangle with a solid RGB color.
  216. public func fillRoundedRectRGB(x: Int, y: Int, w: Int, h: Int,
  217. r: UInt8, g: UInt8, b: UInt8, radius: Int) {
  218. sendCommand(DisplayCommand(
  219. cmd: 4, x: Int32(x), y: toTopLeftY(y: y, h: h), w: Int32(w), h: Int32(h),
  220. r: r, g: g, b: b, a: 255, radius: Int32(radius)
  221. ))
  222. }
  223. /// Fills a rounded rectangle with an RGBA color.
  224. public func fillRoundedRectRGBA(x: Int, y: Int, w: Int, h: Int,
  225. r: UInt8, g: UInt8, b: UInt8, a: UInt8, radius: Int) {
  226. sendCommand(DisplayCommand(
  227. cmd: 4, x: Int32(x), y: toTopLeftY(y: y, h: h), w: Int32(w), h: Int32(h),
  228. r: r, g: g, b: b, a: a, radius: Int32(radius)
  229. ))
  230. }
  231. /// Clears the entire screen to a solid RGB color.
  232. public func clear(r: UInt8, g: UInt8, b: UInt8) {
  233. sendCommand(DisplayCommand(
  234. cmd: 3, x: 0, y: 0, w: 0, h: 0,
  235. r: r, g: g, b: b, a: 255, radius: 0
  236. ))
  237. }
  238. /// Presents the current back buffer to the screen (page flip).
  239. /// Must be called after all drawing commands to make them visible.
  240. public func pan() {
  241. sendCommand(DisplayCommand(
  242. cmd: 2, x: 0, y: 0, w: 0, h: 0,
  243. r: 0, g: 0, b: 0, a: 0, radius: 0
  244. ))
  245. }
  246. /// Draws a horizontal line of 1 pixel height.
  247. public func drawHLine(x: Int, y: Int, w: Int,
  248. r: UInt8, g: UInt8, b: UInt8) {
  249. fillRectRGB(x: x, y: y, w: w, h: 1, r: r, g: g, b: b)
  250. }
  251. /// Draws a vertical line of 1 pixel width.
  252. public func drawVLine(x: Int, y: Int, h: Int,
  253. r: UInt8, g: UInt8, b: UInt8) {
  254. fillRectRGB(x: x, y: y, w: 1, h: h, r: r, g: g, b: b)
  255. }
  256. /// Draws a rectangle outline (border only, no fill).
  257. ///
  258. /// - Parameters:
  259. /// - thickness: Border width in pixels (default 1)
  260. public func drawRectOutline(x: Int, y: Int, w: Int, h: Int,
  261. r: UInt8, g: UInt8, b: UInt8,
  262. thickness: Int = 1) {
  263. // Top edge
  264. fillRectRGB(x: x, y: y, w: w, h: thickness, r: r, g: g, b: b)
  265. // Bottom edge
  266. fillRectRGB(x: x, y: y + h - thickness, w: w, h: thickness, r: r, g: g, b: b)
  267. // Left edge
  268. fillRectRGB(x: x, y: y, w: thickness, h: h, r: r, g: g, b: b)
  269. // Right edge
  270. fillRectRGB(x: x + w - thickness, y: y, w: thickness, h: h, r: r, g: g, b: b)
  271. }
  272. // - Internal Transport -
  273. private func sendCommand(_ cmd: DisplayCommand) {
  274. if fd < 0 { return }
  275. let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: 25)
  276. defer { buffer.deallocate() }
  277. buffer[0] = cmd.cmd
  278. var x = cmd.x
  279. var y = cmd.y
  280. var w = cmd.w
  281. var h = cmd.h
  282. var radius = cmd.radius
  283. _ = withUnsafePointer(to: &x) { memcpy(buffer + 1, $0, 4) }
  284. _ = withUnsafePointer(to: &y) { memcpy(buffer + 5, $0, 4) }
  285. _ = withUnsafePointer(to: &w) { memcpy(buffer + 9, $0, 4) }
  286. _ = withUnsafePointer(to: &h) { memcpy(buffer + 13, $0, 4) }
  287. buffer[17] = cmd.r
  288. buffer[18] = cmd.g
  289. buffer[19] = cmd.b
  290. buffer[20] = cmd.a
  291. _ = withUnsafePointer(to: &radius) { memcpy(buffer + 21, $0, 4) }
  292. var totalWritten = 0
  293. while totalWritten < 25 {
  294. let written = write(fd, buffer + totalWritten, 25 - totalWritten)
  295. if written <= 0 { break }
  296. totalWritten += Int(written)
  297. }
  298. }
  299. }