Implement Plasma Wayland window and EGL foundation

Add runtime-loaded Wayland/EGL, asynchronous xdg-shell windows, KWin server decorations, app identity, scaling metrics, frame synchronization, remapping and POSIX file URIs. Keep unsupported input and desktop-service capabilities false.

Add pinned optional Nix tooling, portable ELF auditing, and native lifecycle/startup/disconnect tests. Verified Plasma/Mesa GL 4.6 pixel checks, hide/show and cancellation lifetimes, 17 core tests, Windows compilation, and x86-64 glibc 2.35 release output without desktop link dependencies.
This commit is contained in:
Peterino2 2026-09-18 13:17:50 -07:00
parent afed52d567
commit 685985e9cd
28 changed files with 1843 additions and 18 deletions

View File

@ -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 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. 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 ## Build
```sh ```sh
zig build # zig-out/lib/libuhdk.a and zig-out/include/ 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 test # run core library tests on the native target
zig build -Dtarget=x86_64-windows-gnu zig build -Dtarget=x86_64-windows-gnu
zig build -Dtarget=x86_64-windows-gnu -Dshared=true zig build -Dtarget=x86_64-windows-gnu -Dshared=true

View File

@ -2,7 +2,7 @@ const std = @import("std");
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); 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 optimize = b.standardOptimizeOption(.{});
const shared = b.option(bool, "shared", "Build libuhdk as a shared library instead of static") orelse false; 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; 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"); const check_step = b.step("check", "Compile library and core tests without running them");
check_step.dependOn(&lib.step); check_step.dependOn(&lib.step);
check_step.dependOn(&unit.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); const unit_run = b.addRunArtifact(unit);
b.step("test", "Run core library tests").dependOn(&unit_run.step); b.step("test", "Run core library tests").dependOn(&unit_run.step);
const public_c = b.addTranslateC(.{ const public_c = b.addTranslateC(.{
@ -79,6 +80,8 @@ fn createLibraryNamed(b: *std.Build, target: std.Build.ResolvedTarget, optimize:
module.linkFramework("Cocoa", .{}); module.linkFramework("Cocoa", .{});
module.linkFramework("OpenGL", .{}); module.linkFramework("OpenGL", .{});
module.linkFramework("CoreServices", .{}); module.linkFramework("CoreServices", .{});
} else if (target.result.os.tag == .linux) {
@import("src/linux/build.zig").configure(b, module, target, optimize);
} else { } else {
module.addCSourceFile(.{ .file = b.path("src/windows/native.c"), .flags = &.{ "-std=c11", "-Wall", "-Wextra", "-Werror", "-DUNICODE", "-D_UNICODE" } }); 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, .{}); for ([_][]const u8{ "user32", "gdi32", "opengl32", "shell32", "ole32", "uuid", "imm32", "comctl32", "shlwapi", "advapi32" }) |name| module.linkSystemLibrary(name, .{});

View File

@ -4,5 +4,5 @@
.version = "0.1.0", .version = "0.1.0",
.minimum_zig_version = "0.16.0", .minimum_zig_version = "0.16.0",
.dependencies = .{}, .dependencies = .{},
.paths = .{ "build.zig", "build.zig.zon", "src", "include", "README.md" }, .paths = .{ "build.zig", "build.zig.zon", "src", "include", "README.md", "shell.nix" },
} }

View File

@ -2,7 +2,7 @@
#define UHDK_H_INCLUDED #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++). * Implementation: Zig. ABI: C. Header baseline: C99 (also usable from C++).
* No libc runtime objects or allocation contract. * 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 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. */ 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_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_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); 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 *parent; /* Optional; must outlive child. */
} uhdk_window_desc; } 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 { typedef struct uhdk_window_metrics {
uint32_t struct_size; uint32_t struct_size;
uint32_t framebuffer_width; 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_major,
uint32_t *out_minor 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); 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. /* Explicitly set 0 (unsynchronized) or 1 (vsync); other values are invalid.
* No initial interval is guaranteed. Unsupported control returns UNSUPPORTED; * No initial interval is guaranteed. Unsupported control returns UNSUPPORTED;

16
shell.nix Normal file
View File

@ -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
];
}

View File

@ -27,6 +27,7 @@ pending_tail: ?*Event = null,
published: ?*Event = null, published: ?*Event = null,
scratch: arenas.BumpArena, scratch: arenas.BumpArena,
name: []u8, name: []u8,
application_id: []const u8 = "",
epoch: u64, epoch: u64,
deadline: u64 = 0, deadline: u64 = 0,
next_id: u64 = 1, next_id: u64 = 1,

View File

@ -16,5 +16,6 @@ node: *Event,
id: u64 = 0, id: u64 = 0,
kind: u32 = c.UHDK_REQUEST_COMMAND, kind: u32 = c.UHDK_REQUEST_COMMAND,
cancelled: bool = false, cancelled: bool = false,
native_deferred: bool = false,
pins: std.ArrayList(*Resource) = .empty, pins: std.ArrayList(*Resource) = .empty,
file_operation: ?FileOperation = null, file_operation: ?FileOperation = null,

View File

@ -1,7 +1,11 @@
//! C ABI entry points for app. //! C ABI entry points for app.
const std = @import("std"); const std = @import("std");
const profile = @import("profile"); 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 c = @import("native");
const runtime = @import("../runtime.zig"); const runtime = @import("../runtime.zig");
const abi = @import("../abi.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); gpa.destroy(backend);
return abi.OOM; 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; application.io_backend = backend;
singleton = true; singleton = true;
out.* = runtime.toAppHandle(application); out.* = runtime.toAppHandle(application);
@ -47,9 +59,9 @@ pub export fn uhdk_app_run(
const application = runtime.fromAppHandle(handle); const application = runtime.fromAppHandle(handle);
if (application.has_run) return abi.UNAVAILABLE; if (application.has_run) return abi.UNAVAILABLE;
profile.thread("UHDK platform"); profile.thread("UHDK platform");
runtime.simpleEvent(application, c.UHDK_EVENT_READY);
const status = c.z_native_start(handle, abi.textSpan(application.name)); const status = c.z_native_start(handle, abi.textSpan(application.name));
if (status != abi.OK) return status; if (status != abi.OK) return status;
runtime.simpleEvent(application, c.UHDK_EVENT_READY);
application.entry = entry; application.entry = entry;
application.user = user; application.user = user;
application.running = true; 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; 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; 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) { 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, 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, else => 0,
@ -280,6 +294,7 @@ pub export fn uhdk_app_destroy(handle: ?*c.uhdk_app) void {
} }
const gpa = application.gpa; const gpa = application.gpa;
gpa.free(application.name); gpa.free(application.name);
gpa.free(application.application_id);
if (application.io_backend) |backend| { if (application.io_backend) |backend| {
backend.deinit(); backend.deinit();
gpa.destroy(backend); gpa.destroy(backend);

160
src/linux/Egl.zig Normal file
View File

@ -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;
}

66
src/linux/Files.zig Normal file
View File

@ -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);
}

126
src/linux/README.md Normal file
View File

@ -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.

34
src/linux/bindings.h Normal file
View File

@ -0,0 +1,34 @@
#ifndef UHDK_LINUX_BINDINGS_H
#define UHDK_LINUX_BINDINGS_H
#define _GNU_SOURCE
#include <errno.h>
#include <poll.h>
#include <sys/eventfd.h>
#include <sys/syscall.h>
#include <unistd.h>
#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

34
src/linux/build.zig Normal file
View File

@ -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);
}

View File

@ -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;

32
src/linux/functions.inc Normal file
View File

@ -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)

98
src/linux/libraries.c Normal file
View File

@ -0,0 +1,98 @@
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <string.h>
#include <sys/syscall.h>
#include <unistd.h>
#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);
}

558
src/linux/native.zig Normal file
View File

@ -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, &registry_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);
}

6
src/linux/nix/pkgs.nix Normal file
View File

@ -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";
}) { }

33
src/linux/redirects.h Normal file
View File

@ -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

View File

@ -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)

246
src/linux/tests/native.c Normal file
View File

@ -0,0 +1,246 @@
#define UHDK_ENABLE_OPENGL
#define UHDK_ENABLE_NATIVE
#include "uhdk.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}

View File

@ -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)')

View File

@ -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)))}')

View File

@ -10,8 +10,8 @@ per-file digests, and the scanner version.
- wayland-protocols 1.45: xdg-shell, xdg-decoration, viewporter, fractional-scale. - wayland-protocols 1.45: xdg-shell, xdg-decoration, viewporter, fractional-scale.
License in `wayland-protocols/COPYING` and each XML's copyright element. License in `wayland-protocols/COPYING` and each XML's copyright element.
- Khronos EGL-Registry revision `5961a7fe64cf8a126890ced6f13d69e0a1e1b83e`: - Khronos EGL-Registry revision `5961a7fe64cf8a126890ced6f13d69e0a1e1b83e`:
EGL and platform ABI headers. Apache-2.0 or MIT notices are embedded in the EGL and platform ABI headers. Apache-2.0 text is in
headers, including the full MIT grant where applicable. `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 Upstream files are unmodified. `wayland-version.h` is produced by substituting
the release version into the upstream template. The scanner generates client the release version into the upstream template. The scanner generates client

202
src/linux/vendor/khronos/LICENSE-2.0.txt vendored Normal file
View File

@ -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.

View File

@ -46,6 +46,11 @@
"url": "https://raw.githubusercontent.com/KhronosGroup/EGL-Registry/5961a7fe64cf8a126890ced6f13d69e0a1e1b83e/api/KHR/khrplatform.h", "url": "https://raw.githubusercontent.com/KhronosGroup/EGL-Registry/5961a7fe64cf8a126890ced6f13d69e0a1e1b83e/api/KHR/khrplatform.h",
"sha256": "7b1e01aaa7ad8f6fc34b5c7bdf79ebf5189bb09e2c4d2e79fc5d350623d11e83", "sha256": "7b1e01aaa7ad8f6fc34b5c7bdf79ebf5189bb09e2c4d2e79fc5d350623d11e83",
"destination": "vendor/khronos/KHR/khrplatform.h" "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": { "files": {
@ -64,7 +69,8 @@
"vendor/khronos/EGL/eglext.h": "a5f574a0074001400c6c50232235ee00e9a67b2168bfd1c364ba0f6f52e5a75c", "vendor/khronos/EGL/eglext.h": "a5f574a0074001400c6c50232235ee00e9a67b2168bfd1c364ba0f6f52e5a75c",
"vendor/khronos/EGL/eglplatform.h": "25b5391655effcf363a38fa00c3154f356a3eab3d35dc067847875513cf1e441", "vendor/khronos/EGL/eglplatform.h": "25b5391655effcf363a38fa00c3154f356a3eab3d35dc067847875513cf1e441",
"vendor/khronos/KHR/khrplatform.h": "7b1e01aaa7ad8f6fc34b5c7bdf79ebf5189bb09e2c4d2e79fc5d350623d11e83", "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": { "generated": {
"protocols/generated/fractional-scale-v1.c": "fb40e729ee05acafe996d1261367424b47722be02dbee65c90b3f4972f259dca", "protocols/generated/fractional-scale-v1.c": "fb40e729ee05acafe996d1261367424b47722be02dbee65c90b3f4972f259dca",

View File

@ -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 std = @import("std");
const c = @import("native"); const c = @import("native");
@ -11,6 +11,7 @@ comptime {
checkExports(@import("api/menus.zig")); checkExports(@import("api/menus.zig"));
checkExports(@import("api/desktop.zig")); checkExports(@import("api/desktop.zig"));
_ = @import("callbacks.zig"); _ = @import("callbacks.zig");
if (@import("builtin").os.tag == .linux) _ = @import("linux/native.zig");
} }
test { test {
@ -18,6 +19,7 @@ test {
std.testing.refAllDecls(@import("FileOperation.zig")); std.testing.refAllDecls(@import("FileOperation.zig"));
_ = @import("core_tests.zig"); _ = @import("core_tests.zig");
_ = @import("arenas.zig"); _ = @import("arenas.zig");
if (@import("builtin").os.tag == .linux) _ = @import("linux/Files.zig");
} }
fn checkExports(comptime module: type) void { fn checkExports(comptime module: type) void {

View File

@ -296,7 +296,7 @@ pub fn applicationMain(application: *Application) void {
application.worker_id = std.Thread.getCurrentId(); application.worker_id = std.Thread.getCurrentId();
application.unlock(); application.unlock();
application.entry.?(toAppHandle(application), application.user); 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.lock();
application.stopping = true; application.stopping = true;
application.worker_done = true; application.worker_done = true;
@ -369,7 +369,7 @@ pub fn destroyObject(object: *Resource) void {
std.debug.assert(checkThread(object.app) == abi.OK); std.debug.assert(checkThread(object.app) == abi.OK);
if (object.payload == .gl) { if (object.payload == .gl) {
std.debug.assert(!object.payload.gl.bracket); 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); _ = c.z_native_gl(object.native, 8, null);
} }
if (object.payload == .window) if (object.payload.window.context) |ctx| { if (object.payload == .window) if (object.payload.window.context) |ctx| {
@ -657,6 +657,8 @@ pub fn tick(handle: ?*c.uhdk_app) void {
continue; 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); const status = c.z_native_execute(handle, &command.data, command.id, &result);
if (status != c.Z_DEFERRED and command.data.op == c.Z_DESTROY) { if (status != c.Z_DEFERRED and command.data.op == c.Z_DESTROY) {
const resource = fromObjectHandle(command.data.target); const resource = fromObjectHandle(command.data.target);
@ -679,15 +681,14 @@ pub fn tick(handle: ?*c.uhdk_app) void {
if (application.io_active) |command| { if (application.io_active) |command| {
if (fileStep(application, command)) application.io_active = null; 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(); application.lock();
var cursor = application.requests; var cursor = application.requests;
var ids: [64]u64 = undefined; var ids: [64]u64 = undefined;
var node: usize = 0; var node: usize = 0;
while (cursor) |request| : (cursor = request.next) { while (cursor) |request| : (cursor = request.next) {
if ((request.cancelled or if ((request.cancelled or
application.stopping) and node < ids.len and (request.data.op == c.Z_DIALOG or application.stopping) and node < ids.len and request.native_deferred)
request.data.op == c.Z_POPUP))
{ {
ids[node] = request.id; ids[node] = request.id;
node += 1; node += 1;
@ -704,7 +705,7 @@ pub fn tick(handle: ?*c.uhdk_app) void {
application.stopped_sent = true; application.stopped_sent = true;
} }
const done = application.worker_done and empty; 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(); application.unlock();
if (done) c.z_native_stop() else if (again) c.z_native_again(); if (done) c.z_native_stop() else if (again) c.z_native_again();
} }