diff options
| -rw-r--r-- | .envrc | 1 | ||||
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | build.zig | 116 | ||||
| -rw-r--r-- | build.zig.zon | 86 | ||||
| -rw-r--r-- | default.nix | 6 | ||||
| -rw-r--r-- | src/context.zig | 6 | ||||
| -rw-r--r-- | src/event-set.zig | 83 | ||||
| -rw-r--r-- | src/main.zig | 15 | ||||
| -rw-r--r-- | src/object.zig | 41 | ||||
| -rw-r--r-- | src/root.zig | 10 | ||||
| -rw-r--r-- | src/types.zig | 18 | ||||
| -rw-r--r-- | src/wl/callback.zig | 33 | ||||
| -rw-r--r-- | src/wl/display.zig | 265 | ||||
| -rw-r--r-- | src/wl/output.zig | 171 | ||||
| -rw-r--r-- | src/wl/registry.zig | 96 | ||||
| -rw-r--r-- | src/wl/root.zig | 4 |
16 files changed, 953 insertions, 0 deletions
@@ -0,0 +1 @@ +use nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..941c29f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.zig-cache +zig-out/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..ca6a90e --- /dev/null +++ b/build.zig @@ -0,0 +1,116 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + // This creates a "module", which represents a collection of source files alongside + // some compilation options, such as optimization mode and linked system libraries. + // Every executable or library we compile will be based on one or more modules. + const lib_mod = b.createModule(.{ + // `root_source_file` is the Zig "entry point" of the module. If a module + // only contains e.g. external object files, you can make this `null`. + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + // We will also create a module for our other entry point, 'main.zig'. + const exe_mod = b.createModule(.{ + // `root_source_file` is the Zig "entry point" of the module. If a module + // only contains e.g. external object files, you can make this `null`. + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + // Modules can depend on one another using the `std.Build.Module.addImport` function. + // This is what allows Zig source code to use `@import("foo")` where 'foo' is not a + // file path. In this case, we set up `exe_mod` to import `lib_mod`. + exe_mod.addImport("wayland", lib_mod); + + // Now, we will create a static library based on the module we created above. + // This creates a `std.Build.Step.Compile`, which is the build step responsible + // for actually invoking the compiler. + const lib = b.addLibrary(.{ + .linkage = .static, + .name = "wayland", + .root_module = lib_mod, + }); + + // This declares intent for the library to be installed into the standard + // location when the user invokes the "install" step (the default step when + // running `zig build`). + b.installArtifact(lib); + + // This creates another `std.Build.Step.Compile`, but this one builds an executable + // rather than a static library. + const exe = b.addExecutable(.{ + .name = "wayland", + .root_module = exe_mod, + }); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const lib_unit_tests = b.addTest(.{ + .root_module = lib_mod, + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + const exe_unit_tests = b.addTest(.{ + .root_module = exe_mod, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_lib_unit_tests.step); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..d057b90 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,86 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save <url>`, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = .wayland, + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // Together with name, this represents a globally unique package + // identifier. This field is generated by the Zig toolchain when the + // package is first created, and then *never changes*. This allows + // unambiguous detection of one package being an updated version of + // another. + // + // When forking a Zig project, this id should be regenerated (delete the + // field and run `zig build`) if the upstream project is still maintained. + // Otherwise, the fork is *hostile*, attempting to take control over the + // original project's identity. Thus it is recommended to leave the comment + // on the following line intact, so that it shows up in code reviews that + // modify the field. + .fingerprint = 0xa8d655e1d1844b2, // Changing this has security and trust implications. + + // Tracks the earliest Zig version that the package considers to be a + // supported use case. + .minimum_zig_version = "0.14.1", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save <url>` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. If the contents of a URL change this will result in a hash mismatch + // // which will prevent zig from using it. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + // + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..0bac3a3 --- /dev/null +++ b/default.nix @@ -0,0 +1,6 @@ +{ pkgs ? import <nixpkgs> { }, ... }: +pkgs.mkShell { + packages = [ + pkgs.zig + ]; +} diff --git a/src/context.zig b/src/context.zig new file mode 100644 index 0000000..55480ce --- /dev/null +++ b/src/context.zig @@ -0,0 +1,6 @@ +const std = @import("std"); +const wayland = @import("root.zig"); +const wl = wayland.wl; + +allocator: std.mem.Allocator, +display: *wl.Display, diff --git a/src/event-set.zig b/src/event-set.zig new file mode 100644 index 0000000..5721a4e --- /dev/null +++ b/src/event-set.zig @@ -0,0 +1,83 @@ +const std = @import("std"); +const wayland = @import("root.zig"); + +pub fn EventSet(T: type, events: anytype) type { + return struct { + pub fn on_event(ptr: *anyopaque, ctx: *wayland.Context, opcode: u16, args: []const u8) void { + std.debug.print("event {} {}\n", .{T, opcode}); + var offset: usize = 0; + inline for (events, 0..) |event, index| { + if (@TypeOf(event) != @TypeOf(null) and index == opcode) { + const Args = std.meta.ArgsTuple(@TypeOf(event)); + var fn_args: Args = undefined; + fn_args[0] = @alignCast(@ptrCast(ptr)); + fn_args[1] = ctx; + + inline for (std.meta.fields(Args)[2..], 2..) |f, arg_index| { + switch (f.type) { + void => {}, + u32, i32 => { + const result = std.mem.readPackedIntNative(f.type, args, offset * 8); + fn_args[arg_index] = result; + offset += @sizeOf(f.type); + }, + []const u8 => { + const size: u32 = std.mem.readPackedIntNative(u32, args, offset * 8); + const padding_size: usize = @mod(@as(usize, @bitCast(-@as(isize, @intCast(size)))), 4); + offset += @sizeOf(u32); + const result = args[offset..offset + size - 1]; + offset += size + padding_size; + fn_args[arg_index] = result; + }, + *wayland.Object => { + const result = std.mem.readPackedIntNative(u32, args, offset * 8); + fn_args[arg_index] = ctx.display.registry.objects.items[result] orelse unreachable; + offset += @sizeOf(u32); + }, + else => |t| switch (@typeInfo(t)) { + .@"enum" => { + const result = std.mem.readPackedIntNative(u32, args, offset * 8); + fn_args[arg_index] = @enumFromInt(result); + offset += @sizeOf(u32); + }, + else => @compileError(std.fmt.comptimePrint("unsupported type {}", .{t})) + }, + } + } + + switch (@typeInfo(@typeInfo(@TypeOf(event)).@"fn".return_type orelse void)) { + .void => @call(.auto, event, fn_args), + .error_union => @call(.auto, event, fn_args) catch |err| { + std.log.err("error on event {} ({}): {}", .{opcode, T, err}); + }, + else => |t| @compileError(std.fmt.comptimePrint("unsupported return type {}", .{t})), + } + + return; + } + } + + std.log.warn("unimplemented event {} for {}", .{opcode, T}); + } + }; +} + +test { + const Dummy = struct { + a: u32 = 0, + b: u32 = 0, + + pub fn something(self: *@This(), a: u32, b: u32) void { + self.a = a; + self.b = b; + } + }; + + var dummy = Dummy{}; + const Events = EventSet(Dummy, .{null, Dummy.something }); + + Events.on_event(&dummy, 1, &(std.mem.toBytes(@as(u32, 1)) ++ std.mem.toBytes(@as(u32, 2)))); + + try std.testing.expectEqual(dummy.a, 1); + try std.testing.expectEqual(dummy.b, 2); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..7e27f94 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,15 @@ +const std = @import("std"); +const wayland = @import("wayland"); +const wl = wayland.wl; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + var display: wl.Display = try .open_default(allocator); + defer display.close(); + try display.init(allocator, &.{wl.Output.handler}); + + try display.roundtrip(allocator); + try display.roundtrip(allocator); +} diff --git a/src/object.zig b/src/object.zig new file mode 100644 index 0000000..0751859 --- /dev/null +++ b/src/object.zig @@ -0,0 +1,41 @@ +const std = @import("std"); +const wayland = @import("root.zig"); + +const Self = @This(); + +const VTable = struct { + on_event: *const fn (ptr: *anyopaque, ctx: *wayland.Context, opcode: u16, args: []const u8) void, +}; + +id: ?u32 = null, +ptr: ?*anyopaque, +vtable: VTable, + +pub inline fn on_event(self: *Self, ctx: *wayland.Context, opcode: u16, args: []const u8) !void { + if (self.ptr) |ptr| { + self.vtable.on_event(ptr, ctx, opcode, args); + } else { + return error.UseAfterRelease; + } +} + +pub inline fn from_self(ptr: anytype) Self { + return .{ + .ptr = ptr, + .vtable = VTable{ + .on_event = @TypeOf(ptr.*).Events.on_event, + }, + }; +} + +pub fn format( + self: *const Self, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, +) !void { + _ = fmt; + _ = options; + + try writer.print("Object {{ {?} }}", .{self.id}); +} diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..ea9357a --- /dev/null +++ b/src/root.zig @@ -0,0 +1,10 @@ +const std = @import("std"); +pub const wl = @import("wl/root.zig"); +pub const Object = @import("object.zig"); +pub const EventSet = @import("event-set.zig").EventSet; +pub const types = @import("types.zig"); +pub const Context = @import("context.zig"); + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/types.zig b/src/types.zig new file mode 100644 index 0000000..2849af1 --- /dev/null +++ b/src/types.zig @@ -0,0 +1,18 @@ +const std = @import("std"); + +pub const Fd = struct { fd: std.posix.fd_t }; + +pub const Fixed = struct { + n: u24, + f: u8, + + pub fn from_f64(float: f64) @This() { + const n: u24 = @intFromFloat(float); + const f: u8 = @intFromFloat(std.math.maxInt(u8) * (float - @as(f64, @floatFromInt(n)))); + + return .{ + .n = n, + .f = f, + }; + } +}; diff --git a/src/wl/callback.zig b/src/wl/callback.zig new file mode 100644 index 0000000..0e7ea5b --- /dev/null +++ b/src/wl/callback.zig @@ -0,0 +1,33 @@ +const std = @import("std"); +const wayland = @import("../root.zig"); +const wl = wayland.wl; + +const log = std.log.scoped(.callback); + +const Self = @This(); + +pub const Events = wayland.EventSet(Self, .{done}); + +is_done: *bool, +interface: wayland.Object, + +pub fn init( + self: *Self, + allocator: std.mem.Allocator, + display: *wl.Display, + is_done: *bool, +) !void { + self.is_done = is_done; + self.interface = wayland.Object.from_self(self); + try display.registry.add_object(allocator, &self.interface); + std.debug.print("ids {any}\n", .{display.registry.objects.items}); + std.debug.print("callback #{?}\n", .{self.interface.id}); + try display.request(display, .{ .sync = .{&self.interface} }); + +} + +fn done(self: *Self, ctx: *wayland.Context, value: u32) void { + _ = value; + _ = ctx; + self.is_done.* = true; +} diff --git a/src/wl/display.zig b/src/wl/display.zig new file mode 100644 index 0000000..4f04746 --- /dev/null +++ b/src/wl/display.zig @@ -0,0 +1,265 @@ +const std = @import("std"); +const wayland = @import("../root.zig"); +const wl = wayland.wl; + +const log = std.log.scoped(.display); + +const Self = @This(); + +pub const Error = enum(u32) { + invalid_object = 0, + invalid_method = 1, + no_memory = 2, + implementation = 3, +}; + +pub const Request = union(enum) { + sync: struct { *wayland.Object }, + get_registry: struct { *wayland.Object }, +}; + +pub const Events = wayland.EventSet(Self, .{ + err, + delete_id, +}); + +stream: std.net.Stream, +interface: wayland.Object, +registry: wl.Registry, + +pub fn init( + self: *Self, + allocator: std.mem.Allocator, + handlers: []const wl.Registry.GlobalHandler, +) !void { + self.interface = wayland.Object.from_self(self); + try self.registry.init(allocator, self, handlers); +} + +pub fn from_fd(fd: std.posix.fd_t) void { + var self: Self = undefined; + self.stream = std.net.Stream{ .handle = fd }; + return self; +} + +pub fn open(path: []const u8) !Self { + var self: Self = undefined; + self.stream = try std.net.connectUnixSocket(path); + return self; +} + +pub fn close(self: *Self) void { + self.stream.close(); +} + +pub fn open_default(allocator: std.mem.Allocator) !Self { + const path = try default_path(allocator); + defer allocator.free(path); + return open(path); +} + +pub fn default_path(allocator: std.mem.Allocator) ![]const u8 { + const env = try std.process.getEnvMap(allocator); + const xdg_runtime_dir = env.get("XDG_RUNTIME_DIR") orelse return error.MissingRuntimeDir; + const wayland_display = env.get("WAYLAND_DISPLAY") orelse "wayland-0"; + return std.fmt.allocPrint(allocator, "{s}/{s}", .{ xdg_runtime_dir, wayland_display }); +} + +pub fn request(self: *Self, object: anytype, args: @TypeOf(object.*).Request) !void { + comptime std.debug.assert(@FieldType(@TypeOf(object.*), "interface") == wayland.Object); + comptime std.debug.assert(@typeInfo(@TypeOf(object.*).Request) == .@"union"); + const tag_type = @typeInfo(@TypeOf(args)).@"union".tag_type.?; + const opcode: u32 = @intFromEnum(args); + + inline for (@typeInfo(tag_type).@"enum".fields) |f| { + if (f.value == opcode) { + const data = @field(args, f.name); + const id = object.interface.id orelse return error.UnregisteredObject; + + var size: u32 = 8; + + inline for (data) |value| { + switch (@TypeOf(value)) { + u32, i32 => |t| size += @sizeOf(t), + *wayland.Object => size += @sizeOf(u32), + []const u8, []u8 => { + const padding_size: usize = @mod(@as(usize, @bitCast(-@as(isize, @intCast(value.len + 1)))), 4); + size += @as(u32, @intCast(@sizeOf(u32) + (value.len + 1) + padding_size)); + }, + else => |t| switch (@typeInfo(t)) { + .pointer => |ptr| if (ptr.size == .slice) { + const padding_size: usize = @mod(@as(usize, @bitCast(-@as(isize, @intCast(value.len * @sizeOf(ptr.child))))), 4); + size += @intCast(@sizeOf(u32) + (value.len * @sizeOf(ptr.child)) + padding_size); + } else @compileError(std.fmt.comptimePrint("size: unsupported type {}", .{t})), + else => @compileError(std.fmt.comptimePrint("size: unsupported type {}", .{t})), + }, + } + } + + _ = try self.stream.writeAll(&std.mem.toBytes(id)); + _ = try self.stream.writeAll(&std.mem.toBytes((size << 16) | opcode)); + + inline for (data) |value| { + switch (@TypeOf(value)) { + u32, i32 => _ = try self.stream.writeAll(std.mem.asBytes(&value)), + *wayland.Object => try self.stream.writeAll(std.mem.asBytes(&(value.id orelse return error.UnregisteredObject))), + []const u8, []u8 => { + const padding_size: usize = @mod(@as(usize, @bitCast(-@as(isize, @intCast(value.len + 1)))), 4); + _ = try self.stream.writeAll(std.mem.asBytes(&@as(u32, @truncate(value.len + 1)))); + _ = try self.stream.writeAll(value); + for (0..padding_size + 1) |_| { + _ = try self.stream.writeAll(std.mem.asBytes(&@as(u8, 0))); + } + }, + else => |t| switch (@typeInfo(t)) { + .pointer => |ptr| if (ptr.size == .slice) { + const padding_size: usize = @mod(@as(usize, @bitCast(-@as(isize, @intCast(value.len * @sizeOf(ptr.child))))), 4); + _ = try self.stream.writeAll(std.mem.asBytes(&@as(u32, @truncate(value.len)))); + _ = try self.stream.writeAll(std.mem.sliceAsBytes(value)); + for (0..padding_size) |_| { + _ = try self.stream.writeAll(std.mem.asBytes(&@as(u8, 0))); + } + } else @compileError(std.fmt.comptimePrint("size: unsupported type {}", .{t})), + else => @compileError(std.fmt.comptimePrint("size: unsupported type {}", .{t})), + }, + } + } + return; + } + } + + return error.InvalidOpcode; +} + +pub fn roundtrip(self: *Self, allocator: std.mem.Allocator) !void { + var done = false; + var callback: wl.Callback = undefined; + try callback.init(allocator, self, &done); + + var context: wayland.Context = .{ + .display = self, + .allocator = allocator, + }; + + while (!done) { + const endian = @import("builtin").target.cpu.arch.endian(); + const reader = self.stream.reader(); + + const object_id = try reader.readInt(u32, endian); + + const message_info = try reader.readInt(u32, endian); + + const opcode: u16 = @truncate(message_info & 0xffff); + const size: u16 = @truncate(((message_info >> 16) & 0xffff)); + const body_size = size - 2 * @sizeOf(u32); + + const body = try allocator.alloc(u8, body_size); + defer allocator.free(body); + _ = try reader.readAll(body); + + if (object_id >= self.registry.objects.items.len) { + return error.UnregisteredObject; + } + + if (self.registry.objects.items[object_id]) |object| { + try object.on_event(&context, opcode, body); + } else { + return error.UnregisteredObject; + } + } +} + +fn err( + self: *Self, + ctx: *wayland.Context, + object: *wayland.Object, + code: u32, + message: []const u8, +) void { + _ = self; + _ = ctx; + log.err("{} ({}): {s}", .{ object, code, message }); +} + +fn delete_id( + self: *Self, + ctx: *wayland.Context, + id: u32, +) void { + _ = ctx; + self.registry.release_object(id); +} + +test "request" { + const Sample = struct { + pub const Request = union(enum) { + req1: struct { u32 }, + req2: struct { *wayland.Object }, + req3: struct { []const u8 }, + req4: struct { []const u16 }, + }; + + pub const Events = wayland.EventSet(@This(), .{}); + + interface: wayland.Object, + + pub fn init(self: *@This()) void { + self.interface = wayland.Object.from_self(self); + } + }; + + const in, const out = try std.posix.pipe(); + defer std.posix.close(in); + + { + var sample: Sample = undefined; + sample.init(); + sample.interface.id = 42; + + var displ: Self = .from_fd(out); + defer displ.close(); + try displ.init(); + try displ.request(sample, .{ .req1 = .{432} }); + try displ.request(sample, .{ .req2 = .{&sample.interface} }); + try displ.request(sample, .{ .req3 = .{"abcd"} }); + try displ.request(sample, .{ .req4 = .{&[_]u16{ 1, 32, 4, 5, 5 }} }); + } + + const file = std.fs.File{ .handle = in }; + var test_buffer: [64]u8 = undefined; + _ = try file.readAll(test_buffer[0..12]); + try std.testing.expectEqualSlices( + u8, + &(std.mem.toBytes(@as(u32, 42)) ++ + std.mem.toBytes(@as(u32, (12 << 16) | 0)) ++ + std.mem.toBytes(@as(u32, 432))), + test_buffer[0..12], + ); + + _ = try file.readAll(test_buffer[0..12]); + try std.testing.expectEqualSlices( + u8, + &(std.mem.toBytes(@as(u32, 42)) ++ + std.mem.toBytes(@as(u32, (12 << 16) | 1)) ++ + std.mem.toBytes(@as(u32, 42))), + test_buffer[0..12], + ); + + _ = try file.readAll(test_buffer[0..20]); + try std.testing.expectEqualSlices( + u8, + &(std.mem.toBytes(@as(u32, 42)) ++ + std.mem.toBytes(@as(u32, (20 << 16) | 2)) ++ + std.mem.toBytes(@as(u32, 5))) ++ "abcd\x00\x00\x00\x00", + test_buffer[0..20], + ); + + _ = try file.readAll(test_buffer[0..24]); + try std.testing.expectEqualSlices( + u8, + &(std.mem.toBytes(@as(u32, 42)) ++ + std.mem.toBytes(@as(u32, (24 << 16) | 3)) ++ + std.mem.toBytes(@as(u32, 5))) ++ std.mem.toBytes([_]u16{ 1, 32, 4, 5, 5, 0 }), + test_buffer[0..24], + ); +} diff --git a/src/wl/output.zig b/src/wl/output.zig new file mode 100644 index 0000000..99e8ca1 --- /dev/null +++ b/src/wl/output.zig @@ -0,0 +1,171 @@ +const std = @import("std"); +const wayland = @import("../root.zig"); +const wl = wayland.wl; + +const Self = @This(); + +pub const Requests = union(enum) { + release: struct {}, +}; + +pub const Events = wayland.EventSet(Self, .{ + geometry, + mode, + done, + scale, + name, + description, +}); + +pub const Subpixel = enum(u32) { + unkown = 0, + none = 1, + orizontal_rgb = 2, + orizontal_bgr = 3, + vertical_rgb = 4, + vertical_bgr = 5, +}; + +pub const Transform = enum(u32) { + normal = 0, + normal_90 = 1, + normal_180 = 2, + normal_270 = 3, + flipped = 4, + flipped_90 = 5, + flipped_180 = 6, + flipped_270 = 7, +}; + +pub const Geometry = struct { + x: u32, + y: u32, + physical_width: u32, + physical_height: u32, + subpixel: Subpixel, + make: []const u8, + model: []const u8, + transform: Transform, +}; + +pub const Mode = struct { + pub const Kind = enum(u32) { + current = 1, + preferred = 2, + }; + + kind: Kind, + width: u32, + height: u32, + refresh: u32, +}; + +pub const Info = struct { + geometry: Geometry, + mode: Mode, + scale: u32, + name: []const u8, + description: []const u8, +}; + +var outputs: std.ArrayListUnmanaged(*Self) = .empty; + +staging: Info = undefined, +info: ?Info = null, +interface: wayland.Object, + +pub fn init( + self: *Self, + allocator: std.mem.Allocator, + display: *wl.Display, +) !void { + self.* = .{ .interface = wayland.Object.from_self(self) }; + try display.registry.add_object(allocator, &self.interface); +} + +pub fn deinit(self: *Self, display: *wl.Display) !void { + display.request(self, .{ .release = void{} }); +} + +pub const handler: wl.Registry.GlobalHandler = .{ + .interface = "wl_output", + .version = 4, + .handler = register, +}; + +pub fn register(ctx: *wayland.Context) ?wayland.Object { + const output = ctx.allocator.create(Self) catch return null; + output.init(ctx.allocator, ctx.display) catch return null; + outputs.append(ctx.allocator, output) catch return null; + return output.interface; +} + +fn geometry( + self: *Self, + ctx: *wayland.Context, + x: u32, + y: u32, + physical_width: u32, + physical_height: u32, + subpixel: Subpixel, + make: []const u8, + model: []const u8, + transform: Transform, +) void { + _ = ctx; + self.staging.geometry = .{ + .x = x, + .y = y, + .physical_width = physical_width, + .physical_height = physical_height, + .subpixel = subpixel, + .make = make, + .model = model, + .transform = transform, + }; +} + +fn mode( + self: *Self, + ctx: *wayland.Context, + flags: Mode.Kind, + width: u32, + height: u32, + refresh: u32, +) void { + _ = ctx; + self.staging.mode = .{ + .kind = flags, + .width = width, + .height = height, + .refresh = refresh, + }; +} + +fn done(self: *Self, ctx: *wayland.Context) void { + if (self.info) |info| { + ctx.allocator.free(info.name); + ctx.allocator.free(info.description); + } + + self.info = self.staging; +} + +fn scale(self: *Self, ctx: *wayland.Context, factor: u32) void { + _ = ctx; + self.staging.scale = factor; +} + +fn name(self: *Self, ctx: *wayland.Context, n: []const u8) !void { + self.staging.name = try ctx.allocator.alloc(u8, n.len); + @memcpy(@constCast(self.staging.name), n); +} + +fn description(self: *Self, ctx: *wayland.Context, d: []const u8) !void { + self.staging.description = try ctx.allocator.alloc(u8, d.len); + @memcpy(@constCast(self.staging.description), d); +} + +pub fn get(index: usize) *Self { + return &outputs.items[index]; +} diff --git a/src/wl/registry.zig b/src/wl/registry.zig new file mode 100644 index 0000000..e4eb3a8 --- /dev/null +++ b/src/wl/registry.zig @@ -0,0 +1,96 @@ +const std = @import("std"); +const wayland = @import("../root.zig"); +const wl = wayland.wl; + +const log = std.log.scoped(.registry); + +const Self = @This(); + +pub const Events = wayland.EventSet(Self, .{ + global, + global_remove, +}); + +pub const Request = union(enum) { + bind: struct { u32, u32 }, +}; + +pub const GlobalHandler = struct { + interface: []const u8, + version: u32, + handler: *const fn (ctx: *wayland.Context) ?wayland.Object, +}; + +objects: std.ArrayListUnmanaged(?*wayland.Object) = .empty, +interface: wayland.Object, +handlers: []const GlobalHandler, + +pub fn init( + self: *Self, + allocator: std.mem.Allocator, + display: *wl.Display, + handlers: []const GlobalHandler, +) !void { + self.* = .{ + .interface = wayland.Object.from_self(self), + .handlers = handlers, + }; + try self.objects.append(allocator, null); + try self.objects.append(allocator, &display.interface); + try self.objects.append(allocator, &self.interface); + + display.interface.id = 1; + self.interface.id = 2; + + try display.request(display, .{ .get_registry = .{&self.interface} }); +} + +pub fn add_object( + self: *Self, + allocator: std.mem.Allocator, + object: *wayland.Object, +) !void { + for (self.objects.items[1..], 1..) |obj, index| { + if (obj == null) { + object.id = @truncate(index); + self.objects.items[index] = object; + return; + } + } + + object.id = @truncate(self.objects.items.len); + try self.objects.append(allocator, object); +} + +pub fn release_object(self: *Self, id: u32) void { + self.objects.items[id] = null; +} + +fn global( + self: *Self, + ctx: *wayland.Context, + name: u32, + interface: []const u8, + version: u32, +) !void { + for (self.handlers) |handler| { + if (version == handler.version and std.mem.eql(u8, interface, handler.interface)) { + if (handler.handler(ctx)) |object| { + try ctx.display.request(self, .{ .bind = .{ + name, + object.id orelse return error.UnregisteredObject, + } }); + } + } + } +} + +fn global_remove( + self: *Self, + ctx: *wayland.Context, + name: u32, +) void { + _ = self; + _ = ctx; + log.debug("remove_global: {}", .{name}); +} diff --git a/src/wl/root.zig b/src/wl/root.zig new file mode 100644 index 0000000..b9e149f --- /dev/null +++ b/src/wl/root.zig @@ -0,0 +1,4 @@ +pub const Display = @import("display.zig"); +pub const Registry = @import("registry.zig"); +pub const Callback = @import("callback.zig"); +pub const Output = @import("output.zig"); |