From d64d30c7db35c2dff4981c7284944230ed46a353 Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Mon, 4 Mar 2024 10:33:16 -0700 Subject: [PATCH] mach-gamemode moves back into the main repository Helps hexops/mach#1165 Signed-off-by: Stephen Gutekanst --- .github/ISSUE_TEMPLATE/dev_zig_nomination.md | 2 - src/gamemode.zig | 263 +++++++++++++++++++ src/main.zig | 2 + 3 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 src/gamemode.zig diff --git a/.github/ISSUE_TEMPLATE/dev_zig_nomination.md b/.github/ISSUE_TEMPLATE/dev_zig_nomination.md index 6ffd3bc3..33c32d39 100644 --- a/.github/ISSUE_TEMPLATE/dev_zig_nomination.md +++ b/.github/ISSUE_TEMPLATE/dev_zig_nomination.md @@ -45,7 +45,6 @@ You may have been linked to this issue because you sent a pull request to update These projects have zero `build.zig.zon` dependencies, we update them first - and in any order. -* [ ] mach-gamemode * [ ] mach-model3d * [ ] mach-sysjs * [ ] mach-objc-generator @@ -133,7 +132,6 @@ These projects have dependencies on other projects. We update them in the exact * [ ] mach-core, which depends on: * build.zig version check * mach-core-example-assets - * mach-gamemode * mach-sysgpu * mach-gpu * mach-glfw diff --git a/src/gamemode.zig b/src/gamemode.zig new file mode 100644 index 00000000..1693841d --- /dev/null +++ b/src/gamemode.zig @@ -0,0 +1,263 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const log = std.log.scoped(.gamemode); + +test { + std.testing.refAllDeclsRecursive(@This()); +} + +pub const LoadError = error{MissingSymbol} || std.DynLib.Error; +pub const Error = error{ RequestFailed, RequestRejected }; + +pub const Status = enum(u8) { + inactive = 0, + active = 1, + active_and_registered = 2, + + pub fn isActive(self: Status) bool { + const lut = [_]bool{ false, true, true }; + return lut[@intFromEnum(self)]; + } +}; + +pub const advanced = switch (builtin.os.tag) { + .linux => linux_impl, + else => noop_impl, +}; + +test "start and end gamemode" { + try std.testing.expectEqual(false, isActive()); + start(); + stop(); + try std.testing.expectEqual(false, isActive()); +} + +pub fn start() void { + if (!advanced.init()) return; + advanced.requestStart() catch |err| { + log.warn("gamemode: failed to start: {}", .{err}); + }; +} + +pub fn isActive() bool { + const status = advanced.queryStatus() catch |err| { + log.warn("gamemode: error querying status: {}", .{err}); + return false; + }; + return status.isActive(); +} + +pub fn stop() void { + advanced.requestEnd() catch |err| { + log.warn("gamemode: failed to end: {}", .{err}); + }; + advanced.deinit(); +} + +const linux_impl = struct { + comptime { + if (!builtin.link_libc) { + // TODO: LibC is currently required for using `dlopen`. + // We might get it working using Zig's `ElfDynLib` later, but this would require + // finding the location of the gamemode lib somehow. It also wouldn't be 100% correct, + // as `LD_PRELOAD` wouldn't be respected. + @compileError("Must link LibC for gamemode!"); + } + } + + /// Global state for the library handle + var state: State = .uninit; + + const State = union(enum) { + uninit, + failed, + init: GamemodeHandle, + }; + + const GamemodeHandle = struct { + lib: std.DynLib, + syms: SymbolTable, + }; + + const SymbolTable = struct { + real_gamemode_error_string: *const fn () callconv(.C) [*:0]const u8, + + real_gamemode_request_start: *const fn () callconv(.C) c_int, + real_gamemode_request_end: *const fn () callconv(.C) c_int, + real_gamemode_query_status: *const fn () callconv(.C) c_int, + + real_gamemode_request_start_for: *const fn (std.os.pid_t) callconv(.C) c_int, + real_gamemode_request_end_for: *const fn (std.os.pid_t) callconv(.C) c_int, + real_gamemode_query_status_for: *const fn (std.os.pid_t) callconv(.C) c_int, + }; + + /// Try to load libgamemode, returning an error when loading fails. + /// If you want to ignore errors, call `init` instead. + /// + /// Unlike `init`, calling `init` or `tryInit` after a failure + /// will attempt to load libgamemode again. + pub fn tryInit() LoadError!void { + if (state == .init) return; + + var dl = std.DynLib.openZ("libgamemode.so.0") catch |e| switch (e) { + // backwards-compatibility for old gamemode versions + error.FileNotFound => try std.DynLib.openZ("libgamemode.so"), + else => return e, + }; + errdefer dl.close(); + + // Populate symbol table. + var sym_table: SymbolTable = undefined; + inline for (@typeInfo(SymbolTable).Struct.fields) |field| { + @field(sym_table, field.name) = dl.lookup(field.type, field.name ++ &[_:0]u8{}) orelse { + log.err("libgamemode missing symbol '{s}'", .{field.name}); + return error.MissingSymbol; + }; + } + + state = .{ .init = .{ .lib = dl, .syms = sym_table } }; + } + + /// Initialize gamemode, logging a possible failure. + /// If this fails, no more attempts at loading libgamemode will be made. + /// Returns true if gamemode is initialized. + pub fn init() bool { + switch (state) { + .init => return true, + .failed => return false, + .uninit => { + tryInit() catch |e| { + if (e != error.FileNotFound) { + log.warn("Loading gamemode: '{}'. Disabling libgamemode support.", .{e}); + } + state = .failed; + return false; + }; + return true; + }, + } + } + + /// Deinitializes gamemode. + pub fn deinit() void { + switch (state) { + .init => |*handle| { + handle.lib.close(); + state = .uninit; + }, + else => {}, + } + } + + /// Returns true if libgamemode has been initialized, false otherwise. + pub fn isInit() bool { + return state == .init; + } + + /// Query the status of gamemode. + /// This does blocking IO! + pub fn queryStatus() Error!Status { + if (!init()) return .inactive; + + const ret = state.init.syms.real_gamemode_query_status(); + if (ret < 0) + return error.RequestFailed; + + return @as(Status, @enumFromInt(ret)); + } + + /// Query the status of gamemode for a given PID. + /// This does blocking IO! + pub fn queryStatusFor(pid: std.os.pid_t) Error!Status { + if (!init()) return .inactive; + + const ret = state.init.syms.real_gamemode_query_status_for(pid); + return switch (ret) { + -1 => error.RequestFailed, + -2 => error.RequestRejected, + else => @as(Status, @enumFromInt(ret)), + }; + } + + /// Request starting gamemode. + /// This does blocking IO! + pub fn requestStart() Error!void { + if (!init()) return; + + const ret = state.init.syms.real_gamemode_request_start(); + if (ret < 0) + return error.RequestFailed; + } + + /// Request starting gamemode for a given PID. + /// This does blocking IO! + pub fn requestStartFor(pid: std.os.pid_t) Error!void { + if (!init()) return; + + const ret = state.init.syms.real_gamemode_request_start_for(pid); + return switch (ret) { + -1 => error.RequestFailed, + -2 => error.RequestRejected, + else => {}, + }; + } + + /// Request stopping gamemode. + /// This does blocking IO! + pub fn requestEnd() Error!void { + if (!init()) return; + + const ret = state.init.syms.real_gamemode_request_end(); + if (ret < 0) + return error.RequestFailed; + } + + /// Request stopping gamemode for a given PID. + /// This does blocking IO! + pub fn requestEndFor(pid: std.os.pid_t) Error!void { + if (!init()) return; + + const ret = state.init.syms.real_gamemode_request_end_for(pid); + return switch (ret) { + -1 => error.RequestFailed, + -2 => error.RequestRejected, + else => {}, + }; + } +}; + +const noop_impl = struct { + pub fn tryInit() LoadError!void {} + + pub fn init() bool { + return false; + } + + pub fn deinit() void {} + + pub fn isInit() bool { + return false; + } + + pub fn queryStatus() Error!Status { + return .inactive; + } + + pub fn queryStatusFor(pid: std.os.pid_t) Error!Status { + _ = pid; + return .inactive; + } + + pub fn requestStart() Error!void {} + + pub fn requestStartFor(pid: std.os.pid_t) Error!void { + _ = pid; + } + + pub fn requestEnd() Error!void {} + + pub fn requestEndFor(pid: std.os.pid_t) Error!void { + _ = pid; + } +}; diff --git a/src/main.zig b/src/main.zig index 500ecab8..5f649dc4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,6 +9,7 @@ pub const sysaudio = @import("mach-sysaudio"); // Mach standard library pub const ecs = @import("ecs/main.zig"); +pub const gamemode = @import("gamemode.zig"); pub const gfx = @import("gfx/main.zig"); pub const math = @import("math/main.zig"); pub const testing = @import("testing.zig"); @@ -30,5 +31,6 @@ test { _ = math; _ = testing; std.testing.refAllDeclsRecursive(ecs); + std.testing.refAllDeclsRecursive(gamemode); std.testing.refAllDeclsRecursive(math); }