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:
parent
afed52d567
commit
685985e9cd
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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, .{});
|
||||||
|
|
|
||||||
|
|
@ -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" },
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,558 @@
|
||||||
|
//! Wayland platform owner. Protocol dispatch and native commands stay on the OS
|
||||||
|
//! thread; the application thread holds each window's lock only while rendering.
|
||||||
|
const std = @import("std");
|
||||||
|
const c = @import("linux_c");
|
||||||
|
const n = @import("native");
|
||||||
|
const abi = @import("../abi.zig");
|
||||||
|
const runtime = @import("../runtime.zig");
|
||||||
|
const Application = @import("../Application.zig");
|
||||||
|
const Egl = @import("Egl.zig");
|
||||||
|
|
||||||
|
pub const Window = struct {
|
||||||
|
next: ?*Window = null,
|
||||||
|
owner: *Application,
|
||||||
|
handle: ?*n.uhdk_window,
|
||||||
|
surface: ?*c.struct_wl_surface = null,
|
||||||
|
xdg: ?*c.struct_xdg_surface = null,
|
||||||
|
top: ?*c.struct_xdg_toplevel = null,
|
||||||
|
decoration: ?*c.struct_zxdg_toplevel_decoration_v1 = null,
|
||||||
|
viewport: ?*c.struct_wp_viewport = null,
|
||||||
|
fractional: ?*c.struct_wp_fractional_scale_v1 = null,
|
||||||
|
egl: ?*Egl.Context = null,
|
||||||
|
mutex: std.Io.Mutex = .init,
|
||||||
|
request: u64 = 0,
|
||||||
|
deadline: u64 = 0,
|
||||||
|
serial: ?u32 = null,
|
||||||
|
dirty: bool = true,
|
||||||
|
configured: bool = false,
|
||||||
|
want_visible: bool = false,
|
||||||
|
visible: bool = false,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
min_width: i32,
|
||||||
|
min_height: i32,
|
||||||
|
max_width: i32,
|
||||||
|
max_height: i32,
|
||||||
|
scale: u32 = 120,
|
||||||
|
focused: bool = false,
|
||||||
|
state: u32 = n.UHDK_WINDOW_NORMAL,
|
||||||
|
flags: u32,
|
||||||
|
metrics: n.uhdk_window_metrics = std.mem.zeroes(n.uhdk_window_metrics),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Backend = struct {
|
||||||
|
app: *Application,
|
||||||
|
display: ?*c.struct_wl_display = null,
|
||||||
|
registry: ?*c.struct_wl_registry = null,
|
||||||
|
compositor: ?*c.struct_wl_compositor = null,
|
||||||
|
shell: ?*c.struct_xdg_wm_base = null,
|
||||||
|
decorations: ?*c.struct_zxdg_decoration_manager_v1 = null,
|
||||||
|
viewporter: ?*c.struct_wp_viewporter = null,
|
||||||
|
fractional: ?*c.struct_wp_fractional_scale_manager_v1 = null,
|
||||||
|
windows: ?*Window = null,
|
||||||
|
egl_display: c.EGLDisplay = null,
|
||||||
|
egl_config: c.EGLConfig = null,
|
||||||
|
running: bool = true,
|
||||||
|
failed: std.atomic.Value(bool) = .init(false),
|
||||||
|
};
|
||||||
|
pub var instance: ?*Backend = null;
|
||||||
|
var wake_fd: std.atomic.Value(c_int) = .init(-1);
|
||||||
|
|
||||||
|
pub fn backend(handle: ?*n.uhdk_app) u32 {
|
||||||
|
if (handle == null) return n.UHDK_BACKEND_AUTO;
|
||||||
|
const app = runtime.fromAppHandle(handle);
|
||||||
|
return if (app.has_run) n.UHDK_BACKEND_WAYLAND else n.UHDK_BACKEND_AUTO;
|
||||||
|
}
|
||||||
|
pub fn capability(handle: ?*n.uhdk_app, cap: u32) n.uhdk_bool {
|
||||||
|
if (handle == null) return 0;
|
||||||
|
const b = instance orelse return 0;
|
||||||
|
if (b.app != runtime.fromAppHandle(handle) or !b.app.running or b.failed.load(.acquire)) return 0;
|
||||||
|
return @intFromBool(cap == n.UHDK_CAP_OPENGL and b.egl_display != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn z_native_is_main_thread() c_int {
|
||||||
|
return c.z_linux_is_main_thread();
|
||||||
|
}
|
||||||
|
export fn z_native_start(handle: ?*n.uhdk_app, _: n.uhdk_text) u32 {
|
||||||
|
const app = runtime.fromAppHandle(handle);
|
||||||
|
if (c.z_linux_libraries_open() == 0) return abi.UNAVAILABLE;
|
||||||
|
const b = app.gpa.create(Backend) catch {
|
||||||
|
c.z_linux_libraries_close();
|
||||||
|
return abi.OOM;
|
||||||
|
};
|
||||||
|
b.* = .{ .app = app };
|
||||||
|
instance = b;
|
||||||
|
const fd = c.eventfd(0, c.EFD_CLOEXEC | c.EFD_NONBLOCK);
|
||||||
|
if (fd < 0) {
|
||||||
|
z_native_finish();
|
||||||
|
return n.UHDK_STATUS_OS_ERROR;
|
||||||
|
}
|
||||||
|
wake_fd.store(fd, .release);
|
||||||
|
b.display = c.z_wl_display_connect.?(null);
|
||||||
|
if (b.display == null) {
|
||||||
|
z_native_finish();
|
||||||
|
return abi.UNAVAILABLE;
|
||||||
|
}
|
||||||
|
b.registry = c.wl_display_get_registry(b.display);
|
||||||
|
if (b.registry == null) {
|
||||||
|
z_native_finish();
|
||||||
|
return abi.OOM;
|
||||||
|
}
|
||||||
|
if (c.wl_registry_add_listener(b.registry, ®istry_listener, b) != 0 or !initialRegistry(b)) {
|
||||||
|
z_native_finish();
|
||||||
|
return n.UHDK_STATUS_OS_ERROR;
|
||||||
|
}
|
||||||
|
if (b.compositor == null or b.shell == null) {
|
||||||
|
z_native_finish();
|
||||||
|
return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
}
|
||||||
|
Egl.initialize(b);
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
// Startup also uses a bounded read/flush/poll handshake: an unresponsive
|
||||||
|
// compositor must not trap app_run before the application thread even starts.
|
||||||
|
fn initialRegistry(b: *Backend) bool {
|
||||||
|
var done = false;
|
||||||
|
const callback = c.wl_display_sync(b.display) orelse return false;
|
||||||
|
defer c.wl_callback_destroy(callback);
|
||||||
|
const listener: c.struct_wl_callback_listener = .{ .done = registryDone };
|
||||||
|
if (c.wl_callback_add_listener(callback, &listener, &done) != 0) return false;
|
||||||
|
const deadline = runtime.now(b.app.io) + 5 * std.time.ns_per_s;
|
||||||
|
while (!done) {
|
||||||
|
if (c.z_wl_display_dispatch_pending.?(b.display) < 0) return false;
|
||||||
|
if (done) return true;
|
||||||
|
const now = runtime.now(b.app.io);
|
||||||
|
if (now >= deadline) return false;
|
||||||
|
if (c.z_wl_display_prepare_read.?(b.display) != 0) continue;
|
||||||
|
var events: c_short = c.POLLIN;
|
||||||
|
if (c.z_wl_display_flush.?(b.display) < 0) {
|
||||||
|
if (c.z_linux_errno() == c.EAGAIN) events |= c.POLLOUT else {
|
||||||
|
c.z_wl_display_cancel_read.?(b.display);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var fd: c.struct_pollfd = .{ .fd = c.z_wl_display_get_fd.?(b.display), .events = events, .revents = 0 };
|
||||||
|
const ready = c.z_linux_poll(&fd, 1, @intCast(@min(5000, (deadline - now + std.time.ns_per_ms - 1) / std.time.ns_per_ms)));
|
||||||
|
if (ready > 0 and fd.revents & c.POLLIN != 0) {
|
||||||
|
if (c.z_wl_display_read_events.?(b.display) < 0) return false;
|
||||||
|
} else c.z_wl_display_cancel_read.?(b.display);
|
||||||
|
if (ready < 0 and c.z_linux_errno() != c.EINTR) return false;
|
||||||
|
if (fd.revents & (c.POLLHUP | c.POLLERR | c.POLLNVAL) != 0) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
fn registryDone(data: ?*anyopaque, _: ?*c.struct_wl_callback, _: u32) callconv(.c) void {
|
||||||
|
const done: *bool = @ptrCast(@alignCast(data.?));
|
||||||
|
done.* = true;
|
||||||
|
}
|
||||||
|
export fn z_native_finish() void {
|
||||||
|
const b = instance orelse return;
|
||||||
|
std.debug.assert(b.windows == null);
|
||||||
|
Egl.finish(b);
|
||||||
|
if (b.fractional != null) c.wp_fractional_scale_manager_v1_destroy(b.fractional);
|
||||||
|
if (b.viewporter != null) c.wp_viewporter_destroy(b.viewporter);
|
||||||
|
if (b.decorations != null) c.zxdg_decoration_manager_v1_destroy(b.decorations);
|
||||||
|
if (b.shell != null) c.xdg_wm_base_destroy(b.shell);
|
||||||
|
if (b.compositor != null) c.wl_compositor_destroy(b.compositor);
|
||||||
|
if (b.registry != null) c.wl_registry_destroy(b.registry);
|
||||||
|
if (b.display != null) c.z_wl_display_disconnect.?(b.display);
|
||||||
|
const fd = wake_fd.swap(-1, .acq_rel);
|
||||||
|
if (fd >= 0) _ = c.close(fd);
|
||||||
|
const gpa = b.app.gpa;
|
||||||
|
instance = null;
|
||||||
|
gpa.destroy(b);
|
||||||
|
c.z_linux_libraries_close();
|
||||||
|
}
|
||||||
|
export fn z_native_wake() void {
|
||||||
|
const fd = wake_fd.load(.acquire);
|
||||||
|
if (fd < 0) return;
|
||||||
|
const one: u64 = 1;
|
||||||
|
while (c.z_linux_write(fd, &one, @sizeOf(u64)) < 0) {
|
||||||
|
if (c.z_linux_errno() != c.EINTR) break; // EAGAIN already means readable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export fn z_native_again() void {
|
||||||
|
z_native_wake();
|
||||||
|
}
|
||||||
|
export fn z_native_stop() void {
|
||||||
|
if (instance) |b| b.running = false;
|
||||||
|
}
|
||||||
|
export fn z_native_pool_push() ?*anyopaque {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
export fn z_native_pool_pop(_: ?*anyopaque) void {}
|
||||||
|
|
||||||
|
fn fail(b: *Backend) void {
|
||||||
|
if (b.failed.swap(true, .acq_rel)) return;
|
||||||
|
b.app.lock();
|
||||||
|
b.app.stopping = true;
|
||||||
|
b.app.unlock();
|
||||||
|
runtime.simpleEvent(b.app, n.UHDK_EVENT_QUIT_REQUESTED);
|
||||||
|
}
|
||||||
|
export fn z_native_run() void {
|
||||||
|
const b = instance.?;
|
||||||
|
while (b.running) {
|
||||||
|
if (!b.failed.load(.acquire) and c.z_wl_display_dispatch_pending.?(b.display) < 0) fail(b);
|
||||||
|
n.z_tick(runtime.toAppHandle(b.app));
|
||||||
|
var cursor = b.windows;
|
||||||
|
var timeout: c_int = -1;
|
||||||
|
while (cursor) |w| {
|
||||||
|
cursor = w.next;
|
||||||
|
if (w.request != 0) {
|
||||||
|
timeout = 50;
|
||||||
|
if (runtime.now(b.app.io) >= w.deadline) {
|
||||||
|
completeCreation(w, abi.UNAVAILABLE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!b.failed.load(.acquire)) apply(w);
|
||||||
|
}
|
||||||
|
if (!b.running) break;
|
||||||
|
var reading = false;
|
||||||
|
var display_events: c_short = c.POLLIN;
|
||||||
|
if (!b.failed.load(.acquire)) {
|
||||||
|
if (c.z_wl_display_prepare_read.?(b.display) != 0) continue;
|
||||||
|
reading = true;
|
||||||
|
if (c.z_wl_display_flush.?(b.display) < 0) {
|
||||||
|
if (c.z_linux_errno() == c.EAGAIN) display_events |= c.POLLOUT else {
|
||||||
|
c.z_wl_display_cancel_read.?(b.display);
|
||||||
|
fail(b);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var fds = [_]c.struct_pollfd{
|
||||||
|
.{ .fd = if (reading) c.z_wl_display_get_fd.?(b.display) else -1, .events = display_events, .revents = 0 },
|
||||||
|
.{ .fd = wake_fd.load(.acquire), .events = c.POLLIN, .revents = 0 },
|
||||||
|
};
|
||||||
|
const ready = c.z_linux_poll(&fds, fds.len, timeout);
|
||||||
|
if (reading) {
|
||||||
|
if (ready > 0 and fds[0].revents & c.POLLIN != 0) {
|
||||||
|
if (c.z_wl_display_read_events.?(b.display) < 0) fail(b);
|
||||||
|
} else c.z_wl_display_cancel_read.?(b.display);
|
||||||
|
}
|
||||||
|
if (ready < 0 and c.z_linux_errno() != c.EINTR) fail(b);
|
||||||
|
if (fds[0].revents & (c.POLLERR | c.POLLHUP | c.POLLNVAL) != 0) fail(b);
|
||||||
|
if (fds[1].revents & c.POLLIN != 0) {
|
||||||
|
var value: u64 = 0;
|
||||||
|
_ = c.z_linux_read(fds[1].fd, &value, @sizeOf(u64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registryGlobal(data: ?*anyopaque, registry: ?*c.struct_wl_registry, name: u32, interface: [*c]const u8, version: u32) callconv(.c) void {
|
||||||
|
const b: *Backend = @ptrCast(@alignCast(data.?));
|
||||||
|
const str = std.mem.span(interface);
|
||||||
|
if (std.mem.eql(u8, str, "wl_compositor") and version >= 4) {
|
||||||
|
b.compositor = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wl_compositor_interface, @min(version, 6)));
|
||||||
|
} else if (std.mem.eql(u8, str, "xdg_wm_base")) {
|
||||||
|
b.shell = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_xdg_wm_base_interface, @min(version, 6)));
|
||||||
|
if (b.shell != null) _ = c.xdg_wm_base_add_listener(b.shell, &shell_listener, b);
|
||||||
|
} else if (std.mem.eql(u8, str, "zxdg_decoration_manager_v1")) {
|
||||||
|
b.decorations = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_zxdg_decoration_manager_v1_interface, 1));
|
||||||
|
} else if (std.mem.eql(u8, str, "wp_viewporter")) {
|
||||||
|
b.viewporter = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wp_viewporter_interface, 1));
|
||||||
|
} else if (std.mem.eql(u8, str, "wp_fractional_scale_manager_v1")) {
|
||||||
|
b.fractional = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wp_fractional_scale_manager_v1_interface, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn registryRemove(_: ?*anyopaque, _: ?*c.struct_wl_registry, _: u32) callconv(.c) void {}
|
||||||
|
const registry_listener: c.struct_wl_registry_listener = .{ .global = registryGlobal, .global_remove = registryRemove };
|
||||||
|
fn ping(_: ?*anyopaque, shell: ?*c.struct_xdg_wm_base, serial: u32) callconv(.c) void {
|
||||||
|
c.xdg_wm_base_pong(shell, serial);
|
||||||
|
}
|
||||||
|
const shell_listener: c.struct_xdg_wm_base_listener = .{ .ping = ping };
|
||||||
|
fn window(data: ?*anyopaque) *Window {
|
||||||
|
return @ptrCast(@alignCast(data.?));
|
||||||
|
}
|
||||||
|
fn configure(data: ?*anyopaque, _: ?*c.struct_xdg_surface, serial: u32) callconv(.c) void {
|
||||||
|
const w = window(data);
|
||||||
|
w.serial = serial;
|
||||||
|
w.dirty = true;
|
||||||
|
}
|
||||||
|
const surface_listener: c.struct_xdg_surface_listener = .{ .configure = configure };
|
||||||
|
fn topConfigure(data: ?*anyopaque, _: ?*c.struct_xdg_toplevel, width: i32, height: i32, states: [*c]c.struct_wl_array) callconv(.c) void {
|
||||||
|
const w = window(data);
|
||||||
|
if (width > 0) w.width = width;
|
||||||
|
if (height > 0) w.height = height;
|
||||||
|
w.focused = false;
|
||||||
|
w.state = n.UHDK_WINDOW_NORMAL;
|
||||||
|
if (states.*.size == 0) return;
|
||||||
|
const values: [*]const u32 = @ptrCast(@alignCast(states.*.data));
|
||||||
|
for (values[0 .. states.*.size / @sizeOf(u32)]) |value| {
|
||||||
|
switch (value) {
|
||||||
|
c.XDG_TOPLEVEL_STATE_ACTIVATED => w.focused = true,
|
||||||
|
c.XDG_TOPLEVEL_STATE_MAXIMIZED => if (w.state != n.UHDK_WINDOW_FULLSCREEN) {
|
||||||
|
w.state = n.UHDK_WINDOW_MAXIMIZED;
|
||||||
|
},
|
||||||
|
c.XDG_TOPLEVEL_STATE_FULLSCREEN => w.state = n.UHDK_WINDOW_FULLSCREEN,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn emit(w: *Window, kind: u32) void {
|
||||||
|
var event = std.mem.zeroes(n.z_event);
|
||||||
|
event.type = kind;
|
||||||
|
event.window = w.handle;
|
||||||
|
n.z_emit(runtime.toAppHandle(w.owner), &event);
|
||||||
|
}
|
||||||
|
fn topClose(data: ?*anyopaque, _: ?*c.struct_xdg_toplevel) callconv(.c) void {
|
||||||
|
emit(window(data), n.UHDK_EVENT_WINDOW_CLOSE_REQUESTED);
|
||||||
|
}
|
||||||
|
fn bounds(_: ?*anyopaque, _: ?*c.struct_xdg_toplevel, _: i32, _: i32) callconv(.c) void {}
|
||||||
|
fn wmCaps(_: ?*anyopaque, _: ?*c.struct_xdg_toplevel, _: [*c]c.struct_wl_array) callconv(.c) void {}
|
||||||
|
const top_listener: c.struct_xdg_toplevel_listener = .{ .configure = topConfigure, .close = topClose, .configure_bounds = bounds, .wm_capabilities = wmCaps };
|
||||||
|
fn decorationConfigure(data: ?*anyopaque, _: ?*c.struct_zxdg_toplevel_decoration_v1, mode: u32) callconv(.c) void {
|
||||||
|
const w = window(data);
|
||||||
|
// Until the client-decoration adapter exists, don't silently create a
|
||||||
|
// decorated window with no title bar on compositors that decline SSD.
|
||||||
|
if (mode != c.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE and w.request != 0) completeCreation(w, n.UHDK_STATUS_UNSUPPORTED);
|
||||||
|
}
|
||||||
|
const decoration_listener: c.struct_zxdg_toplevel_decoration_v1_listener = .{ .configure = decorationConfigure };
|
||||||
|
fn fraction(data: ?*anyopaque, _: ?*c.struct_wp_fractional_scale_v1, scale: u32) callconv(.c) void {
|
||||||
|
const w = window(data);
|
||||||
|
if (scale == 0 or scale > 120 * 16) return;
|
||||||
|
w.scale = scale;
|
||||||
|
w.dirty = true;
|
||||||
|
}
|
||||||
|
const fraction_listener: c.struct_wp_fractional_scale_v1_listener = .{ .preferred_scale = fraction };
|
||||||
|
fn enter(_: ?*anyopaque, _: ?*c.struct_wl_surface, _: ?*c.struct_wl_output) callconv(.c) void {}
|
||||||
|
fn bufferScale(data: ?*anyopaque, _: ?*c.struct_wl_surface, scale: i32) callconv(.c) void {
|
||||||
|
const w = window(data);
|
||||||
|
if (w.fractional == null and scale > 0 and scale <= 16) {
|
||||||
|
w.scale = @as(u32, @intCast(scale)) * 120;
|
||||||
|
w.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn bufferTransform(_: ?*anyopaque, _: ?*c.struct_wl_surface, _: u32) callconv(.c) void {}
|
||||||
|
const wl_surface_listener: c.struct_wl_surface_listener = .{ .enter = enter, .leave = enter, .preferred_buffer_scale = bufferScale, .preferred_buffer_transform = bufferTransform };
|
||||||
|
|
||||||
|
fn completeCreation(w: *Window, status: u32) void {
|
||||||
|
const id = w.request;
|
||||||
|
if (id == 0) return;
|
||||||
|
w.request = 0;
|
||||||
|
const app = runtime.toAppHandle(w.owner);
|
||||||
|
var result = std.mem.zeroes(n.z_request_result);
|
||||||
|
result.value.window = w.handle;
|
||||||
|
if (status != abi.OK) {
|
||||||
|
n.z_object_set_native(w.handle, null);
|
||||||
|
destroyWindow(w);
|
||||||
|
}
|
||||||
|
n.z_complete(app, id, status, &result);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(w: *Window) void {
|
||||||
|
if (!w.dirty or !w.mutex.tryLock()) return;
|
||||||
|
// Never block the display dispatcher behind eglSwapBuffers, which itself
|
||||||
|
// may be waiting for the dispatcher. end_frame wakes us to retry.
|
||||||
|
if (w.serial) |serial| {
|
||||||
|
c.xdg_surface_ack_configure(w.xdg, serial);
|
||||||
|
w.serial = null;
|
||||||
|
w.configured = true;
|
||||||
|
}
|
||||||
|
if (!w.configured) {
|
||||||
|
w.mutex.unlock(w.owner.io);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const width = @min(w.width, 65536);
|
||||||
|
const height = @min(w.height, 65536);
|
||||||
|
const pixel_width: u32 = @intCast(@divTrunc(@as(i64, width) * w.scale + 119, 120));
|
||||||
|
const pixel_height: u32 = @intCast(@divTrunc(@as(i64, height) * w.scale + 119, 120));
|
||||||
|
if (w.viewport != null) {
|
||||||
|
c.wl_surface_set_buffer_scale(w.surface, 1);
|
||||||
|
c.wp_viewport_set_destination(w.viewport, width, height);
|
||||||
|
} else c.wl_surface_set_buffer_scale(w.surface, @intCast(w.scale / 120));
|
||||||
|
c.xdg_surface_set_window_geometry(w.xdg, 0, 0, width, height);
|
||||||
|
if (w.egl) |ctx| c.z_wl_egl_window_resize.?(ctx.native, @intCast(pixel_width), @intCast(pixel_height), 0, 0);
|
||||||
|
if (!w.want_visible and w.visible) {
|
||||||
|
if (w.egl) |ctx| Egl.resetSurface(ctx);
|
||||||
|
c.wl_surface_attach(w.surface, null, 0, 0);
|
||||||
|
c.wl_surface_commit(w.surface);
|
||||||
|
w.configured = false;
|
||||||
|
}
|
||||||
|
w.visible = w.want_visible;
|
||||||
|
w.metrics = .{ .struct_size = @sizeOf(n.uhdk_window_metrics), .framebuffer_width = pixel_width, .framebuffer_height = pixel_height, .logical_size = .{ .width = @floatFromInt(width), .height = @floatFromInt(height) }, .scale = @as(f32, @floatFromInt(w.scale)) / 120, .state = w.state, .focused = @intFromBool(w.focused), .visible = @intFromBool(w.visible) };
|
||||||
|
const metrics = w.metrics;
|
||||||
|
w.dirty = false;
|
||||||
|
w.mutex.unlock(w.owner.io);
|
||||||
|
n.z_metrics(w.handle, &metrics);
|
||||||
|
emit(w, n.UHDK_EVENT_WINDOW_REDRAW);
|
||||||
|
if (w.request != 0) completeCreation(w, abi.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn createWindow(b: *Backend, command: *const n.z_command, id: u64) u32 {
|
||||||
|
const desc = command.window;
|
||||||
|
if (desc.flags & n.UHDK_WINDOW_DECORATED != 0 and b.decorations == null) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
const w = b.app.gpa.create(Window) catch return abi.OOM;
|
||||||
|
w.* = .{ .owner = b.app, .handle = @ptrCast(command.target), .width = @intFromFloat(@max(1, desc.content_size.width)), .height = @intFromFloat(@max(1, desc.content_size.height)), .min_width = @intFromFloat(desc.min_size.width), .min_height = @intFromFloat(desc.min_size.height), .max_width = @intFromFloat(desc.max_size.width), .max_height = @intFromFloat(desc.max_size.height), .flags = desc.flags };
|
||||||
|
w.next = b.windows;
|
||||||
|
b.windows = w;
|
||||||
|
const status = setupWindow(b, w, desc);
|
||||||
|
if (status != abi.OK) {
|
||||||
|
destroyWindow(w);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
n.z_object_set_native(command.target, w);
|
||||||
|
w.request = id;
|
||||||
|
w.deadline = runtime.now(b.app.io) + 5 * std.time.ns_per_s;
|
||||||
|
c.wl_surface_commit(w.surface); // Empty initial commit; wait for configure.
|
||||||
|
return n.Z_DEFERRED;
|
||||||
|
}
|
||||||
|
fn setupWindow(b: *Backend, w: *Window, desc: n.uhdk_window_desc) u32 {
|
||||||
|
const title = b.app.gpa.dupeZ(u8, abi.textBytes(desc.title)) catch return abi.OOM;
|
||||||
|
defer b.app.gpa.free(title);
|
||||||
|
const app_id = b.app.gpa.dupeZ(u8, b.app.application_id) catch return abi.OOM;
|
||||||
|
defer b.app.gpa.free(app_id);
|
||||||
|
w.surface = c.wl_compositor_create_surface(b.compositor) orelse return abi.OOM;
|
||||||
|
if (c.wl_surface_add_listener(w.surface, &wl_surface_listener, w) != 0) return abi.OOM;
|
||||||
|
w.xdg = c.xdg_wm_base_get_xdg_surface(b.shell, w.surface) orelse return abi.OOM;
|
||||||
|
if (c.xdg_surface_add_listener(w.xdg, &surface_listener, w) != 0) return abi.OOM;
|
||||||
|
w.top = c.xdg_surface_get_toplevel(w.xdg) orelse return abi.OOM;
|
||||||
|
if (c.xdg_toplevel_add_listener(w.top, &top_listener, w) != 0) return abi.OOM;
|
||||||
|
c.xdg_toplevel_set_title(w.top, title);
|
||||||
|
c.xdg_toplevel_set_app_id(w.top, app_id);
|
||||||
|
if (desc.parent) |parent| {
|
||||||
|
const p: *Window = @ptrCast(@alignCast(n.z_object_native(parent) orelse return abi.UNAVAILABLE));
|
||||||
|
c.xdg_toplevel_set_parent(w.top, p.top);
|
||||||
|
}
|
||||||
|
if (w.flags & n.UHDK_WINDOW_RESIZABLE == 0) {
|
||||||
|
w.min_width = w.width;
|
||||||
|
w.min_height = w.height;
|
||||||
|
w.max_width = w.width;
|
||||||
|
w.max_height = w.height;
|
||||||
|
}
|
||||||
|
if (w.flags & n.UHDK_WINDOW_RESIZABLE != 0) {
|
||||||
|
w.width = std.math.clamp(w.width, @max(1, w.min_width), if (w.max_width == 0) 65536 else w.max_width);
|
||||||
|
w.height = std.math.clamp(w.height, @max(1, w.min_height), if (w.max_height == 0) 65536 else w.max_height);
|
||||||
|
}
|
||||||
|
setLimits(w);
|
||||||
|
if (w.flags & n.UHDK_WINDOW_DECORATED != 0) {
|
||||||
|
w.decoration = c.zxdg_decoration_manager_v1_get_toplevel_decoration(b.decorations, w.top) orelse return abi.OOM;
|
||||||
|
if (c.zxdg_toplevel_decoration_v1_add_listener(w.decoration, &decoration_listener, w) != 0) return abi.OOM;
|
||||||
|
c.zxdg_toplevel_decoration_v1_set_mode(w.decoration, c.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
|
||||||
|
}
|
||||||
|
if (b.fractional != null and b.viewporter != null) {
|
||||||
|
w.viewport = c.wp_viewporter_get_viewport(b.viewporter, w.surface) orelse return abi.OOM;
|
||||||
|
w.fractional = c.wp_fractional_scale_manager_v1_get_fractional_scale(b.fractional, w.surface) orelse return abi.OOM;
|
||||||
|
if (c.wp_fractional_scale_v1_add_listener(w.fractional, &fraction_listener, w) != 0) return abi.OOM;
|
||||||
|
}
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
fn setLimits(w: *Window) void {
|
||||||
|
c.xdg_toplevel_set_min_size(w.top, w.min_width, w.min_height);
|
||||||
|
c.xdg_toplevel_set_max_size(w.top, w.max_width, w.max_height);
|
||||||
|
}
|
||||||
|
fn destroyWindow(w: *Window) void {
|
||||||
|
const b = instance.?;
|
||||||
|
std.debug.assert(w.egl == null);
|
||||||
|
var link = &b.windows;
|
||||||
|
while (link.*) |item| {
|
||||||
|
if (item == w) {
|
||||||
|
link.* = item.next;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
link = &item.next;
|
||||||
|
}
|
||||||
|
if (w.fractional != null) c.wp_fractional_scale_v1_destroy(w.fractional);
|
||||||
|
if (w.viewport != null) c.wp_viewport_destroy(w.viewport);
|
||||||
|
if (w.decoration != null) c.zxdg_toplevel_decoration_v1_destroy(w.decoration);
|
||||||
|
if (w.top != null) c.xdg_toplevel_destroy(w.top);
|
||||||
|
if (w.xdg != null) c.xdg_surface_destroy(w.xdg);
|
||||||
|
if (w.surface != null) c.wl_surface_destroy(w.surface);
|
||||||
|
w.owner.gpa.destroy(w);
|
||||||
|
}
|
||||||
|
export fn z_native_execute(_: ?*n.uhdk_app, raw: [*c]const n.z_command, id: u64, result: [*c]n.z_request_result) u32 {
|
||||||
|
const b = instance.?;
|
||||||
|
const cmd: *const n.z_command = &raw.*;
|
||||||
|
if (cmd.op == n.Z_DESTROY) {
|
||||||
|
if (n.z_object_native(cmd.target)) |ptr| {
|
||||||
|
z_native_destroy(n.z_object_kind(cmd.target), ptr);
|
||||||
|
n.z_object_set_native(cmd.target, null);
|
||||||
|
}
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
if (b.failed.load(.acquire)) return abi.UNAVAILABLE;
|
||||||
|
if (cmd.op == n.Z_WINDOW) return createWindow(b, cmd, id);
|
||||||
|
const ptr = if (cmd.target != null) n.z_object_native(cmd.target) else null;
|
||||||
|
const w: *Window = if (ptr) |p| @ptrCast(@alignCast(p)) else return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
switch (cmd.op) {
|
||||||
|
n.Z_TITLE => {
|
||||||
|
const title = b.app.gpa.dupeZ(u8, abi.textBytes(cmd.text)) catch return abi.OOM;
|
||||||
|
defer b.app.gpa.free(title);
|
||||||
|
c.xdg_toplevel_set_title(w.top, title);
|
||||||
|
},
|
||||||
|
n.Z_SIZE => {
|
||||||
|
if (w.state != n.UHDK_WINDOW_NORMAL) return abi.UNAVAILABLE;
|
||||||
|
w.width = @intFromFloat(@max(1, cmd.size.width));
|
||||||
|
w.height = @intFromFloat(@max(1, cmd.size.height));
|
||||||
|
if (w.flags & n.UHDK_WINDOW_RESIZABLE != 0) {
|
||||||
|
w.width = std.math.clamp(w.width, @max(1, w.min_width), if (w.max_width == 0) 65536 else w.max_width);
|
||||||
|
w.height = std.math.clamp(w.height, @max(1, w.min_height), if (w.max_height == 0) 65536 else w.max_height);
|
||||||
|
}
|
||||||
|
if (w.flags & n.UHDK_WINDOW_RESIZABLE == 0) {
|
||||||
|
w.min_width = w.width;
|
||||||
|
w.max_width = w.width;
|
||||||
|
w.min_height = w.height;
|
||||||
|
w.max_height = w.height;
|
||||||
|
setLimits(w);
|
||||||
|
}
|
||||||
|
w.dirty = true;
|
||||||
|
},
|
||||||
|
n.Z_STATE => switch (cmd.number) {
|
||||||
|
n.UHDK_WINDOW_MINIMIZED => c.xdg_toplevel_set_minimized(w.top),
|
||||||
|
n.UHDK_WINDOW_MAXIMIZED => {
|
||||||
|
c.xdg_toplevel_unset_fullscreen(w.top);
|
||||||
|
c.xdg_toplevel_set_maximized(w.top);
|
||||||
|
},
|
||||||
|
n.UHDK_WINDOW_FULLSCREEN => c.xdg_toplevel_set_fullscreen(w.top, null),
|
||||||
|
else => {
|
||||||
|
c.xdg_toplevel_unset_fullscreen(w.top);
|
||||||
|
c.xdg_toplevel_unset_maximized(w.top);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
n.Z_VISIBLE => {
|
||||||
|
w.want_visible = cmd.number != 0;
|
||||||
|
if (w.want_visible and !w.configured) c.wl_surface_commit(w.surface);
|
||||||
|
w.dirty = true;
|
||||||
|
},
|
||||||
|
n.Z_FOCUS => return n.UHDK_STATUS_UNSUPPORTED,
|
||||||
|
n.Z_REDRAW => emit(w, n.UHDK_EVENT_WINDOW_REDRAW),
|
||||||
|
n.Z_GL => return Egl.create(b, w, cmd.other, result),
|
||||||
|
else => return n.UHDK_STATUS_UNSUPPORTED,
|
||||||
|
}
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
export fn z_native_cancel(id: u64) void {
|
||||||
|
const b = instance orelse return;
|
||||||
|
var cursor = b.windows;
|
||||||
|
while (cursor) |w| : (cursor = w.next) {
|
||||||
|
if (w.request == id) {
|
||||||
|
completeCreation(w, abi.CANCELLED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export fn z_native_destroy(kind: u32, ptr: ?*anyopaque) void {
|
||||||
|
const p = ptr orelse return;
|
||||||
|
switch (kind) {
|
||||||
|
n.Z_KIND_WINDOW => {
|
||||||
|
const w: *Window = @ptrCast(@alignCast(p));
|
||||||
|
if (w.request != 0) completeCreation(w, abi.CANCELLED) else destroyWindow(w);
|
||||||
|
},
|
||||||
|
n.Z_KIND_GL => Egl.destroy(@ptrCast(@alignCast(p))),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export fn z_native_handles(ptr: ?*anyopaque, out: [*c]n.uhdk_native_window) void {
|
||||||
|
const w: *Window = @ptrCast(@alignCast(ptr.?));
|
||||||
|
out.* = .{ .struct_size = @sizeOf(n.uhdk_native_window), .backend = n.UHDK_BACKEND_WAYLAND, .primary = instance.?.display, .secondary = w.surface, .value = 0 };
|
||||||
|
}
|
||||||
|
export fn z_native_gl(ptr: ?*anyopaque, op: u32, out: ?*anyopaque) u32 {
|
||||||
|
return Egl.call(ptr, op, out);
|
||||||
|
}
|
||||||
|
export fn z_native_gl_proc(ptr: ?*anyopaque, name: n.uhdk_text, out: [*c]n.uhdk_gl_proc) u32 {
|
||||||
|
return Egl.proc(ptr, name, out);
|
||||||
|
}
|
||||||
|
export fn z_native_file_init(file: ?*n.uhdk_file, path: n.uhdk_bytes, uri: n.uhdk_text, grant: ?*anyopaque) u32 {
|
||||||
|
return @import("Files.zig").init(file, path, uri, grant);
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}) { }
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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)')
|
||||||
|
|
@ -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)))}')
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue