diff --git a/README.md b/README.md index 27addf1..56478a9 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,17 @@ macOS builds require Xcode Command Line Tools. The Windows Win32/WGL backend cross-compiles for `x86_64-windows-gnu`; runtime verification on Windows is pending. It requires Windows 10 version 1703 or later and an OpenGL 3.3-capable driver. +The Linux Wayland preview implements windows and EGL/OpenGL, initially tested +on KDE Plasma. Input and desktop services are still pending. It vendors only ABI +headers and protocol bindings and loads system libraries at runtime. Normal +builds need Zig alone; the optional `shell.nix` supplies maintainer tools and +NixOS runtime library paths. See [Linux support and limitations](src/linux/README.md). + ## Build ```sh zig build # zig-out/lib/libuhdk.a and zig-out/include/ -zig build -Dshared=true # zig-out/lib/libuhdk.dylib and headers +zig build -Dshared=true # platform shared library and headers zig build test # run core library tests on the native target zig build -Dtarget=x86_64-windows-gnu zig build -Dtarget=x86_64-windows-gnu -Dshared=true diff --git a/build.zig b/build.zig index 3e4e36e..55fec6b 100644 --- a/build.zig +++ b/build.zig @@ -2,7 +2,7 @@ const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); - if (target.result.os.tag != .macos and target.result.os.tag != .windows) @panic("Uhdk supports macOS and Windows."); + if (target.result.os.tag != .macos and target.result.os.tag != .windows and target.result.os.tag != .linux) @panic("Uhdk supports macOS, Windows and Linux."); const optimize = b.standardOptimizeOption(.{}); const shared = b.option(bool, "shared", "Build libuhdk as a shared library instead of static") orelse false; const tracy = b.option(bool, "tracy", "Emit Tracy 0.14.1 on-demand hooks; consumer must link the client") orelse false; @@ -24,6 +24,7 @@ pub fn build(b: *std.Build) void { const check_step = b.step("check", "Compile library and core tests without running them"); check_step.dependOn(&lib.step); check_step.dependOn(&unit.step); + if (target.result.os.tag == .linux) @import("src/linux/build.zig").addTests(b, lib, target, optimize, check_step); const unit_run = b.addRunArtifact(unit); b.step("test", "Run core library tests").dependOn(&unit_run.step); const public_c = b.addTranslateC(.{ @@ -79,6 +80,8 @@ fn createLibraryNamed(b: *std.Build, target: std.Build.ResolvedTarget, optimize: module.linkFramework("Cocoa", .{}); module.linkFramework("OpenGL", .{}); module.linkFramework("CoreServices", .{}); + } else if (target.result.os.tag == .linux) { + @import("src/linux/build.zig").configure(b, module, target, optimize); } else { module.addCSourceFile(.{ .file = b.path("src/windows/native.c"), .flags = &.{ "-std=c11", "-Wall", "-Wextra", "-Werror", "-DUNICODE", "-D_UNICODE" } }); for ([_][]const u8{ "user32", "gdi32", "opengl32", "shell32", "ole32", "uuid", "imm32", "comctl32", "shlwapi", "advapi32" }) |name| module.linkSystemLibrary(name, .{}); diff --git a/build.zig.zon b/build.zig.zon index 9c92eb8..e2bbd8e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,5 +4,5 @@ .version = "0.1.0", .minimum_zig_version = "0.16.0", .dependencies = .{}, - .paths = .{ "build.zig", "build.zig.zon", "src", "include", "README.md" }, + .paths = .{ "build.zig", "build.zig.zon", "src", "include", "README.md", "shell.nix" }, } diff --git a/include/uhdk.h b/include/uhdk.h index 0abd94c..4cebc35 100644 --- a/include/uhdk.h +++ b/include/uhdk.h @@ -2,7 +2,7 @@ #define UHDK_H_INCLUDED /* - * Uhdk desktop platform API — v0.1, macOS and Windows backends. + * Uhdk desktop platform API — v0.1, macOS, Windows and Linux backends. * * Implementation: Zig. ABI: C. Header baseline: C99 (also usable from C++). * No libc runtime objects or allocation contract. @@ -252,6 +252,9 @@ UHDK_API uint64_t UHDK_CALL uhdk_app_get_time_ns(uhdk_app *app); */ UHDK_API uhdk_status UHDK_CALL uhdk_app_set_wake_deadline(uhdk_app *app, uint64_t deadline_ns); UHDK_API void UHDK_CALL uhdk_app_destroy(uhdk_app *app); /* After run; releases child objects. */ +/* Linux: AUTO/false before successful startup; thereafter the selected backend + * and implemented, available mechanisms. Capabilities do not guarantee success + * for a particular operation. Other platforms retain their fixed backend. */ UHDK_API uhdk_backend UHDK_CALL uhdk_app_get_backend(uhdk_app *app); UHDK_API uhdk_bool UHDK_CALL uhdk_app_has_capability(uhdk_app *app, uhdk_capability capability); UHDK_API uhdk_status UHDK_CALL uhdk_request_cancel(uhdk_app *app, uhdk_request_id request_id); @@ -321,6 +324,9 @@ typedef struct uhdk_window_desc { uhdk_window *parent; /* Optional; must outlive child. */ } uhdk_window_desc; +/* On Wayland, visible is application show/hide intent, not compositor visibility. + * State is the last observable state; Wayland does not report minimization. + * Async size/state completion acknowledges submission; geometry arrives here. */ typedef struct uhdk_window_metrics { uint32_t struct_size; uint32_t framebuffer_width; @@ -415,6 +421,7 @@ UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_get_version( uint32_t *out_major, uint32_t *out_minor ); +/* Wayland returns UNAVAILABLE while hidden or awaiting mapping configuration. */ UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_swap_buffers(uhdk_gl_context *context); /* Explicitly set 0 (unsynchronized) or 1 (vsync); other values are invalid. * No initial interval is guaranteed. Unsupported control returns UNSUPPORTED; diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..8584d19 --- /dev/null +++ b/shell.nix @@ -0,0 +1,16 @@ +{ + pkgs ? import ./src/linux/nix/pkgs.nix, +}: +pkgs.mkShell { + packages = [ + pkgs.python3 + pkgs.wayland-scanner + pkgs.clang-tools + ]; + # NixOS does not put desktop libraries in the conventional loader search path. + # This affects running an unwrapped development executable, not compilation. + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + pkgs.wayland + pkgs.libglvnd + ]; +} diff --git a/src/Application.zig b/src/Application.zig index 51a66d4..72d2953 100644 --- a/src/Application.zig +++ b/src/Application.zig @@ -27,6 +27,7 @@ pending_tail: ?*Event = null, published: ?*Event = null, scratch: arenas.BumpArena, name: []u8, +application_id: []const u8 = "", epoch: u64, deadline: u64 = 0, next_id: u64 = 1, diff --git a/src/Request.zig b/src/Request.zig index 37942d5..042f316 100644 --- a/src/Request.zig +++ b/src/Request.zig @@ -16,5 +16,6 @@ node: *Event, id: u64 = 0, kind: u32 = c.UHDK_REQUEST_COMMAND, cancelled: bool = false, +native_deferred: bool = false, pins: std.ArrayList(*Resource) = .empty, file_operation: ?FileOperation = null, diff --git a/src/api/app.zig b/src/api/app.zig index a770bd5..88a8348 100644 --- a/src/api/app.zig +++ b/src/api/app.zig @@ -1,7 +1,11 @@ //! C ABI entry points for app. const std = @import("std"); const profile = @import("profile"); -const backend_kind = if (@import("builtin").os.tag == .windows) c.UHDK_BACKEND_WINDOWS else c.UHDK_BACKEND_MACOS; +const backend_kind = switch (@import("builtin").os.tag) { + .windows => c.UHDK_BACKEND_WINDOWS, + .linux => c.UHDK_BACKEND_WAYLAND, + else => c.UHDK_BACKEND_MACOS, +}; const c = @import("native"); const runtime = @import("../runtime.zig"); const abi = @import("../abi.zig"); @@ -31,6 +35,14 @@ pub export fn uhdk_app_create(desc: [*c]const c.uhdk_app_desc, out: [*c]?*c.uhdk gpa.destroy(backend); return abi.OOM; }; + application.application_id = gpa.dupe(u8, abi.textBytes(desc.*.application_id)) catch { + application.scratch.deinit(); + gpa.free(application.name); + gpa.destroy(application); + backend.deinit(); + gpa.destroy(backend); + return abi.OOM; + }; application.io_backend = backend; singleton = true; out.* = runtime.toAppHandle(application); @@ -47,9 +59,9 @@ pub export fn uhdk_app_run( const application = runtime.fromAppHandle(handle); if (application.has_run) return abi.UNAVAILABLE; profile.thread("UHDK platform"); - runtime.simpleEvent(application, c.UHDK_EVENT_READY); const status = c.z_native_start(handle, abi.textSpan(application.name)); if (status != abi.OK) return status; + runtime.simpleEvent(application, c.UHDK_EVENT_READY); application.entry = entry; application.user = user; application.running = true; @@ -235,11 +247,13 @@ pub export fn uhdk_app_set_wake_deadline(handle: ?*c.uhdk_app, deadline: u64) ab return abi.OK; } -pub export fn uhdk_app_get_backend(_: ?*c.uhdk_app) c.uhdk_backend { +pub export fn uhdk_app_get_backend(handle: ?*c.uhdk_app) c.uhdk_backend { + if (@import("builtin").os.tag == .linux) return @import("../linux/native.zig").backend(handle); return backend_kind; } -pub export fn uhdk_app_has_capability(_: ?*c.uhdk_app, cap: c.uhdk_capability) c.uhdk_bool { +pub export fn uhdk_app_has_capability(handle: ?*c.uhdk_app, cap: c.uhdk_capability) c.uhdk_bool { + if (@import("builtin").os.tag == .linux) return @import("../linux/native.zig").capability(handle, cap); return switch (cap) { c.UHDK_CAP_FILE_DIALOGS, c.UHDK_CAP_CLIPBOARD, c.UHDK_CAP_DROP_FILES, c.UHDK_CAP_APP_MENU, c.UHDK_CAP_CONTEXT_MENU, c.UHDK_CAP_APP_ICON, c.UHDK_CAP_POINTER_CAPTURE, c.UHDK_CAP_TEXT_INPUT, c.UHDK_CAP_OPENGL => 1, else => 0, @@ -280,6 +294,7 @@ pub export fn uhdk_app_destroy(handle: ?*c.uhdk_app) void { } const gpa = application.gpa; gpa.free(application.name); + gpa.free(application.application_id); if (application.io_backend) |backend| { backend.deinit(); gpa.destroy(backend); diff --git a/src/linux/Egl.zig b/src/linux/Egl.zig new file mode 100644 index 0000000..0b14f6b --- /dev/null +++ b/src/linux/Egl.zig @@ -0,0 +1,160 @@ +//! EGL owns presentation resources only. The compositor thread must never wait +//! for a frame lock: drivers may wait for Wayland events during presentation. +const std = @import("std"); +const c = @import("linux_c"); +const n = @import("native"); +const abi = @import("../abi.zig"); +const native = @import("native.zig"); + +pub const Context = struct { + window: *native.Window, + native: ?*c.struct_wl_egl_window, + surface: c.EGLSurface, + context: c.EGLContext, +}; +threadlocal var frame: ?*Context = null; + +pub fn initialize(b: *native.Backend) void { + if (c.z_linux_egl_available() == 0) return; + const display = c.z_eglGetPlatformDisplay.?(c.EGL_PLATFORM_WAYLAND_KHR, b.display, null); + if (display == null) return; + var major: c.EGLint = 0; + var minor: c.EGLint = 0; + if (c.z_eglInitialize.?(display, &major, &minor) == c.EGL_FALSE) return; + if (major < 1 or (major == 1 and minor < 5)) { + _ = c.z_eglTerminate.?(display); + return; + } + const attributes = [_]c.EGLint{ c.EGL_SURFACE_TYPE, c.EGL_WINDOW_BIT, c.EGL_RENDERABLE_TYPE, c.EGL_OPENGL_BIT, c.EGL_RED_SIZE, 8, c.EGL_GREEN_SIZE, 8, c.EGL_BLUE_SIZE, 8, c.EGL_ALPHA_SIZE, 8, c.EGL_NONE }; + var config: c.EGLConfig = null; + var count: c.EGLint = 0; + if (c.z_eglChooseConfig.?(display, &attributes, &config, 1, &count) == c.EGL_FALSE or count == 0) { + _ = c.z_eglTerminate.?(display); + return; + } + b.egl_display = display; + b.egl_config = config; +} +pub fn finish(b: *native.Backend) void { + if (b.egl_display != null) { + _ = c.z_eglTerminate.?(b.egl_display); + _ = c.z_eglReleaseThread.?(); + b.egl_display = null; + } +} +pub fn create(b: *native.Backend, w: *native.Window, object: ?*anyopaque, result: [*c]n.z_request_result) u32 { + if (b.egl_display == null or w.flags & n.UHDK_WINDOW_OPENGL == 0) return n.UHDK_STATUS_UNSUPPORTED; + if (w.egl != null) return n.UHDK_STATUS_BUSY; + if (c.z_eglBindAPI.?(c.EGL_OPENGL_API) == c.EGL_FALSE) return n.UHDK_STATUS_OS_ERROR; + const attrs = [_]c.EGLint{ c.EGL_CONTEXT_MAJOR_VERSION, 3, c.EGL_CONTEXT_MINOR_VERSION, 3, c.EGL_CONTEXT_OPENGL_PROFILE_MASK, c.EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, c.EGL_NONE }; + const context = c.z_eglCreateContext.?(b.egl_display, b.egl_config, null, &attrs); + if (context == null) return n.UHDK_STATUS_UNSUPPORTED; + const win = c.z_wl_egl_window_create.?(w.surface, @intCast(w.metrics.framebuffer_width), @intCast(w.metrics.framebuffer_height)); + if (win == null) { + _ = c.z_eglDestroyContext.?(b.egl_display, context); + return abi.OOM; + } + const surface = c.z_eglCreateWindowSurface.?(b.egl_display, b.egl_config, @ptrCast(win), null); + if (surface == null) { + c.z_wl_egl_window_destroy.?(win); + _ = c.z_eglDestroyContext.?(b.egl_display, context); + return n.UHDK_STATUS_OS_ERROR; + } + const ctx = b.app.gpa.create(Context) catch { + _ = c.z_eglDestroySurface.?(b.egl_display, surface); + c.z_wl_egl_window_destroy.?(win); + _ = c.z_eglDestroyContext.?(b.egl_display, context); + return abi.OOM; + }; + ctx.* = .{ .window = w, .native = win, .surface = surface, .context = context }; + w.egl = ctx; + n.z_object_set_native(object, ctx); + result.*.value.gl_context = @ptrCast(object); + return abi.OK; +} +pub fn destroy(ctx: *Context) void { + const b = native.instance.?; + _ = c.z_eglDestroySurface.?(b.egl_display, ctx.surface); + _ = c.z_eglDestroyContext.?(b.egl_display, ctx.context); + c.z_wl_egl_window_destroy.?(ctx.native); + ctx.window.egl = null; + b.app.gpa.destroy(ctx); +} +pub fn resetSurface(ctx: *Context) void { + const b = native.instance.?; + // An unmapped surface may never receive the driver's outstanding frame + // callback. Drop its EGL surface before hiding so remapping cannot wait + // forever in the next swap; the GL context and application resources survive. + if (ctx.surface != null) _ = c.z_eglDestroySurface.?(b.egl_display, ctx.surface); + ctx.surface = c.z_eglCreateWindowSurface.?(b.egl_display, b.egl_config, ctx.native, null); +} +fn makeCurrent(b: *native.Backend, ctx: *Context) bool { + return ctx.surface != null and c.z_eglBindAPI.?(c.EGL_OPENGL_API) != c.EGL_FALSE and + c.z_eglMakeCurrent.?(b.egl_display, ctx.surface, ctx.surface, ctx.context) != c.EGL_FALSE; +} +pub fn call(ptr: ?*anyopaque, op: u32, out: ?*anyopaque) u32 { + const b = native.instance orelse return abi.UNAVAILABLE; + if (b.egl_display == null) return n.UHDK_STATUS_UNSUPPORTED; + if (op == 4 or op == 8) { + if (op == 8 and frame != null) return n.UHDK_STATUS_BUSY; + return if (c.z_eglMakeCurrent.?(b.egl_display, null, null, null) != c.EGL_FALSE) abi.OK else n.UHDK_STATUS_OS_ERROR; + } + const ctx: *Context = @ptrCast(@alignCast(ptr orelse return abi.INVALID)); + if (op == 1) { + if (frame != null) return n.UHDK_STATUS_BUSY; + if (b.failed.load(.acquire)) return abi.UNAVAILABLE; + ctx.window.mutex.lockUncancelable(b.app.io); + if (!makeCurrent(b, ctx)) { + ctx.window.mutex.unlock(b.app.io); + return n.UHDK_STATUS_OS_ERROR; + } + frame = ctx; + return abi.OK; + } + if (frame != ctx) return n.UHDK_STATUS_BUSY; + switch (op) { + 2 => { + // Keep ownership on failure so a caller cannot destroy a current context. + if (c.z_eglMakeCurrent.?(b.egl_display, null, null, null) == c.EGL_FALSE) return n.UHDK_STATUS_OS_ERROR; + frame = null; + ctx.window.mutex.unlock(b.app.io); + n.z_native_wake(); + return abi.OK; + }, + 3 => return if (makeCurrent(b, ctx)) abi.OK else n.UHDK_STATUS_OS_ERROR, + 5 => { + if (c.z_eglGetCurrentContext.?() != ctx.context) return n.UHDK_STATUS_BUSY; + const address = c.z_eglGetProcAddress.?("glGetIntegerv") orelse return n.UHDK_STATUS_NOT_FOUND; + const get_integer: *const fn (c_uint, *c_int) callconv(.c) void = @ptrCast(address); + var major: c_int = 0; + var minor: c_int = 0; + get_integer(0x821B, &major); // GL_MAJOR_VERSION + get_integer(0x821C, &minor); // GL_MINOR_VERSION + const version: *[2]u32 = @ptrCast(@alignCast(out.?)); + version.* = .{ @intCast(major), @intCast(minor) }; + }, + 6 => { + if (b.failed.load(.acquire)) return abi.UNAVAILABLE; + if (!ctx.window.visible or !ctx.window.configured) return abi.UNAVAILABLE; + if (c.z_eglGetCurrentContext.?() != ctx.context) return n.UHDK_STATUS_BUSY; + if (c.z_eglSwapBuffers.?(b.egl_display, ctx.surface) == c.EGL_FALSE) return n.UHDK_STATUS_OS_ERROR; + }, + 7 => { + if (c.z_eglGetCurrentContext.?() != ctx.context) return n.UHDK_STATUS_BUSY; + const interval: *const i32 = @ptrCast(@alignCast(out.?)); + if (c.z_eglSwapInterval.?(b.egl_display, interval.*) == c.EGL_FALSE) return n.UHDK_STATUS_OS_ERROR; + }, + else => return abi.INVALID, + } + return abi.OK; +} +pub fn proc(ptr: ?*anyopaque, name: n.uhdk_text, out: [*c]n.uhdk_gl_proc) u32 { + const ctx: *Context = @ptrCast(@alignCast(ptr orelse return abi.INVALID)); + if (frame != ctx or c.z_eglGetCurrentContext.?() != ctx.context) return n.UHDK_STATUS_BUSY; + const allocator = ctx.window.owner.gpa; + const terminated = allocator.dupeZ(u8, abi.textBytes(name)) catch return abi.OOM; + defer allocator.free(terminated); + const address = c.z_eglGetProcAddress.?(terminated) orelse return n.UHDK_STATUS_NOT_FOUND; + out.* = @ptrCast(address); + return abi.OK; +} diff --git a/src/linux/Files.zig b/src/linux/Files.zig new file mode 100644 index 0000000..42fb59c --- /dev/null +++ b/src/linux/Files.zig @@ -0,0 +1,66 @@ +//! Local file URIs preserve arbitrary POSIX filename bytes. +const std = @import("std"); +const n = @import("native"); +const abi = @import("../abi.zig"); +const runtime = @import("../runtime.zig"); + +fn decode(allocator: std.mem.Allocator, uri: []const u8) ![]u8 { + const encoded = if (std.mem.startsWith(u8, uri, "file:///")) uri[7..] else if (std.mem.startsWith(u8, uri, "file://localhost/")) uri[16..] else return error.Invalid; + var result: std.Io.Writer.Allocating = .init(allocator); + errdefer result.deinit(); + var i: usize = 0; + while (i < encoded.len) : (i += 1) { + var byte = encoded[i]; + if (byte == '%') { + if (encoded.len - i < 3) return error.Invalid; + if (!std.ascii.isHex(encoded[i + 1]) or !std.ascii.isHex(encoded[i + 2])) return error.Invalid; + byte = std.fmt.parseInt(u8, encoded[i + 1 ..][0..2], 16) catch return error.Invalid; + i += 2; + } else if (byte == '?' or byte == '#') return error.Invalid; + if (byte == 0) return error.Invalid; + try result.writer.writeByte(byte); + } + return result.toOwnedSlice(); +} +fn encode(allocator: std.mem.Allocator, path: []const u8) ![]u8 { + var result: std.Io.Writer.Allocating = .init(allocator); + errdefer result.deinit(); + try result.writer.writeAll("file://"); + for (path) |byte| { + if (std.ascii.isAlphanumeric(byte) or std.mem.findScalar(u8, "/-._~", byte) != null) { + try result.writer.writeByte(byte); + } else try result.writer.print("%{X:0>2}", .{byte}); + } + return result.toOwnedSlice(); +} +pub fn init(file: ?*n.uhdk_file, path: n.uhdk_bytes, uri: n.uhdk_text, grant: ?*anyopaque) u32 { + if (grant != null) return n.UHDK_STATUS_UNSUPPORTED; + const object = runtime.fromObjectHandle(file); + var arena: std.heap.ArenaAllocator = .init(object.app.gpa); + defer arena.deinit(); + const allocator = arena.allocator(); + const bytes = if (path.len != 0) abi.bytes(path) else decode(allocator, abi.textBytes(uri)) catch |err| return if (err == error.Invalid) abi.INVALID else abi.OOM; + if (bytes.len == 0 or bytes[0] != '/' or std.mem.findScalar(u8, bytes, 0) != null) return abi.INVALID; + const encoded = encode(allocator, bytes) catch return abi.OOM; + const basename = std.fs.path.basename(bytes); + // Display text is lossy only when the native byte name isn't UTF-8. + const display = if (std.unicode.utf8ValidateSlice(basename)) basename else "(non-UTF-8 filename)"; + return n.z_file_init(file, .{ .ptr = bytes.ptr, .len = bytes.len }, abi.textSpan(encoded), abi.textSpan(display), null); +} + +test "file URI preserves POSIX bytes and rejects remote, NUL, malformed escapes" { + const allocator = std.testing.allocator; + const original = "/tmp/a b%#\xff"; + const uri = try encode(allocator, original); + defer allocator.free(uri); + try std.testing.expectEqualStrings("file:///tmp/a%20b%25%23%FF", uri); + const path = try decode(allocator, uri); + defer allocator.free(path); + try std.testing.expectEqualStrings(original, path); + for ([_][]const u8{ "https://host/file", "file://host/path", "file:///a%", "file:///a%XX", "file:///a%00", "file:///a%+1", "file:///a#fragment" }) |bad| { + try std.testing.expectError(error.Invalid, decode(allocator, bad)); + } + const local = try decode(allocator, "file://localhost/tmp/a"); + defer allocator.free(local); + try std.testing.expectEqualStrings("/tmp/a", local); +} diff --git a/src/linux/README.md b/src/linux/README.md new file mode 100644 index 0000000..7b6a16c --- /dev/null +++ b/src/linux/README.md @@ -0,0 +1,126 @@ +# Linux Wayland preview + +The first implementation targets KDE Plasma Wayland using standard protocols. +It implements window creation/destruction, titles, parent relationships, size +constraints, maximize/fullscreen/minimize requests, show/hide, close requests, +logical/pixel metrics, EGL desktop OpenGL, and local POSIX file references. + +Keyboard/pointer input, cursors, text input/IME, clipboard, drag and drop, +portals, preferences, file watches, native menus, icons and activation requests +are not implemented yet. Their capabilities are false and requests return +`UNSUPPORTED`. X11 is also unsupported. This is a window/graphics foundation, +not yet a complete interactive desktop backend. + +## Build and run + +Normal builds require only Zig 0.16.0 on a glibc Linux target: + +```sh +zig build +zig build check +zig build test +zig build -Dshared=true +``` + +These commands do not download dependencies or run Python, a protocol scanner, +pkg-config, Nix, or an external C compiler. Zig compiles the checked-in protocol +C files and translates the private headers at build time. + +At runtime, UHDK opens `libwayland-client.so.0`. OpenGL also needs +`libwayland-egl.so.1`, `libEGL.so.1` and a working system graphics driver. +Missing EGL disables the OpenGL capability without preventing window creation. +No desktop libraries are linked or bundled. EGL 1.5 and desktop GL 3.3 core are +required for graphics; there is no OpenGL ES fallback. A non-null capability +means the mechanism is available, not that every context creation must succeed. + +`AUTO` selects Wayland; explicit X11 requests return `UNSUPPORTED`. Before a +successful run, the Linux backend query returns `AUTO` and all capabilities are +false. Startup discovers the registry before publishing `READY`. The protocol +handshake and initial window configure have five-second deadlines. Display +failure requests shutdown and cancels pending work; it never switches backends. + +## NixOS development + +NixOS does not normally expose every desktop library through the conventional +runtime linker search path. The optional package-owned shell supplies the pinned +scanner, Python, clang-format and runtime library paths: + +```sh +nix-shell +# Run a native application or the native tests from this shell. +zig build test-linux-native +``` + +Zig 0.16.0 should already be on PATH. The shell is optional for compilation. +Its nixpkgs revision and source hash live in `nix/pkgs.nix`. It also supports +maintainer [header/protocol refreshes](vendor/README.md). Running packaged apps +on NixOS should use the application's normal Nix wrapper and graphics setup. +UHDK first loads library SONAMEs, then checks the stable system profiles +`/run/current-system/sw/lib` and `/run/opengl-driver/lib`; it does not search +arbitrary Nix store directories or embed store paths. + +## Portable executables + +An executable built for the native NixOS target can name a Nix store loader. +For distribution on conventional glibc systems, build the application explicitly +for the chosen baseline, for example: + +```sh +zig build -Dtarget=x86_64-linux-gnu.2.35 -Dcpu=baseline -Doptimize=ReleaseSafe +python3 src/linux/tools/audit-elf.py /path/to/application +``` + +The audit checks the executable's ELF interpreter, runtime dependencies and +required glibc symbol versions. This is not a substitute for running the artifact +on each supported distribution/driver. The current acceptance environment is +NixOS with Plasma 6.5.6 and Mesa 25.2.6; other systems are not runtime-qualified. + +## Window and GL semantics + +- A decorated window requires xdg-decoration server-side mode. A compositor that + lacks it returns `UNSUPPORTED`; a libdecor adapter is future work. Undecorated + windows remain possible. No desktop-name checks or Qt/KDE linkage are used. +- The application ID is copied from `uhdk_app_desc` and sent as the xdg-toplevel + app ID. Applications own their desktop-file and icon installation. +- A window is created hidden after its first configure. Showing requests mapping; + the first successful buffer swap supplies its content. `visible` is the + application's show/hide intent, not proof that the compositor displays it. +- Size/state commands acknowledge submission. Confirmed geometry and focus arrive + through metrics. Wayland cannot report minimized state, force focus, or directly + undo minimization. Activation tokens will be added separately. +- Fractional scale uses fractional-scale-v1 plus viewporter; otherwise preferred + integer buffer scale is used when wl_surface v6 is available. Older compositors + without those mechanisms currently use scale 1. Fractional/mixed-DPI behavior + still needs runtime validation. +- Native requests and protocol dispatch run on the OS main thread. GL work runs + on the application thread. Main-thread drawable updates use a nonblocking lock + attempt; the dispatcher never waits behind an in-progress swap. End-frame wakes + it to retry deferred updates. +- Hiding replaces the EGL presentation surface so an outstanding frame callback + cannot hang remapping. The GL context and its application-owned resources + survive. Swapping a hidden/unconfigured window returns `UNAVAILABLE`. +- Every GL call belongs inside begin/end-frame. Clear/make-current are supported + within that bracket. Actual GL version reporting is queried in the active + context. Window framebuffer dimensions describe the presentation target; + applications continue to own their viewport and rendering resources. + +## Validation + +From the package directory: + +```sh +zig build check # also compile the native harness +zig build test # non-graphical core and file URI tests +zig build test-linux-native # opens windows in the active session +zig build linux-test-bin +python3 src/linux/tests/startup.py zig-out/bin/linux-native-tests +python3 src/linux/tests/disconnect.py zig-out/bin/linux-native-tests +python3 src/linux/tools/protocols.py --check +``` + +The native harness exercises repeated application/window lifetimes, cancellation, +shutdown with queued creation, show/hide/remapping, resize, GL context recreation, +actual GL version, pixel readback and byte-preserving file URIs. The startup test +uses isolated sockets for absent, disconnected and unresponsive servers. The +runtime-disconnect test proxies and closes only its own client connection; it +does not stop or modify the desktop compositor. diff --git a/src/linux/bindings.h b/src/linux/bindings.h new file mode 100644 index 0000000..1c58fb1 --- /dev/null +++ b/src/linux/bindings.h @@ -0,0 +1,34 @@ +#ifndef UHDK_LINUX_BINDINGS_H +#define UHDK_LINUX_BINDINGS_H +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include "wayland-client-core.h" +#include "wayland-egl-core.h" +#define EGL_NO_X11 +#include "EGL/egl.h" +#include "EGL/eglext.h" +#include "private-symbols.h" + +/* Runtime imports are checked against upstream declarations in libraries.c. + * typeof(function) in translated Zig debug info can create unwanted link-time + * references, so the translated header uses explicit function-pointer types. */ +#include "function-pointers.h" +#include "redirects.h" +#include "wayland-client-protocol.h" +#include "xdg-shell.h" +#include "xdg-decoration-unstable-v1.h" +#include "viewporter.h" +#include "fractional-scale-v1.h" +int z_linux_libraries_open(void); +int z_linux_egl_available(void); +void z_linux_libraries_close(void); +int z_linux_errno(void); +int z_linux_poll(struct pollfd *, nfds_t, int); +ssize_t z_linux_read(int, void *, size_t); +ssize_t z_linux_write(int, const void *, size_t); +int z_linux_is_main_thread(void); +#endif diff --git a/src/linux/build.zig b/src/linux/build.zig new file mode 100644 index 0000000..639b930 --- /dev/null +++ b/src/linux/build.zig @@ -0,0 +1,34 @@ +const std = @import("std"); + +pub fn configure(b: *std.Build, module: *std.Build.Module, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) void { + const bindings = b.addTranslateC(.{ + .root_source_file = b.path("src/linux/bindings.h"), + .target = target, + .optimize = optimize, + }); + for ([_][]const u8{ "src/linux", "src/linux/vendor/wayland", "src/linux/vendor/khronos", "src/linux/protocols/generated" }) |path| { + bindings.addIncludePath(b.path(path)); + module.addIncludePath(b.path(path)); + } + module.addImport("linux_c", bindings.createModule()); + module.addCSourceFile(.{ .file = b.path("src/linux/libraries.c"), .flags = &.{ "-std=gnu11", "-Wall", "-Wextra", "-Werror", "-fvisibility=hidden" } }); + for ([_][]const u8{ "wayland", "xdg-shell", "xdg-decoration-unstable-v1", "viewporter", "fractional-scale-v1" }) |name| { + module.addCSourceFile(.{ + .file = b.path(b.fmt("src/linux/protocols/generated/{s}.c", .{name})), + .flags = &.{ "-std=c99", "-include", b.path("src/linux/protocols/generated/private-symbols.h").getPath(b) }, + }); + } + module.linkSystemLibrary("dl", .{}); +} + +pub fn addTests(b: *std.Build, lib: *std.Build.Step.Compile, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, check: *std.Build.Step) void { + const test_module = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true }); + test_module.addIncludePath(b.path("include")); + test_module.addCSourceFile(.{ .file = b.path("src/linux/tests/native.c"), .flags = &.{ "-std=c99", "-Wall", "-Wextra", "-Werror" } }); + test_module.linkLibrary(lib); + const exe = b.addExecutable(.{ .name = "linux-native-tests", .root_module = test_module }); + check.dependOn(&exe.step); + b.step("test-linux-native", "Exercise Wayland/EGL lifecycle in the active desktop session").dependOn(&b.addRunArtifact(exe).step); + const install = b.addInstallArtifact(exe, .{}); + b.step("linux-test-bin", "Install the Linux native test harness").dependOn(&install.step); +} diff --git a/src/linux/function-pointers.h b/src/linux/function-pointers.h new file mode 100644 index 0000000..1ff7eb1 --- /dev/null +++ b/src/linux/function-pointers.h @@ -0,0 +1,42 @@ +/* Typed runtime imports. libraries.c defines these using upstream typeof, so + * the C compiler checks the Wayland signatures here against the pinned ABI. */ +extern struct wl_display *(*z_wl_display_connect)(const char *); +extern void (*z_wl_display_disconnect)(struct wl_display *); +extern int (*z_wl_display_get_fd)(struct wl_display *); +extern int (*z_wl_display_dispatch_pending)(struct wl_display *); +extern int (*z_wl_display_prepare_read)(struct wl_display *); +extern int (*z_wl_display_read_events)(struct wl_display *); +extern void (*z_wl_display_cancel_read)(struct wl_display *); +extern int (*z_wl_display_flush)(struct wl_display *); +extern struct wl_proxy *(*z_wl_proxy_marshal_flags)( + struct wl_proxy *, + uint32_t, + const struct wl_interface *, + uint32_t, + uint32_t, + ... +); +extern int (*z_wl_proxy_add_listener)(struct wl_proxy *, void (**)(void), void *); +extern void (*z_wl_proxy_destroy)(struct wl_proxy *); +extern uint32_t (*z_wl_proxy_get_version)(struct wl_proxy *); +extern void (*z_wl_proxy_set_user_data)(struct wl_proxy *, void *); +extern void *(*z_wl_proxy_get_user_data)(struct wl_proxy *); +extern struct wl_egl_window *(*z_wl_egl_window_create)(struct wl_surface *, int, int); +extern void (*z_wl_egl_window_destroy)(struct wl_egl_window *); +extern void (*z_wl_egl_window_resize)(struct wl_egl_window *, int, int, int, int); + +extern PFNEGLGETPLATFORMDISPLAYPROC z_eglGetPlatformDisplay; +extern PFNEGLINITIALIZEPROC z_eglInitialize; +extern PFNEGLTERMINATEPROC z_eglTerminate; +extern PFNEGLBINDAPIPROC z_eglBindAPI; +extern PFNEGLCHOOSECONFIGPROC z_eglChooseConfig; +extern PFNEGLCREATECONTEXTPROC z_eglCreateContext; +extern PFNEGLDESTROYCONTEXTPROC z_eglDestroyContext; +extern PFNEGLCREATEWINDOWSURFACEPROC z_eglCreateWindowSurface; +extern PFNEGLDESTROYSURFACEPROC z_eglDestroySurface; +extern PFNEGLMAKECURRENTPROC z_eglMakeCurrent; +extern PFNEGLGETCURRENTCONTEXTPROC z_eglGetCurrentContext; +extern PFNEGLSWAPBUFFERSPROC z_eglSwapBuffers; +extern PFNEGLSWAPINTERVALPROC z_eglSwapInterval; +extern PFNEGLGETPROCADDRESSPROC z_eglGetProcAddress; +extern PFNEGLRELEASETHREADPROC z_eglReleaseThread; diff --git a/src/linux/functions.inc b/src/linux/functions.inc new file mode 100644 index 0000000..34ef01f --- /dev/null +++ b/src/linux/functions.inc @@ -0,0 +1,32 @@ +Z_FUNCTION(wl_display_connect) +Z_FUNCTION(wl_display_disconnect) +Z_FUNCTION(wl_display_get_fd) +Z_FUNCTION(wl_display_dispatch_pending) +Z_FUNCTION(wl_display_prepare_read) +Z_FUNCTION(wl_display_read_events) +Z_FUNCTION(wl_display_cancel_read) +Z_FUNCTION(wl_display_flush) +Z_FUNCTION(wl_proxy_marshal_flags) +Z_FUNCTION(wl_proxy_add_listener) +Z_FUNCTION(wl_proxy_destroy) +Z_FUNCTION(wl_proxy_get_version) +Z_FUNCTION(wl_proxy_set_user_data) +Z_FUNCTION(wl_proxy_get_user_data) +Z_FUNCTION(wl_egl_window_create) +Z_FUNCTION(wl_egl_window_destroy) +Z_FUNCTION(wl_egl_window_resize) +Z_FUNCTION(eglGetPlatformDisplay) +Z_FUNCTION(eglInitialize) +Z_FUNCTION(eglTerminate) +Z_FUNCTION(eglBindAPI) +Z_FUNCTION(eglChooseConfig) +Z_FUNCTION(eglCreateContext) +Z_FUNCTION(eglDestroyContext) +Z_FUNCTION(eglCreateWindowSurface) +Z_FUNCTION(eglDestroySurface) +Z_FUNCTION(eglMakeCurrent) +Z_FUNCTION(eglGetCurrentContext) +Z_FUNCTION(eglSwapBuffers) +Z_FUNCTION(eglSwapInterval) +Z_FUNCTION(eglGetProcAddress) +Z_FUNCTION(eglReleaseThread) diff --git a/src/linux/libraries.c b/src/linux/libraries.c new file mode 100644 index 0000000..cd289c4 --- /dev/null +++ b/src/linux/libraries.c @@ -0,0 +1,98 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include "wayland-client-core.h" +#include "wayland-egl-core.h" +#define EGL_NO_X11 +#include "EGL/egl.h" +#include "EGL/eglext.h" +#include "function-pointers.h" +#define Z_FUNCTION(name) __typeof__(name) *z_##name; +#include "functions.inc" +#undef Z_FUNCTION + +static void *wayland, *wayland_egl, *egl; +static int have_egl; + +static void *open_library(const char *name) { + void *handle = dlopen(name, RTLD_NOW | RTLD_LOCAL); + if (handle) + return handle; + /* Stable system profiles only; never bake a Nix store path into the binary. */ + const char *prefixes[] = {"/run/current-system/sw/lib/", "/run/opengl-driver/lib/"}; + char path[256]; + for (unsigned i = 0; i < sizeof(prefixes) / sizeof(prefixes[0]); ++i) { + snprintf(path, sizeof(path), "%s%s", prefixes[i], name); + handle = dlopen(path, RTLD_NOW | RTLD_LOCAL); + if (handle) + return handle; + } + return NULL; +} + +void z_linux_libraries_close(void) { + have_egl = 0; + if (egl) + dlclose(egl); + if (wayland_egl) + dlclose(wayland_egl); + if (wayland) + dlclose(wayland); + wayland = wayland_egl = egl = NULL; +#define Z_FUNCTION(name) z_##name = NULL; +#include "functions.inc" +#undef Z_FUNCTION +} + +int z_linux_libraries_open(void) { + wayland = open_library("libwayland-client.so.0"); + if (!wayland) + return 0; + wayland_egl = open_library("libwayland-egl.so.1"); + egl = open_library("libEGL.so.1"); + have_egl = wayland_egl && egl; +#define Z_FUNCTION(name) \ + do { \ + const char *symbol = #name; \ + int graphics = strncmp(symbol, "egl", 3) == 0 || strncmp(symbol, "wl_egl_", 7) == 0; \ + void *library = strncmp(symbol, "egl", 3) == 0 ? egl : (graphics ? wayland_egl : wayland); \ + if (library) \ + z_##name = (__typeof__(z_##name))dlsym(library, symbol); \ + if (!z_##name) { \ + if (!graphics) { \ + z_linux_libraries_close(); \ + return 0; \ + } \ + have_egl = 0; \ + } \ + } while (0); +#include "functions.inc" +#undef Z_FUNCTION + return 1; +} + +int z_linux_egl_available(void) { + return have_egl; +} +int z_linux_errno(void) { + return errno; +} +int z_linux_is_main_thread(void) { + return syscall(SYS_gettid) == getpid(); +} + +/* Keep libc's optimization/fortify inline implementation on the C side. */ +int z_linux_poll(struct pollfd *fds, nfds_t count, int timeout) { + return poll(fds, count, timeout); +} +ssize_t z_linux_read(int fd, void *buffer, size_t count) { + return read(fd, buffer, count); +} +ssize_t z_linux_write(int fd, const void *buffer, size_t count) { + return write(fd, buffer, count); +} diff --git a/src/linux/native.zig b/src/linux/native.zig new file mode 100644 index 0000000..0e60cbc --- /dev/null +++ b/src/linux/native.zig @@ -0,0 +1,558 @@ +//! Wayland platform owner. Protocol dispatch and native commands stay on the OS +//! thread; the application thread holds each window's lock only while rendering. +const std = @import("std"); +const c = @import("linux_c"); +const n = @import("native"); +const abi = @import("../abi.zig"); +const runtime = @import("../runtime.zig"); +const Application = @import("../Application.zig"); +const Egl = @import("Egl.zig"); + +pub const Window = struct { + next: ?*Window = null, + owner: *Application, + handle: ?*n.uhdk_window, + surface: ?*c.struct_wl_surface = null, + xdg: ?*c.struct_xdg_surface = null, + top: ?*c.struct_xdg_toplevel = null, + decoration: ?*c.struct_zxdg_toplevel_decoration_v1 = null, + viewport: ?*c.struct_wp_viewport = null, + fractional: ?*c.struct_wp_fractional_scale_v1 = null, + egl: ?*Egl.Context = null, + mutex: std.Io.Mutex = .init, + request: u64 = 0, + deadline: u64 = 0, + serial: ?u32 = null, + dirty: bool = true, + configured: bool = false, + want_visible: bool = false, + visible: bool = false, + width: i32, + height: i32, + min_width: i32, + min_height: i32, + max_width: i32, + max_height: i32, + scale: u32 = 120, + focused: bool = false, + state: u32 = n.UHDK_WINDOW_NORMAL, + flags: u32, + metrics: n.uhdk_window_metrics = std.mem.zeroes(n.uhdk_window_metrics), +}; + +pub const Backend = struct { + app: *Application, + display: ?*c.struct_wl_display = null, + registry: ?*c.struct_wl_registry = null, + compositor: ?*c.struct_wl_compositor = null, + shell: ?*c.struct_xdg_wm_base = null, + decorations: ?*c.struct_zxdg_decoration_manager_v1 = null, + viewporter: ?*c.struct_wp_viewporter = null, + fractional: ?*c.struct_wp_fractional_scale_manager_v1 = null, + windows: ?*Window = null, + egl_display: c.EGLDisplay = null, + egl_config: c.EGLConfig = null, + running: bool = true, + failed: std.atomic.Value(bool) = .init(false), +}; +pub var instance: ?*Backend = null; +var wake_fd: std.atomic.Value(c_int) = .init(-1); + +pub fn backend(handle: ?*n.uhdk_app) u32 { + if (handle == null) return n.UHDK_BACKEND_AUTO; + const app = runtime.fromAppHandle(handle); + return if (app.has_run) n.UHDK_BACKEND_WAYLAND else n.UHDK_BACKEND_AUTO; +} +pub fn capability(handle: ?*n.uhdk_app, cap: u32) n.uhdk_bool { + if (handle == null) return 0; + const b = instance orelse return 0; + if (b.app != runtime.fromAppHandle(handle) or !b.app.running or b.failed.load(.acquire)) return 0; + return @intFromBool(cap == n.UHDK_CAP_OPENGL and b.egl_display != null); +} + +export fn z_native_is_main_thread() c_int { + return c.z_linux_is_main_thread(); +} +export fn z_native_start(handle: ?*n.uhdk_app, _: n.uhdk_text) u32 { + const app = runtime.fromAppHandle(handle); + if (c.z_linux_libraries_open() == 0) return abi.UNAVAILABLE; + const b = app.gpa.create(Backend) catch { + c.z_linux_libraries_close(); + return abi.OOM; + }; + b.* = .{ .app = app }; + instance = b; + const fd = c.eventfd(0, c.EFD_CLOEXEC | c.EFD_NONBLOCK); + if (fd < 0) { + z_native_finish(); + return n.UHDK_STATUS_OS_ERROR; + } + wake_fd.store(fd, .release); + b.display = c.z_wl_display_connect.?(null); + if (b.display == null) { + z_native_finish(); + return abi.UNAVAILABLE; + } + b.registry = c.wl_display_get_registry(b.display); + if (b.registry == null) { + z_native_finish(); + return abi.OOM; + } + if (c.wl_registry_add_listener(b.registry, ®istry_listener, b) != 0 or !initialRegistry(b)) { + z_native_finish(); + return n.UHDK_STATUS_OS_ERROR; + } + if (b.compositor == null or b.shell == null) { + z_native_finish(); + return n.UHDK_STATUS_UNSUPPORTED; + } + Egl.initialize(b); + return abi.OK; +} +// Startup also uses a bounded read/flush/poll handshake: an unresponsive +// compositor must not trap app_run before the application thread even starts. +fn initialRegistry(b: *Backend) bool { + var done = false; + const callback = c.wl_display_sync(b.display) orelse return false; + defer c.wl_callback_destroy(callback); + const listener: c.struct_wl_callback_listener = .{ .done = registryDone }; + if (c.wl_callback_add_listener(callback, &listener, &done) != 0) return false; + const deadline = runtime.now(b.app.io) + 5 * std.time.ns_per_s; + while (!done) { + if (c.z_wl_display_dispatch_pending.?(b.display) < 0) return false; + if (done) return true; + const now = runtime.now(b.app.io); + if (now >= deadline) return false; + if (c.z_wl_display_prepare_read.?(b.display) != 0) continue; + var events: c_short = c.POLLIN; + if (c.z_wl_display_flush.?(b.display) < 0) { + if (c.z_linux_errno() == c.EAGAIN) events |= c.POLLOUT else { + c.z_wl_display_cancel_read.?(b.display); + return false; + } + } + var fd: c.struct_pollfd = .{ .fd = c.z_wl_display_get_fd.?(b.display), .events = events, .revents = 0 }; + const ready = c.z_linux_poll(&fd, 1, @intCast(@min(5000, (deadline - now + std.time.ns_per_ms - 1) / std.time.ns_per_ms))); + if (ready > 0 and fd.revents & c.POLLIN != 0) { + if (c.z_wl_display_read_events.?(b.display) < 0) return false; + } else c.z_wl_display_cancel_read.?(b.display); + if (ready < 0 and c.z_linux_errno() != c.EINTR) return false; + if (fd.revents & (c.POLLHUP | c.POLLERR | c.POLLNVAL) != 0) return false; + } + return true; +} +fn registryDone(data: ?*anyopaque, _: ?*c.struct_wl_callback, _: u32) callconv(.c) void { + const done: *bool = @ptrCast(@alignCast(data.?)); + done.* = true; +} +export fn z_native_finish() void { + const b = instance orelse return; + std.debug.assert(b.windows == null); + Egl.finish(b); + if (b.fractional != null) c.wp_fractional_scale_manager_v1_destroy(b.fractional); + if (b.viewporter != null) c.wp_viewporter_destroy(b.viewporter); + if (b.decorations != null) c.zxdg_decoration_manager_v1_destroy(b.decorations); + if (b.shell != null) c.xdg_wm_base_destroy(b.shell); + if (b.compositor != null) c.wl_compositor_destroy(b.compositor); + if (b.registry != null) c.wl_registry_destroy(b.registry); + if (b.display != null) c.z_wl_display_disconnect.?(b.display); + const fd = wake_fd.swap(-1, .acq_rel); + if (fd >= 0) _ = c.close(fd); + const gpa = b.app.gpa; + instance = null; + gpa.destroy(b); + c.z_linux_libraries_close(); +} +export fn z_native_wake() void { + const fd = wake_fd.load(.acquire); + if (fd < 0) return; + const one: u64 = 1; + while (c.z_linux_write(fd, &one, @sizeOf(u64)) < 0) { + if (c.z_linux_errno() != c.EINTR) break; // EAGAIN already means readable. + } +} +export fn z_native_again() void { + z_native_wake(); +} +export fn z_native_stop() void { + if (instance) |b| b.running = false; +} +export fn z_native_pool_push() ?*anyopaque { + return null; +} +export fn z_native_pool_pop(_: ?*anyopaque) void {} + +fn fail(b: *Backend) void { + if (b.failed.swap(true, .acq_rel)) return; + b.app.lock(); + b.app.stopping = true; + b.app.unlock(); + runtime.simpleEvent(b.app, n.UHDK_EVENT_QUIT_REQUESTED); +} +export fn z_native_run() void { + const b = instance.?; + while (b.running) { + if (!b.failed.load(.acquire) and c.z_wl_display_dispatch_pending.?(b.display) < 0) fail(b); + n.z_tick(runtime.toAppHandle(b.app)); + var cursor = b.windows; + var timeout: c_int = -1; + while (cursor) |w| { + cursor = w.next; + if (w.request != 0) { + timeout = 50; + if (runtime.now(b.app.io) >= w.deadline) { + completeCreation(w, abi.UNAVAILABLE); + continue; + } + } + if (!b.failed.load(.acquire)) apply(w); + } + if (!b.running) break; + var reading = false; + var display_events: c_short = c.POLLIN; + if (!b.failed.load(.acquire)) { + if (c.z_wl_display_prepare_read.?(b.display) != 0) continue; + reading = true; + if (c.z_wl_display_flush.?(b.display) < 0) { + if (c.z_linux_errno() == c.EAGAIN) display_events |= c.POLLOUT else { + c.z_wl_display_cancel_read.?(b.display); + fail(b); + continue; + } + } + } + var fds = [_]c.struct_pollfd{ + .{ .fd = if (reading) c.z_wl_display_get_fd.?(b.display) else -1, .events = display_events, .revents = 0 }, + .{ .fd = wake_fd.load(.acquire), .events = c.POLLIN, .revents = 0 }, + }; + const ready = c.z_linux_poll(&fds, fds.len, timeout); + if (reading) { + if (ready > 0 and fds[0].revents & c.POLLIN != 0) { + if (c.z_wl_display_read_events.?(b.display) < 0) fail(b); + } else c.z_wl_display_cancel_read.?(b.display); + } + if (ready < 0 and c.z_linux_errno() != c.EINTR) fail(b); + if (fds[0].revents & (c.POLLERR | c.POLLHUP | c.POLLNVAL) != 0) fail(b); + if (fds[1].revents & c.POLLIN != 0) { + var value: u64 = 0; + _ = c.z_linux_read(fds[1].fd, &value, @sizeOf(u64)); + } + } +} + +fn registryGlobal(data: ?*anyopaque, registry: ?*c.struct_wl_registry, name: u32, interface: [*c]const u8, version: u32) callconv(.c) void { + const b: *Backend = @ptrCast(@alignCast(data.?)); + const str = std.mem.span(interface); + if (std.mem.eql(u8, str, "wl_compositor") and version >= 4) { + b.compositor = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wl_compositor_interface, @min(version, 6))); + } else if (std.mem.eql(u8, str, "xdg_wm_base")) { + b.shell = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_xdg_wm_base_interface, @min(version, 6))); + if (b.shell != null) _ = c.xdg_wm_base_add_listener(b.shell, &shell_listener, b); + } else if (std.mem.eql(u8, str, "zxdg_decoration_manager_v1")) { + b.decorations = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_zxdg_decoration_manager_v1_interface, 1)); + } else if (std.mem.eql(u8, str, "wp_viewporter")) { + b.viewporter = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wp_viewporter_interface, 1)); + } else if (std.mem.eql(u8, str, "wp_fractional_scale_manager_v1")) { + b.fractional = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wp_fractional_scale_manager_v1_interface, 1)); + } +} +fn registryRemove(_: ?*anyopaque, _: ?*c.struct_wl_registry, _: u32) callconv(.c) void {} +const registry_listener: c.struct_wl_registry_listener = .{ .global = registryGlobal, .global_remove = registryRemove }; +fn ping(_: ?*anyopaque, shell: ?*c.struct_xdg_wm_base, serial: u32) callconv(.c) void { + c.xdg_wm_base_pong(shell, serial); +} +const shell_listener: c.struct_xdg_wm_base_listener = .{ .ping = ping }; +fn window(data: ?*anyopaque) *Window { + return @ptrCast(@alignCast(data.?)); +} +fn configure(data: ?*anyopaque, _: ?*c.struct_xdg_surface, serial: u32) callconv(.c) void { + const w = window(data); + w.serial = serial; + w.dirty = true; +} +const surface_listener: c.struct_xdg_surface_listener = .{ .configure = configure }; +fn topConfigure(data: ?*anyopaque, _: ?*c.struct_xdg_toplevel, width: i32, height: i32, states: [*c]c.struct_wl_array) callconv(.c) void { + const w = window(data); + if (width > 0) w.width = width; + if (height > 0) w.height = height; + w.focused = false; + w.state = n.UHDK_WINDOW_NORMAL; + if (states.*.size == 0) return; + const values: [*]const u32 = @ptrCast(@alignCast(states.*.data)); + for (values[0 .. states.*.size / @sizeOf(u32)]) |value| { + switch (value) { + c.XDG_TOPLEVEL_STATE_ACTIVATED => w.focused = true, + c.XDG_TOPLEVEL_STATE_MAXIMIZED => if (w.state != n.UHDK_WINDOW_FULLSCREEN) { + w.state = n.UHDK_WINDOW_MAXIMIZED; + }, + c.XDG_TOPLEVEL_STATE_FULLSCREEN => w.state = n.UHDK_WINDOW_FULLSCREEN, + else => {}, + } + } +} +fn emit(w: *Window, kind: u32) void { + var event = std.mem.zeroes(n.z_event); + event.type = kind; + event.window = w.handle; + n.z_emit(runtime.toAppHandle(w.owner), &event); +} +fn topClose(data: ?*anyopaque, _: ?*c.struct_xdg_toplevel) callconv(.c) void { + emit(window(data), n.UHDK_EVENT_WINDOW_CLOSE_REQUESTED); +} +fn bounds(_: ?*anyopaque, _: ?*c.struct_xdg_toplevel, _: i32, _: i32) callconv(.c) void {} +fn wmCaps(_: ?*anyopaque, _: ?*c.struct_xdg_toplevel, _: [*c]c.struct_wl_array) callconv(.c) void {} +const top_listener: c.struct_xdg_toplevel_listener = .{ .configure = topConfigure, .close = topClose, .configure_bounds = bounds, .wm_capabilities = wmCaps }; +fn decorationConfigure(data: ?*anyopaque, _: ?*c.struct_zxdg_toplevel_decoration_v1, mode: u32) callconv(.c) void { + const w = window(data); + // Until the client-decoration adapter exists, don't silently create a + // decorated window with no title bar on compositors that decline SSD. + if (mode != c.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE and w.request != 0) completeCreation(w, n.UHDK_STATUS_UNSUPPORTED); +} +const decoration_listener: c.struct_zxdg_toplevel_decoration_v1_listener = .{ .configure = decorationConfigure }; +fn fraction(data: ?*anyopaque, _: ?*c.struct_wp_fractional_scale_v1, scale: u32) callconv(.c) void { + const w = window(data); + if (scale == 0 or scale > 120 * 16) return; + w.scale = scale; + w.dirty = true; +} +const fraction_listener: c.struct_wp_fractional_scale_v1_listener = .{ .preferred_scale = fraction }; +fn enter(_: ?*anyopaque, _: ?*c.struct_wl_surface, _: ?*c.struct_wl_output) callconv(.c) void {} +fn bufferScale(data: ?*anyopaque, _: ?*c.struct_wl_surface, scale: i32) callconv(.c) void { + const w = window(data); + if (w.fractional == null and scale > 0 and scale <= 16) { + w.scale = @as(u32, @intCast(scale)) * 120; + w.dirty = true; + } +} +fn bufferTransform(_: ?*anyopaque, _: ?*c.struct_wl_surface, _: u32) callconv(.c) void {} +const wl_surface_listener: c.struct_wl_surface_listener = .{ .enter = enter, .leave = enter, .preferred_buffer_scale = bufferScale, .preferred_buffer_transform = bufferTransform }; + +fn completeCreation(w: *Window, status: u32) void { + const id = w.request; + if (id == 0) return; + w.request = 0; + const app = runtime.toAppHandle(w.owner); + var result = std.mem.zeroes(n.z_request_result); + result.value.window = w.handle; + if (status != abi.OK) { + n.z_object_set_native(w.handle, null); + destroyWindow(w); + } + n.z_complete(app, id, status, &result); +} + +fn apply(w: *Window) void { + if (!w.dirty or !w.mutex.tryLock()) return; + // Never block the display dispatcher behind eglSwapBuffers, which itself + // may be waiting for the dispatcher. end_frame wakes us to retry. + if (w.serial) |serial| { + c.xdg_surface_ack_configure(w.xdg, serial); + w.serial = null; + w.configured = true; + } + if (!w.configured) { + w.mutex.unlock(w.owner.io); + return; + } + const width = @min(w.width, 65536); + const height = @min(w.height, 65536); + const pixel_width: u32 = @intCast(@divTrunc(@as(i64, width) * w.scale + 119, 120)); + const pixel_height: u32 = @intCast(@divTrunc(@as(i64, height) * w.scale + 119, 120)); + if (w.viewport != null) { + c.wl_surface_set_buffer_scale(w.surface, 1); + c.wp_viewport_set_destination(w.viewport, width, height); + } else c.wl_surface_set_buffer_scale(w.surface, @intCast(w.scale / 120)); + c.xdg_surface_set_window_geometry(w.xdg, 0, 0, width, height); + if (w.egl) |ctx| c.z_wl_egl_window_resize.?(ctx.native, @intCast(pixel_width), @intCast(pixel_height), 0, 0); + if (!w.want_visible and w.visible) { + if (w.egl) |ctx| Egl.resetSurface(ctx); + c.wl_surface_attach(w.surface, null, 0, 0); + c.wl_surface_commit(w.surface); + w.configured = false; + } + w.visible = w.want_visible; + w.metrics = .{ .struct_size = @sizeOf(n.uhdk_window_metrics), .framebuffer_width = pixel_width, .framebuffer_height = pixel_height, .logical_size = .{ .width = @floatFromInt(width), .height = @floatFromInt(height) }, .scale = @as(f32, @floatFromInt(w.scale)) / 120, .state = w.state, .focused = @intFromBool(w.focused), .visible = @intFromBool(w.visible) }; + const metrics = w.metrics; + w.dirty = false; + w.mutex.unlock(w.owner.io); + n.z_metrics(w.handle, &metrics); + emit(w, n.UHDK_EVENT_WINDOW_REDRAW); + if (w.request != 0) completeCreation(w, abi.OK); +} + +fn createWindow(b: *Backend, command: *const n.z_command, id: u64) u32 { + const desc = command.window; + if (desc.flags & n.UHDK_WINDOW_DECORATED != 0 and b.decorations == null) return n.UHDK_STATUS_UNSUPPORTED; + const w = b.app.gpa.create(Window) catch return abi.OOM; + w.* = .{ .owner = b.app, .handle = @ptrCast(command.target), .width = @intFromFloat(@max(1, desc.content_size.width)), .height = @intFromFloat(@max(1, desc.content_size.height)), .min_width = @intFromFloat(desc.min_size.width), .min_height = @intFromFloat(desc.min_size.height), .max_width = @intFromFloat(desc.max_size.width), .max_height = @intFromFloat(desc.max_size.height), .flags = desc.flags }; + w.next = b.windows; + b.windows = w; + const status = setupWindow(b, w, desc); + if (status != abi.OK) { + destroyWindow(w); + return status; + } + n.z_object_set_native(command.target, w); + w.request = id; + w.deadline = runtime.now(b.app.io) + 5 * std.time.ns_per_s; + c.wl_surface_commit(w.surface); // Empty initial commit; wait for configure. + return n.Z_DEFERRED; +} +fn setupWindow(b: *Backend, w: *Window, desc: n.uhdk_window_desc) u32 { + const title = b.app.gpa.dupeZ(u8, abi.textBytes(desc.title)) catch return abi.OOM; + defer b.app.gpa.free(title); + const app_id = b.app.gpa.dupeZ(u8, b.app.application_id) catch return abi.OOM; + defer b.app.gpa.free(app_id); + w.surface = c.wl_compositor_create_surface(b.compositor) orelse return abi.OOM; + if (c.wl_surface_add_listener(w.surface, &wl_surface_listener, w) != 0) return abi.OOM; + w.xdg = c.xdg_wm_base_get_xdg_surface(b.shell, w.surface) orelse return abi.OOM; + if (c.xdg_surface_add_listener(w.xdg, &surface_listener, w) != 0) return abi.OOM; + w.top = c.xdg_surface_get_toplevel(w.xdg) orelse return abi.OOM; + if (c.xdg_toplevel_add_listener(w.top, &top_listener, w) != 0) return abi.OOM; + c.xdg_toplevel_set_title(w.top, title); + c.xdg_toplevel_set_app_id(w.top, app_id); + if (desc.parent) |parent| { + const p: *Window = @ptrCast(@alignCast(n.z_object_native(parent) orelse return abi.UNAVAILABLE)); + c.xdg_toplevel_set_parent(w.top, p.top); + } + if (w.flags & n.UHDK_WINDOW_RESIZABLE == 0) { + w.min_width = w.width; + w.min_height = w.height; + w.max_width = w.width; + w.max_height = w.height; + } + if (w.flags & n.UHDK_WINDOW_RESIZABLE != 0) { + w.width = std.math.clamp(w.width, @max(1, w.min_width), if (w.max_width == 0) 65536 else w.max_width); + w.height = std.math.clamp(w.height, @max(1, w.min_height), if (w.max_height == 0) 65536 else w.max_height); + } + setLimits(w); + if (w.flags & n.UHDK_WINDOW_DECORATED != 0) { + w.decoration = c.zxdg_decoration_manager_v1_get_toplevel_decoration(b.decorations, w.top) orelse return abi.OOM; + if (c.zxdg_toplevel_decoration_v1_add_listener(w.decoration, &decoration_listener, w) != 0) return abi.OOM; + c.zxdg_toplevel_decoration_v1_set_mode(w.decoration, c.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); + } + if (b.fractional != null and b.viewporter != null) { + w.viewport = c.wp_viewporter_get_viewport(b.viewporter, w.surface) orelse return abi.OOM; + w.fractional = c.wp_fractional_scale_manager_v1_get_fractional_scale(b.fractional, w.surface) orelse return abi.OOM; + if (c.wp_fractional_scale_v1_add_listener(w.fractional, &fraction_listener, w) != 0) return abi.OOM; + } + return abi.OK; +} +fn setLimits(w: *Window) void { + c.xdg_toplevel_set_min_size(w.top, w.min_width, w.min_height); + c.xdg_toplevel_set_max_size(w.top, w.max_width, w.max_height); +} +fn destroyWindow(w: *Window) void { + const b = instance.?; + std.debug.assert(w.egl == null); + var link = &b.windows; + while (link.*) |item| { + if (item == w) { + link.* = item.next; + break; + } + link = &item.next; + } + if (w.fractional != null) c.wp_fractional_scale_v1_destroy(w.fractional); + if (w.viewport != null) c.wp_viewport_destroy(w.viewport); + if (w.decoration != null) c.zxdg_toplevel_decoration_v1_destroy(w.decoration); + if (w.top != null) c.xdg_toplevel_destroy(w.top); + if (w.xdg != null) c.xdg_surface_destroy(w.xdg); + if (w.surface != null) c.wl_surface_destroy(w.surface); + w.owner.gpa.destroy(w); +} +export fn z_native_execute(_: ?*n.uhdk_app, raw: [*c]const n.z_command, id: u64, result: [*c]n.z_request_result) u32 { + const b = instance.?; + const cmd: *const n.z_command = &raw.*; + if (cmd.op == n.Z_DESTROY) { + if (n.z_object_native(cmd.target)) |ptr| { + z_native_destroy(n.z_object_kind(cmd.target), ptr); + n.z_object_set_native(cmd.target, null); + } + return abi.OK; + } + if (b.failed.load(.acquire)) return abi.UNAVAILABLE; + if (cmd.op == n.Z_WINDOW) return createWindow(b, cmd, id); + const ptr = if (cmd.target != null) n.z_object_native(cmd.target) else null; + const w: *Window = if (ptr) |p| @ptrCast(@alignCast(p)) else return n.UHDK_STATUS_UNSUPPORTED; + switch (cmd.op) { + n.Z_TITLE => { + const title = b.app.gpa.dupeZ(u8, abi.textBytes(cmd.text)) catch return abi.OOM; + defer b.app.gpa.free(title); + c.xdg_toplevel_set_title(w.top, title); + }, + n.Z_SIZE => { + if (w.state != n.UHDK_WINDOW_NORMAL) return abi.UNAVAILABLE; + w.width = @intFromFloat(@max(1, cmd.size.width)); + w.height = @intFromFloat(@max(1, cmd.size.height)); + if (w.flags & n.UHDK_WINDOW_RESIZABLE != 0) { + w.width = std.math.clamp(w.width, @max(1, w.min_width), if (w.max_width == 0) 65536 else w.max_width); + w.height = std.math.clamp(w.height, @max(1, w.min_height), if (w.max_height == 0) 65536 else w.max_height); + } + if (w.flags & n.UHDK_WINDOW_RESIZABLE == 0) { + w.min_width = w.width; + w.max_width = w.width; + w.min_height = w.height; + w.max_height = w.height; + setLimits(w); + } + w.dirty = true; + }, + n.Z_STATE => switch (cmd.number) { + n.UHDK_WINDOW_MINIMIZED => c.xdg_toplevel_set_minimized(w.top), + n.UHDK_WINDOW_MAXIMIZED => { + c.xdg_toplevel_unset_fullscreen(w.top); + c.xdg_toplevel_set_maximized(w.top); + }, + n.UHDK_WINDOW_FULLSCREEN => c.xdg_toplevel_set_fullscreen(w.top, null), + else => { + c.xdg_toplevel_unset_fullscreen(w.top); + c.xdg_toplevel_unset_maximized(w.top); + }, + }, + n.Z_VISIBLE => { + w.want_visible = cmd.number != 0; + if (w.want_visible and !w.configured) c.wl_surface_commit(w.surface); + w.dirty = true; + }, + n.Z_FOCUS => return n.UHDK_STATUS_UNSUPPORTED, + n.Z_REDRAW => emit(w, n.UHDK_EVENT_WINDOW_REDRAW), + n.Z_GL => return Egl.create(b, w, cmd.other, result), + else => return n.UHDK_STATUS_UNSUPPORTED, + } + return abi.OK; +} +export fn z_native_cancel(id: u64) void { + const b = instance orelse return; + var cursor = b.windows; + while (cursor) |w| : (cursor = w.next) { + if (w.request == id) { + completeCreation(w, abi.CANCELLED); + return; + } + } +} +export fn z_native_destroy(kind: u32, ptr: ?*anyopaque) void { + const p = ptr orelse return; + switch (kind) { + n.Z_KIND_WINDOW => { + const w: *Window = @ptrCast(@alignCast(p)); + if (w.request != 0) completeCreation(w, abi.CANCELLED) else destroyWindow(w); + }, + n.Z_KIND_GL => Egl.destroy(@ptrCast(@alignCast(p))), + else => {}, + } +} +export fn z_native_handles(ptr: ?*anyopaque, out: [*c]n.uhdk_native_window) void { + const w: *Window = @ptrCast(@alignCast(ptr.?)); + out.* = .{ .struct_size = @sizeOf(n.uhdk_native_window), .backend = n.UHDK_BACKEND_WAYLAND, .primary = instance.?.display, .secondary = w.surface, .value = 0 }; +} +export fn z_native_gl(ptr: ?*anyopaque, op: u32, out: ?*anyopaque) u32 { + return Egl.call(ptr, op, out); +} +export fn z_native_gl_proc(ptr: ?*anyopaque, name: n.uhdk_text, out: [*c]n.uhdk_gl_proc) u32 { + return Egl.proc(ptr, name, out); +} +export fn z_native_file_init(file: ?*n.uhdk_file, path: n.uhdk_bytes, uri: n.uhdk_text, grant: ?*anyopaque) u32 { + return @import("Files.zig").init(file, path, uri, grant); +} diff --git a/src/linux/nix/pkgs.nix b/src/linux/nix/pkgs.nix new file mode 100644 index 0000000..d1259ce --- /dev/null +++ b/src/linux/nix/pkgs.nix @@ -0,0 +1,6 @@ +# Maintainer tools and a runtime library search path for NixOS development. +# Normal Zig builds do not import this file. +import (builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/d6df3513510aa548c83868fd22bfddd0a8c0a0d4.tar.gz"; + sha256 = "0k16bbpgr4wnqwddzfb4j0vfaz6hwf48m8wy9qiaf8xw73s6r5mq"; +}) { } diff --git a/src/linux/redirects.h b/src/linux/redirects.h new file mode 100644 index 0000000..441d58a --- /dev/null +++ b/src/linux/redirects.h @@ -0,0 +1,33 @@ +/* Keep in sync with functions.inc. */ +#define wl_display_connect z_wl_display_connect +#define wl_display_disconnect z_wl_display_disconnect +#define wl_display_get_fd z_wl_display_get_fd +#define wl_display_dispatch_pending z_wl_display_dispatch_pending +#define wl_display_prepare_read z_wl_display_prepare_read +#define wl_display_read_events z_wl_display_read_events +#define wl_display_cancel_read z_wl_display_cancel_read +#define wl_display_flush z_wl_display_flush +#define wl_proxy_marshal_flags z_wl_proxy_marshal_flags +#define wl_proxy_add_listener z_wl_proxy_add_listener +#define wl_proxy_destroy z_wl_proxy_destroy +#define wl_proxy_get_version z_wl_proxy_get_version +#define wl_proxy_set_user_data z_wl_proxy_set_user_data +#define wl_proxy_get_user_data z_wl_proxy_get_user_data +#define wl_egl_window_create z_wl_egl_window_create +#define wl_egl_window_destroy z_wl_egl_window_destroy +#define wl_egl_window_resize z_wl_egl_window_resize +#define eglGetPlatformDisplay z_eglGetPlatformDisplay +#define eglInitialize z_eglInitialize +#define eglTerminate z_eglTerminate +#define eglBindAPI z_eglBindAPI +#define eglChooseConfig z_eglChooseConfig +#define eglCreateContext z_eglCreateContext +#define eglDestroyContext z_eglDestroyContext +#define eglCreateWindowSurface z_eglCreateWindowSurface +#define eglDestroySurface z_eglDestroySurface +#define eglMakeCurrent z_eglMakeCurrent +#define eglGetCurrentContext z_eglGetCurrentContext +#define eglSwapBuffers z_eglSwapBuffers +#define eglSwapInterval z_eglSwapInterval +#define eglGetProcAddress z_eglGetProcAddress +#define eglReleaseThread z_eglReleaseThread diff --git a/src/linux/tests/disconnect.py b/src/linux/tests/disconnect.py new file mode 100644 index 0000000..1f33b5d --- /dev/null +++ b/src/linux/tests/disconnect.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Drop only a test client's proxied Wayland connection after startup.""" +import array +import os +from pathlib import Path +import select +import socket +import subprocess +import sys +import threading + +binary = str(Path(sys.argv[1]).resolve()) +display = os.environ.get('WAYLAND_DISPLAY', 'wayland-0') +if not os.path.isabs(display): + display = os.path.join(os.environ['XDG_RUNTIME_DIR'], display) +upstream = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +upstream.connect(display) +client, proxy = socket.socketpair() + + +def relay(source, destination): + try: + while True: + data, ancillary, flags, _ = source.recvmsg(65536, socket.CMSG_SPACE(256 * 4)) + if not data: + break + assert not flags & socket.MSG_CTRUNC + descriptors = [] + for level, kind, value in ancillary: + if level == socket.SOL_SOCKET and kind == socket.SCM_RIGHTS: + fds = array.array('i') + fds.frombytes(value) + descriptors.extend(fds) + try: + sent = destination.sendmsg([data], ancillary) + if sent < len(data): + destination.sendall(data[sent:]) + finally: + for fd in descriptors: + os.close(fd) + except OSError: + pass + + +threads = [threading.Thread(target=relay, args=pair, daemon=True) + for pair in [(proxy, upstream), (upstream, proxy)]] +for thread in threads: + thread.start() +process = subprocess.Popen([binary, '--disconnect'], + env=dict(os.environ, WAYLAND_SOCKET=str(client.fileno())), + pass_fds=(client.fileno(),), stdout=subprocess.PIPE, text=True) +client.close() +try: + assert select.select([process.stdout], [], [], 15)[0], 'Startup timed out' + assert process.stdout.readline().strip() == 'connected' + for sock in (proxy, upstream): + sock.shutdown(socket.SHUT_RDWR) + assert process.wait(timeout=10) == 0 + print('Runtime display disconnect: PASS') +finally: + if process.poll() is None: + process.kill() + process.wait() + for sock in (proxy, upstream): + sock.close() + for thread in threads: + thread.join(timeout=1) diff --git a/src/linux/tests/native.c b/src/linux/tests/native.c new file mode 100644 index 0000000..23dcf58 --- /dev/null +++ b/src/linux/tests/native.c @@ -0,0 +1,246 @@ +#define UHDK_ENABLE_OPENGL +#define UHDK_ENABLE_NATIVE +#include "uhdk.h" +#include +#include +#include +#include + +#define OK(expression) \ + do { \ + uhdk_status status = (expression); \ + if (status != UHDK_STATUS_OK) { \ + fprintf(stderr, "%s:%d: %s returned %u\n", __FILE__, __LINE__, #expression, status); \ + abort(); \ + } \ + } while (0) + +static uhdk_text text(const char *s) { + return (uhdk_text){s, strlen(s)}; +} +static int entered; + +static uhdk_request_result wait_request(uhdk_app *app, uint64_t id) { + uint64_t deadline = uhdk_app_get_time_ns(app) + 10000000000ull; + while (uhdk_app_get_time_ns(app) < deadline) { + uhdk_event_list events; + OK(uhdk_app_poll_events(app, 100000000ull, &events)); + for (uint64_t i = 0; i < events.len; ++i) { + const uhdk_event *event = &events.ptr[i]; + if (event->type == UHDK_EVENT_REQUEST_DONE) { + const uhdk_request_result *result = event->data; + assert(result->id == id); + return *result; + } + } + } + fputs("Timed out awaiting request\n", stderr); + abort(); +} + +static uhdk_window_metrics metrics(uhdk_window *w) { + uhdk_window_metrics m = {0}; + m.struct_size = sizeof(m); + OK(uhdk_window_get_metrics(w, &m)); + return m; +} + +static void wait_metrics(uhdk_app *app, uhdk_window *w, int visible, int width) { + uint64_t deadline = uhdk_app_get_time_ns(app) + 10000000000ull; + while (uhdk_app_get_time_ns(app) < deadline) { + uhdk_window_metrics m = metrics(w); + if (m.visible == visible && m.logical_size.width == width) + return; + uhdk_event_list events; + OK(uhdk_app_poll_events(app, 100000000ull, &events)); + } + fputs("Timed out awaiting metrics\n", stderr); + abort(); +} + +static uhdk_window_desc descriptor(void) { + uhdk_window_desc desc = {0}; + desc.struct_size = sizeof(desc); + desc.title = text("UHDK Linux lifecycle test"); + desc.flags = UHDK_WINDOW_OPENGL | UHDK_WINDOW_DECORATED | UHDK_WINDOW_RESIZABLE; + desc.content_size = (uhdk_size){320, 240}; + return desc; +} + +static uhdk_window *create_window(uhdk_app *app) { + uhdk_window_desc desc = descriptor(); + uint64_t id; + OK(uhdk_window_create_async(app, &desc, &id)); + uhdk_request_result result = wait_request(app, id); + OK(result.status); + return ((const uhdk_window_result *)result.data)->window; +} + +static void test_main(uhdk_app *app, void *user) { + ++entered; + assert(uhdk_app_get_backend(app) == UHDK_BACKEND_WAYLAND); + assert(!uhdk_app_has_capability(app, UHDK_CAP_APP_MENU)); + assert(!uhdk_app_has_capability(app, UHDK_CAP_FILE_DIALOGS)); + uhdk_event_list ready; + OK(uhdk_app_poll_events(app, 0, &ready)); + assert(ready.len > 0 && ready.ptr[0].type == UHDK_EVENT_READY); + + // Cancellation must complete exactly once even before the first configure. + uint64_t id; + uhdk_window_desc desc = descriptor(); + OK(uhdk_window_create_async(app, &desc, &id)); + OK(uhdk_request_cancel(app, id)); + uhdk_request_result cancelled = wait_request(app, id); + assert(cancelled.status == UHDK_STATUS_CANCELLED || cancelled.status == UHDK_STATUS_OK); + if (cancelled.status == UHDK_STATUS_OK) + uhdk_window_destroy(((const uhdk_window_result *)cancelled.data)->window); + + // Destroying a parent must also complete a child still awaiting configure. + uhdk_window *parent = create_window(app); + desc.parent = parent; + OK(uhdk_window_create_async(app, &desc, &id)); + uhdk_window_destroy(parent); + uhdk_request_result child = wait_request(app, id); + assert(child.status == UHDK_STATUS_CANCELLED || child.status == UHDK_STATUS_OK); + desc.parent = NULL; + + uhdk_window *w = create_window(app); + assert(metrics(w).visible == 0); + uhdk_native_window native = {0}; + native.struct_size = sizeof(native); + OK(uhdk_window_get_native(w, &native)); + assert(native.backend == UHDK_BACKEND_WAYLAND && native.primary && native.secondary); + + const uint8_t path[] = "/tmp/uhdk file-%\xff"; + uhdk_file *file; + OK(uhdk_file_create_from_native_path(app, (uhdk_bytes){path, sizeof(path) - 1}, &file)); + uint8_t uri[256]; + uint64_t len; + OK(uhdk_file_copy_representation(file, UHDK_FILE_URI, uri, sizeof(uri), &len)); + assert(len == strlen("file:///tmp/uhdk%20file-%25%FF")); + assert(memcmp(uri, "file:///tmp/uhdk%20file-%25%FF", len) == 0); + uhdk_file_release(file); + + if (!user) { + assert(uhdk_app_has_capability(app, UHDK_CAP_OPENGL)); + OK(uhdk_gl_context_create_async(w, &id)); + uhdk_request_result gl_result = wait_request(app, id); + OK(gl_result.status); + uhdk_gl_context *gl = ((const uhdk_gl_context_result *)gl_result.data)->gl_context; + OK(uhdk_gl_context_begin_frame(gl)); + assert(uhdk_gl_context_begin_frame(gl) == UHDK_STATUS_BUSY); + uint32_t major, minor; + OK(uhdk_gl_context_get_version(gl, &major, &minor)); + assert(major > 3 || (major == 3 && minor >= 3)); + fprintf(stderr, "Linux context: OpenGL %u.%u\n", major, minor); + OK(uhdk_gl_clear_current(app)); + assert(uhdk_gl_context_get_version(gl, &major, &minor) == UHDK_STATUS_BUSY); + OK(uhdk_gl_context_make_current(gl)); + uhdk_gl_proc address; + OK(uhdk_gl_context_get_proc(gl, text("glClearColor"), &address)); + PFNGLCLEARCOLORPROC clear_color = (PFNGLCLEARCOLORPROC)address; + OK(uhdk_gl_context_get_proc(gl, text("glClear"), &address)); + PFNGLCLEARPROC clear = (PFNGLCLEARPROC)address; + OK(uhdk_gl_context_get_proc(gl, text("glReadPixels"), &address)); + PFNGLREADPIXELSPROC read_pixels = (PFNGLREADPIXELSPROC)address; + OK(uhdk_gl_context_set_swap_interval(gl, 1)); + assert(uhdk_gl_context_swap_buffers(gl) == UHDK_STATUS_UNAVAILABLE); + OK(uhdk_gl_context_end_frame(gl)); + + for (int pass = 0; pass < 3; ++pass) { + int width = 320 + pass * 20; + OK(uhdk_window_set_size_async(w, (uhdk_size){width, 240}, &id)); + OK(wait_request(app, id).status); + OK(uhdk_window_set_visible_async(w, 1, &id)); + OK(wait_request(app, id).status); + wait_metrics(app, w, 1, width); + for (int frame = 0; frame < 4; ++frame) { + OK(uhdk_gl_context_begin_frame(gl)); + clear_color(0.25f, 0.5f, 0.75f, 1.0f); + clear(GL_COLOR_BUFFER_BIT); + unsigned char pixel[4] = {0}; + read_pixels(10, 10, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + assert( + pixel[0] >= 63 && pixel[0] <= 65 && pixel[1] >= 127 && pixel[1] <= 129 && + pixel[2] >= 190 && pixel[2] <= 192 + ); + OK(uhdk_gl_context_swap_buffers(gl)); + OK(uhdk_gl_context_end_frame(gl)); + } + OK(uhdk_window_set_visible_async(w, 0, &id)); + OK(wait_request(app, id).status); + wait_metrics(app, w, 0, width); + } + uhdk_gl_context_destroy(gl); + // Destroying a context releases its reservation on the window. + OK(uhdk_gl_context_create_async(w, &id)); + gl_result = wait_request(app, id); + OK(gl_result.status); + uhdk_gl_context_destroy(((const uhdk_gl_context_result *)gl_result.data)->gl_context); + } + uhdk_window_destroy(w); + // Returning with a queued creation must cancel/drain it before native teardown. + OK(uhdk_window_create_async(app, &desc, &id)); +} + +static void disconnect_main(uhdk_app *app, void *user) { + (void)user; + ++entered; + puts("connected"); + fflush(stdout); + uint64_t deadline = uhdk_app_get_time_ns(app) + 5000000000ull; + int quit_seen = 0; + while (uhdk_app_get_time_ns(app) < deadline) { + uhdk_event_list events; + OK(uhdk_app_poll_events(app, 100000000ull, &events)); + for (uint64_t i = 0; i < events.len; ++i) { + if (events.ptr[i].type == UHDK_EVENT_QUIT_REQUESTED) + quit_seen = 1; + if (events.ptr[i].type == UHDK_EVENT_STOPPED) { + assert(quit_seen); + return; + } + } + } + abort(); +} + +static void should_not_run(uhdk_app *app, void *user) { + (void)app; + (void)user; + abort(); +} + +int main(int argc, char **argv) { + int failure = argc > 1 && strcmp(argv[1], "--startup-failure") == 0; + int windows_only = argc > 1 && strcmp(argv[1], "--windows-only") == 0; + int disconnect = argc > 1 && strcmp(argv[1], "--disconnect") == 0; + int passes = disconnect ? 1 : 2; + for (int pass = 0; pass < passes; ++pass) { + uhdk_app_desc desc = {0}; + desc.struct_size = sizeof(desc); + desc.abi_version = UHDK_ABI_VERSION; + desc.display_name = text("UHDK Linux test"); + char app_id[] = "org.uhdk.LifecycleTest"; + desc.application_id = text(app_id); + uhdk_app *app = NULL; + desc.preferred_backend = UHDK_BACKEND_X11; + assert(uhdk_app_create(&desc, &app) == UHDK_STATUS_UNSUPPORTED); + desc.preferred_backend = UHDK_BACKEND_AUTO; + OK(uhdk_app_create(&desc, &app)); + memset(app_id, 'x', sizeof(app_id) - 1); // Descriptor bytes are copied. + assert(uhdk_app_get_backend(app) == UHDK_BACKEND_AUTO); + assert(!uhdk_app_has_capability(app, UHDK_CAP_OPENGL)); + if (failure) { + assert(uhdk_app_run(app, should_not_run, NULL) != UHDK_STATUS_OK); + assert(uhdk_app_get_backend(app) == UHDK_BACKEND_AUTO); + } else if (disconnect) { + OK(uhdk_app_run(app, disconnect_main, NULL)); + } else + OK(uhdk_app_run(app, test_main, windows_only ? &windows_only : NULL)); + uhdk_app_destroy(app); + } + assert(entered == (failure ? 0 : passes)); + fputs("Linux lifecycle: PASS\n", stderr); + return 0; +} diff --git a/src/linux/tests/startup.py b/src/linux/tests/startup.py new file mode 100644 index 0000000..acce75d --- /dev/null +++ b/src/linux/tests/startup.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Exercise failed Wayland startup without opening windows or changing a desktop.""" +import os +from pathlib import Path +import socket +import subprocess +import sys +import tempfile +import time + +binary = str(Path(sys.argv[1]).resolve()) +with tempfile.TemporaryDirectory(prefix='uhdk-startup-') as directory: + env = dict(os.environ, WAYLAND_DISPLAY=directory + '/missing', XDG_RUNTIME_DIR=directory) + env.pop('WAYLAND_SOCKET', None) + subprocess.run([binary, '--startup-failure'], env=env, check=True, timeout=15) + for scenario in ('disconnected', 'unresponsive'): + client, server = socket.socketpair() + env['WAYLAND_SOCKET'] = str(client.fileno()) + if scenario == 'disconnected': + server.close() + start = time.monotonic() + subprocess.run([binary, '--startup-failure'], env=env, + pass_fds=(client.fileno(),), check=True, timeout=15) + elapsed = time.monotonic() - start + client.close() + server.close() + if scenario == 'unresponsive': + assert 4 <= elapsed < 12, elapsed + print(f'{scenario}: PASS ({elapsed:.2f}s)') diff --git a/src/linux/tools/audit-elf.py b/src/linux/tools/audit-elf.py new file mode 100644 index 0000000..f1db68c --- /dev/null +++ b/src/linux/tools/audit-elf.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Check a portable Linux executable's loader, imports, and glibc symbol floor.""" +import argparse +from pathlib import Path +import re +import subprocess + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('binary', type=Path) +parser.add_argument('--glibc', default='2.35', help='maximum allowed glibc symbol version') +args = parser.parse_args() + + +def readelf(*options): + return subprocess.check_output(['readelf', '--wide', *options, str(args.binary)], text=True) + + +program = readelf('-l') +dynamic = readelf('-d') +versions = readelf('--version-info') +symbols = readelf('--dyn-syms') +assert '/nix/store/' not in program + dynamic, 'Nix path in runtime loader or dynamic section' +assert 'Requesting program interpreter: /lib' in program, 'Expected a conventional Linux loader' +needed = re.findall(r'\(NEEDED\).*?\[(.*?)\]', dynamic) +assert needed, 'Expected dynamic libc for runtime desktop library loading' +for name in needed: + assert not re.search(r'wayland|EGL|libGL|xkbcommon|dbus|decor', name), name +for line in symbols.splitlines(): + if ' UND ' in line: + assert not re.search(r'\b(?:wl_|xdg_|egl|xkb_|dbus_)', line), line +floor = tuple(map(int, args.glibc.split('.'))) +required = [tuple(map(int, v.split('.'))) for v in re.findall(r'Name: GLIBC_([0-9.]+)', versions)] +assert required and max(required) <= floor, required +print(f'{args.binary}: PASS; imports={needed}; maximum glibc symbol={".".join(map(str, max(required)))}') diff --git a/src/linux/vendor/README.md b/src/linux/vendor/README.md index 28f2222..97e90ce 100644 --- a/src/linux/vendor/README.md +++ b/src/linux/vendor/README.md @@ -10,8 +10,8 @@ per-file digests, and the scanner version. - wayland-protocols 1.45: xdg-shell, xdg-decoration, viewporter, fractional-scale. License in `wayland-protocols/COPYING` and each XML's copyright element. - Khronos EGL-Registry revision `5961a7fe64cf8a126890ced6f13d69e0a1e1b83e`: - EGL and platform ABI headers. Apache-2.0 or MIT notices are embedded in the - headers, including the full MIT grant where applicable. + EGL and platform ABI headers. Apache-2.0 text is in + `khronos/LICENSE-2.0.txt`; copyright/SPDX and full MIT notices remain in the headers. Upstream files are unmodified. `wayland-version.h` is produced by substituting the release version into the upstream template. The scanner generates client diff --git a/src/linux/vendor/khronos/LICENSE-2.0.txt b/src/linux/vendor/khronos/LICENSE-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/src/linux/vendor/khronos/LICENSE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/linux/vendor/manifest.json b/src/linux/vendor/manifest.json index 3057126..00319ff 100644 --- a/src/linux/vendor/manifest.json +++ b/src/linux/vendor/manifest.json @@ -46,6 +46,11 @@ "url": "https://raw.githubusercontent.com/KhronosGroup/EGL-Registry/5961a7fe64cf8a126890ced6f13d69e0a1e1b83e/api/KHR/khrplatform.h", "sha256": "7b1e01aaa7ad8f6fc34b5c7bdf79ebf5189bb09e2c4d2e79fc5d350623d11e83", "destination": "vendor/khronos/KHR/khrplatform.h" + }, + { + "url": "https://www.apache.org/licenses/LICENSE-2.0.txt", + "sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30", + "destination": "vendor/khronos/LICENSE-2.0.txt" } ], "files": { @@ -64,7 +69,8 @@ "vendor/khronos/EGL/eglext.h": "a5f574a0074001400c6c50232235ee00e9a67b2168bfd1c364ba0f6f52e5a75c", "vendor/khronos/EGL/eglplatform.h": "25b5391655effcf363a38fa00c3154f356a3eab3d35dc067847875513cf1e441", "vendor/khronos/KHR/khrplatform.h": "7b1e01aaa7ad8f6fc34b5c7bdf79ebf5189bb09e2c4d2e79fc5d350623d11e83", - "vendor/wayland/wayland-client.h": "e9dd1fb014aa6ad848cb7d241cc223b1a70fc0b17fed5997262024e4c05532f7" + "vendor/wayland/wayland-client.h": "e9dd1fb014aa6ad848cb7d241cc223b1a70fc0b17fed5997262024e4c05532f7", + "vendor/khronos/LICENSE-2.0.txt": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" }, "generated": { "protocols/generated/fractional-scale-v1.c": "fb40e729ee05acafe996d1261367424b47722be02dbee65c90b3f4972f259dca", diff --git a/src/root.zig b/src/root.zig index c59a0a1..61d3726 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,4 +1,4 @@ -//! Uhdk C ABI. Runtime state lives in Application; native code owns AppKit objects. +//! Uhdk C ABI. Runtime state lives in Application; platform backends own native resources. const std = @import("std"); const c = @import("native"); @@ -11,6 +11,7 @@ comptime { checkExports(@import("api/menus.zig")); checkExports(@import("api/desktop.zig")); _ = @import("callbacks.zig"); + if (@import("builtin").os.tag == .linux) _ = @import("linux/native.zig"); } test { @@ -18,6 +19,7 @@ test { std.testing.refAllDecls(@import("FileOperation.zig")); _ = @import("core_tests.zig"); _ = @import("arenas.zig"); + if (@import("builtin").os.tag == .linux) _ = @import("linux/Files.zig"); } fn checkExports(comptime module: type) void { diff --git a/src/runtime.zig b/src/runtime.zig index 78f8597..5b94b5e 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -296,7 +296,7 @@ pub fn applicationMain(application: *Application) void { application.worker_id = std.Thread.getCurrentId(); application.unlock(); application.entry.?(toAppHandle(application), application.user); - if (@import("builtin").os.tag == .windows) _ = c.z_native_gl(null, 4, null); + if (@import("builtin").os.tag == .windows or @import("builtin").os.tag == .linux) _ = c.z_native_gl(null, 4, null); application.lock(); application.stopping = true; application.worker_done = true; @@ -369,7 +369,7 @@ pub fn destroyObject(object: *Resource) void { std.debug.assert(checkThread(object.app) == abi.OK); if (object.payload == .gl) { std.debug.assert(!object.payload.gl.bracket); - if (@import("builtin").os.tag == .windows and object.native != null) + if ((@import("builtin").os.tag == .windows or @import("builtin").os.tag == .linux) and object.native != null) _ = c.z_native_gl(object.native, 8, null); } if (object.payload == .window) if (object.payload.window.context) |ctx| { @@ -657,6 +657,8 @@ pub fn tick(handle: ?*c.uhdk_app) void { continue; } } + // Mark before entering native code: a backend may complete inline. + command.native_deferred = true; const status = c.z_native_execute(handle, &command.data, command.id, &result); if (status != c.Z_DEFERRED and command.data.op == c.Z_DESTROY) { const resource = fromObjectHandle(command.data.target); @@ -679,15 +681,14 @@ pub fn tick(handle: ?*c.uhdk_app) void { if (application.io_active) |command| { if (fileStep(application, command)) application.io_active = null; } - // Deferred native operations (panels/popups) have separate cancellation hooks. + // Only operations dispatched to native code need native cancellation hooks. application.lock(); var cursor = application.requests; var ids: [64]u64 = undefined; var node: usize = 0; while (cursor) |request| : (cursor = request.next) { if ((request.cancelled or - application.stopping) and node < ids.len and (request.data.op == c.Z_DIALOG or - request.data.op == c.Z_POPUP)) + application.stopping) and node < ids.len and request.native_deferred) { ids[node] = request.id; node += 1; @@ -704,7 +705,7 @@ pub fn tick(handle: ?*c.uhdk_app) void { application.stopped_sent = true; } const done = application.worker_done and empty; - const again = application.queue_head != null or application.io_head != null or application.io_active != null; + const again = application.queue_head != null or application.io_head != null or application.io_active != null or node == ids.len; application.unlock(); if (done) c.z_native_stop() else if (again) c.z_native_again(); }