Add config and profile autowatching + config for proton system files

This commit is contained in:
ferreo 2025-01-14 13:38:23 +00:00
parent e28c061fcf
commit 1f73aeee51
17 changed files with 580 additions and 426 deletions

View File

@ -1,3 +1,9 @@
falcond (1.0.1-101pika3) pika; urgency=low
* Add config and profile autowatching + config for proton system files
-- ferreo <ferreo@pika-os.com> Sun, 12 Jan 2025 13:48:00 +0300
falcond (1.0.0-101pika3) pika; urgency=low
* Initial release

View File

@ -34,7 +34,6 @@ pub const DBusError = error{
NetworkSubsystemFailed,
};
/// Simple DBus interface that uses busctl under the hood
pub const DBus = struct {
allocator: std.mem.Allocator,
bus_name: []const u8,

View File

@ -1,6 +1,6 @@
const std = @import("std");
const dbus = @import("dbus.zig");
const Config = @import("config.zig").Config;
const dbus = @import("./dbus.zig");
const Config = @import("../config/config.zig").Config;
pub const PowerProfiles = struct {
const PP_NAME = "org.freedesktop.UPower.PowerProfiles";
@ -9,11 +9,11 @@ pub const PowerProfiles = struct {
allocator: std.mem.Allocator,
dbus: dbus.DBus,
config: *Config,
config: Config,
original_profile: ?[]const u8,
has_performance: bool,
pub fn init(allocator: std.mem.Allocator, config: *Config) !*PowerProfiles {
pub fn init(allocator: std.mem.Allocator, config: Config) !*PowerProfiles {
var self = try allocator.create(PowerProfiles);
errdefer allocator.destroy(self);
@ -107,6 +107,7 @@ pub const PowerProfiles = struct {
if (self.dbus.getProperty("ActiveProfile")) |profile| {
defer self.allocator.free(profile);
if (!std.mem.eql(u8, profile, "performance")) {
std.log.info("Storing original power profile: {s}", .{profile});
self.original_profile = self.allocator.dupe(u8, profile) catch |err| {
std.log.err("Failed to store original power profile: {}", .{err});
return;
@ -122,6 +123,7 @@ pub const PowerProfiles = struct {
pub fn disablePerformanceMode(self: *PowerProfiles) void {
if (self.original_profile) |profile| {
std.log.info("Restoring power profile to: {s}", .{profile});
self.dbus.setProperty("ActiveProfile", profile) catch |err| {
std.log.err("Failed to restore power profile: {}", .{err});
};

View File

@ -1,5 +1,5 @@
const std = @import("std");
const dbus = @import("dbus.zig");
const dbus = @import("./dbus.zig");
pub const ScxError = dbus.DBusError;

View File

@ -14,7 +14,10 @@ var previous_mode_buffer: [10]u8 = undefined;
pub fn applyVCacheMode(vcache_mode: VCacheMode) void {
const file = fs.openFileAbsolute(vcache_path, .{ .mode = .read_write }) catch |err| switch (err) {
error.FileNotFound => return,
error.FileNotFound => {
std.log.info("AMD 3D vcache support not detected", .{});
return;
},
else => {
std.log.err("Failed to open vcache file: {}", .{err});
return;
@ -24,6 +27,7 @@ pub fn applyVCacheMode(vcache_mode: VCacheMode) void {
if (vcache_mode == .none) {
if (previous_mode) |mode| {
std.log.info("Restoring previous vcache mode: {s}", .{mode});
file.writeAll(mode) catch |err| {
std.log.err("Failed to restore previous vcache mode: {}", .{err});
};
@ -50,6 +54,8 @@ pub fn applyVCacheMode(vcache_mode: VCacheMode) void {
.cache => "cache",
.none => unreachable,
};
std.log.info("Setting vcache mode to: {s}", .{mode_str});
file.writeAll(mode_str) catch |err| {
std.log.err("Failed to write vcache mode: {}", .{err});
};

View File

@ -1,51 +0,0 @@
const std = @import("std");
const fs = std.fs;
const confloader = @import("confloader.zig");
const vcache_setting = @import("vcache_setting.zig");
const scx_scheds = @import("scx_scheds.zig");
pub const Config = struct {
enable_performance_mode: bool = true,
scx_sched: scx_scheds.ScxScheduler = .none,
scx_sched_props: ?scx_scheds.ScxSchedModes = null,
vcache_mode: vcache_setting.VCacheMode = .none,
pub fn load(allocator: std.mem.Allocator) !Config {
const config_path = "/etc/falcond/config.conf";
var config = Config{};
const file = fs.openFileAbsolute(config_path, .{}) catch |err| switch (err) {
error.FileNotFound => {
try config.save();
return config;
},
else => return err,
};
defer file.close();
config = try confloader.loadConf(Config, allocator, config_path);
return config;
}
pub fn save(self: Config) !void {
const config_dir = "/etc/falcond/";
fs.makeDirAbsolute(config_dir) catch |err| switch (err) {
error.PathAlreadyExists => {},
else => return err,
};
var file_buf: [fs.max_path_bytes]u8 = undefined;
const config_path = try std.fmt.bufPrint(
&file_buf,
"{s}/config.conf",
.{config_dir},
);
const file = try fs.createFileAbsolute(config_path, .{});
defer file.close();
try file.writer().print("enable_performance_mode = {}\n", .{self.enable_performance_mode});
try file.writer().print("scx_sched = {s}\n", .{@tagName(self.scx_sched)});
try file.writer().print("vcache_mode = {s}\n", .{@tagName(self.vcache_mode)});
}
};

View File

@ -0,0 +1,92 @@
const std = @import("std");
const fs = std.fs;
const confloader = @import("confloader.zig");
const vcache_setting = @import("../clients/vcache_setting.zig");
const scx_scheds = @import("../clients/scx_scheds.zig");
const default_system_processes = [_][]const u8{
"steam.exe",
"services.exe",
"winedevice.exe",
"plugplay.exe",
"svchost.exe",
"explorer.exe",
"rpcss.exe",
"tabtip.exe",
"wineboot.exe",
"rundll32.exe",
"iexplore.exe",
"conhost.exe",
"crashpad_handler.exe",
"iscriptevaluator.exe",
"VC_redist.x86.exe",
"VC_redist.x64.exe",
"cmd.exe",
"REDEngineErrorReporter.exe",
"REDprelauncher.exe",
"SteamService.exe",
"UnityCrashHandler64.exe",
"start.exe",
};
pub const Config = struct {
enable_performance_mode: bool = true,
scx_sched: scx_scheds.ScxScheduler = .none,
scx_sched_props: ?scx_scheds.ScxSchedModes = null,
vcache_mode: vcache_setting.VCacheMode = .none,
system_processes: []const []const u8 = &default_system_processes,
arena: ?std.heap.ArenaAllocator = null,
pub fn load(allocator: std.mem.Allocator, path: []const u8) !Config {
var config = Config{};
const file = fs.openFileAbsolute(path, .{}) catch |err| switch (err) {
error.FileNotFound => {
try config.save(path);
return config;
},
else => return err,
};
defer file.close();
var arena = std.heap.ArenaAllocator.init(allocator);
errdefer arena.deinit();
config = try confloader.loadConf(Config, arena.allocator(), path);
config.arena = arena;
return config;
}
pub fn deinit(self: *Config) void {
if (self.arena) |*arena| {
arena.deinit();
self.arena = null;
}
}
pub fn save(self: Config, path: []const u8) !void {
var file_buf: [fs.max_path_bytes]u8 = undefined;
const config_dir = try std.fmt.bufPrint(
&file_buf,
"{s}/",
.{std.fs.path.dirname(path).?},
);
fs.makeDirAbsolute(config_dir) catch |err| switch (err) {
error.PathAlreadyExists => {},
else => return err,
};
const file = try fs.createFileAbsolute(path, .{});
defer file.close();
try file.writer().print("enable_performance_mode = {}\n", .{self.enable_performance_mode});
try file.writer().print("scx_sched = {s}\n", .{@tagName(self.scx_sched)});
try file.writer().print("vcache_mode = {s}\n", .{@tagName(self.vcache_mode)});
try file.writer().print("system_processes = [\n", .{});
for (self.system_processes) |proc| {
try file.writer().print(" \"{s}\",\n", .{proc});
}
try file.writer().print("]\n", .{});
}
};

View File

@ -142,6 +142,43 @@ pub fn Parser(comptime T: type) type {
return error.InvalidSyntax;
}
fn parseStringArray(self: *Self) ![]const []const u8 {
if (self.pos >= self.content.len or self.content[self.pos] != '[')
return error.InvalidSyntax;
self.pos += 1;
var values = std.ArrayList([]const u8).init(self.allocator);
errdefer {
for (values.items) |str| {
self.allocator.free(str);
}
values.deinit();
}
while (self.pos < self.content.len) {
self.skipWhitespace();
if (self.content[self.pos] == ']') {
self.pos += 1;
return values.toOwnedSlice();
}
const str = try self.parseString();
try values.append(str);
self.skipWhitespace();
if (self.content[self.pos] == ',') {
self.pos += 1;
continue;
}
if (self.content[self.pos] == ']') {
self.pos += 1;
return values.toOwnedSlice();
}
return error.InvalidSyntax;
}
return error.InvalidSyntax;
}
fn parseIdentifier(self: *Self) ![]const u8 {
const start = self.pos;
const v_size = std.simd.suggestVectorLength(u8) orelse 32;
@ -259,6 +296,9 @@ pub fn Parser(comptime T: type) type {
i64 => {
@field(result, field.name) = try self.parseArray();
},
[]const u8 => {
@field(result, field.name) = try self.parseStringArray();
},
else => return error.InvalidSyntax,
}
},

View File

@ -1,34 +1,150 @@
const std = @import("std");
const ProfileManager = @import("profile.zig").ProfileManager;
const Profile = @import("profile.zig").Profile;
const Config = @import("config.zig").Config;
const ProfileManager = @import("profile/manager.zig").ProfileManager;
const Profile = @import("profile/types.zig").Profile;
const ProfileLoader = @import("profile/loader.zig");
const Config = @import("config/config.zig").Config;
const linux = std.os.linux;
const posix = std.posix;
const PowerProfiles = @import("power_profiles.zig").PowerProfiles;
const scx_scheds = @import("scx_scheds.zig");
const PowerProfiles = @import("clients/power_profiles.zig").PowerProfiles;
const scx_scheds = @import("clients/scx_scheds.zig");
pub const Daemon = struct {
allocator: std.mem.Allocator,
config_path: []const u8,
profile_manager: ProfileManager,
oneshot: bool,
known_pids: ?std.AutoHashMap(u32, *const Profile),
power_profiles: *PowerProfiles,
performance_mode: bool,
last_profiles_check: i128,
last_config_check: i128,
config: Config,
const Self = @This();
pub fn init(allocator: std.mem.Allocator, config: ?*Config, oneshot: bool, power_profiles: *PowerProfiles) !Self {
var profile_manager = ProfileManager.init(allocator, power_profiles, config.?);
try profile_manager.loadProfiles(oneshot);
pub fn init(allocator: std.mem.Allocator, config_path: []const u8, oneshot: bool) !*Self {
const config_path_owned = try allocator.dupe(u8, config_path);
errdefer allocator.free(config_path_owned);
const config = try Config.load(allocator, config_path);
const power_profiles = try PowerProfiles.init(allocator, config);
errdefer power_profiles.deinit();
const performance_mode = power_profiles.isPerformanceAvailable();
if (performance_mode) {
std.log.info("Performance profile available - power profile management enabled", .{});
}
var profile_manager = ProfileManager.init(allocator, power_profiles, config);
try ProfileLoader.loadProfiles(allocator, &profile_manager.profiles, &profile_manager.proton_profile, oneshot);
const current_time = std.time.nanoTimestamp();
try scx_scheds.init(allocator);
return Self{
const self = try allocator.create(Self);
self.* = .{
.allocator = allocator,
.profile_manager = profile_manager,
.oneshot = oneshot,
.known_pids = null,
.known_pids = if (!oneshot) std.AutoHashMap(u32, *const Profile).init(allocator) else null,
.power_profiles = power_profiles,
.last_profiles_check = current_time,
.last_config_check = current_time,
.config = config,
.config_path = config_path_owned,
.performance_mode = performance_mode,
};
return self;
}
pub fn deinit(self: *Self) void {
scx_scheds.deinit();
self.profile_manager.deinit();
self.power_profiles.deinit();
if (self.known_pids) |*map| {
map.deinit();
}
self.config.deinit();
self.allocator.free(self.config_path);
self.allocator.destroy(self);
}
fn reloadConfig(self: *Self) !void {
if (self.known_pids) |*map| {
map.clearRetainingCapacity();
}
const config = try Config.load(self.allocator, self.config_path);
const power_profiles = try PowerProfiles.init(self.allocator, config);
self.power_profiles.deinit();
self.config.deinit();
self.config = config;
self.power_profiles = power_profiles;
self.performance_mode = self.power_profiles.isPerformanceAvailable();
var profile_manager = ProfileManager.init(self.allocator, power_profiles, config);
try ProfileLoader.loadProfiles(self.allocator, &profile_manager.profiles, &profile_manager.proton_profile, self.oneshot);
self.profile_manager.deinit();
self.profile_manager = profile_manager;
}
fn checkConfigChanges(self: *Self) !bool {
const stat = try std.fs.cwd().statFile(self.config_path);
const mtime = @as(i128, @intCast(stat.mtime));
if (mtime > self.last_config_check) {
std.log.info("Config file changed, reloading profiles", .{});
self.last_config_check = std.time.nanoTimestamp();
return true;
}
return false;
}
fn checkProfilesChanges(self: *Self) !bool {
var dir = try std.fs.cwd().openDir(self.profile_manager.profiles_dir, .{ .iterate = true });
defer dir.close();
var latest_mtime: i128 = self.last_profiles_check;
var file_count: usize = 0;
var iter = dir.iterate();
while (try iter.next()) |entry| {
if (entry.kind == .file) {
file_count += 1;
const stat = try dir.statFile(entry.name);
const mtime = @as(i128, @intCast(stat.mtime));
if (mtime > latest_mtime) {
latest_mtime = mtime;
}
}
}
if (latest_mtime > self.last_profiles_check or file_count != self.profile_manager.profiles.items.len) {
std.log.info("Profile changes detected, reloading profiles", .{});
self.last_profiles_check = std.time.nanoTimestamp();
return true;
}
return false;
}
pub fn run(self: *Self) !void {
if (self.oneshot) {
try self.handleProcesses();
return;
}
while (true) {
const config_changed = try self.checkConfigChanges();
const profiles_changed = try self.checkProfilesChanges();
if (config_changed or profiles_changed) {
try self.reloadConfig();
}
try self.handleProcesses();
std.time.sleep(std.time.ns_per_s * 9);
}
}
fn scanProcesses(allocator: std.mem.Allocator) !std.AutoHashMap(u32, []const u8) {
@ -91,7 +207,7 @@ pub const Daemon = struct {
return try allocator.dupe(u8, exe_name);
}
pub fn checkProcesses(self: *Self) !void {
fn handleProcesses(self: *Self) !void {
var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit();
const arena_allocator = arena.allocator();
@ -113,14 +229,16 @@ pub const Daemon = struct {
if (!self.oneshot) {
if (self.known_pids) |*known| {
if (!known.contains(pid)) {
if (try self.profile_manager.matchProcess(arena.allocator(), try std.fmt.allocPrint(arena_allocator, "{d}", .{pid}), process_name)) |profile| {
if (try self.profile_manager.matchProcess(arena_allocator, try std.fmt.allocPrint(arena_allocator, "{d}", .{pid}), process_name)) |profile| {
try known.put(pid, profile);
try self.profile_manager.activateProfile(profile);
}
}
}
} else {
try self.handleProcess(try std.fmt.allocPrint(arena_allocator, "{d}", .{pid}), process_name);
if (try self.profile_manager.matchProcess(arena_allocator, try std.fmt.allocPrint(arena_allocator, "{d}", .{pid}), process_name)) |profile| {
try self.profile_manager.activateProfile(profile);
}
}
}
@ -130,60 +248,7 @@ pub const Daemon = struct {
while (known_it.next()) |entry| {
const pid = entry.key_ptr.*;
if (!processes.contains(pid)) {
try self.handleProcessExit(try std.fmt.allocPrint(arena_allocator, "{d}", .{pid}));
}
}
}
}
}
pub fn run(self: *Self) !void {
if (!self.oneshot) {
self.known_pids = std.AutoHashMap(u32, *const Profile).init(self.allocator);
}
try self.checkProcesses();
if (self.oneshot) {
return;
}
while (true) {
try self.checkProcesses();
std.time.sleep(std.time.ns_per_s * 3);
}
}
pub fn deinit(self: *Self) void {
scx_scheds.deinit();
if (self.known_pids) |*pids| {
pids.deinit();
}
self.profile_manager.deinit();
}
pub fn handleProcess(self: *Self, pid: []const u8, process_name: []const u8) !void {
var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit();
if (try self.profile_manager.matchProcess(arena.allocator(), pid, process_name)) |profile| {
if (!self.oneshot) {
if (self.known_pids) |*known| {
try known.put(try std.fmt.parseInt(u32, pid, 10), profile);
}
}
try self.profile_manager.activateProfile(profile);
}
}
pub fn handleProcessExit(self: *Self, pid: []const u8) !void {
if (self.known_pids) |*pids| {
const pid_num = std.fmt.parseInt(u32, pid, 10) catch |err| {
std.log.warn("Failed to parse PID: {}", .{err});
return;
};
if (pids.get(pid_num)) |profile| {
const profile = entry.value_ptr.*;
var found_profile = false;
if (self.profile_manager.active_profile) |active| {
@ -204,7 +269,9 @@ pub const Daemon = struct {
}
}
_ = pids.remove(pid_num);
_ = known.remove(pid);
}
}
}
}
}

View File

@ -1,7 +1,5 @@
const std = @import("std");
const Daemon = @import("daemon.zig").Daemon;
const Config = @import("config.zig").Config;
const PowerProfiles = @import("power_profiles.zig").PowerProfiles;
pub const std_options = std.Options{
.log_level = .debug,
@ -80,16 +78,6 @@ pub fn main() !void {
},
};
var config = try Config.load(allocator);
var power_profiles = try PowerProfiles.init(allocator, &config);
defer power_profiles.deinit();
if (!power_profiles.isPerformanceAvailable()) {
std.log.warn("Performance profile not available - power profile management disabled", .{});
} else {
std.log.info("Performance profile available - power profile management enabled", .{});
}
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
@ -97,7 +85,7 @@ pub fn main() !void {
if (std.mem.eql(u8, arg, "--oneshot")) break true;
} else false;
var daemon = try Daemon.init(allocator, &config, oneshot, power_profiles);
var daemon = try Daemon.init(allocator, "/etc/falcond/config.conf", oneshot);
defer daemon.deinit();
try daemon.run();

View File

@ -1,270 +0,0 @@
const std = @import("std");
const fs = std.fs;
const confloader = @import("confloader.zig");
const PowerProfiles = @import("power_profiles.zig").PowerProfiles;
const Config = @import("config.zig").Config;
const vcache_setting = @import("vcache_setting.zig");
const scx_scheds = @import("scx_scheds.zig");
const Child = std.process.Child;
const linux = std.os.linux;
const CPU_SETSIZE = 1024;
const CPU_SET = extern struct {
bits: [CPU_SETSIZE / 64]u64,
};
pub const Profile = struct {
const LscpuCoreStrategy = enum { HighestFreq, Sequential };
name: []const u8,
performance_mode: bool = false,
scx_sched: scx_scheds.ScxScheduler = .none,
scx_sched_props: ?scx_scheds.ScxSchedModes = null,
vcache_mode: vcache_setting.VCacheMode = .cache,
pub fn matches(self: *const Profile, process_name: []const u8) bool {
const is_match = std.ascii.eqlIgnoreCase(self.name, process_name);
if (is_match) {
std.log.info("Found match: {s} for process {s}", .{ self.name, process_name });
}
return is_match;
}
};
const CacheEntry = struct {
pid: u32,
timestamp: i64,
is_proton: bool,
};
pub const ProfileManager = struct {
allocator: std.mem.Allocator,
profiles: std.ArrayList(Profile),
proton_profile: ?*const Profile,
active_profile: ?*const Profile = null,
queued_profiles: std.ArrayList(*const Profile),
power_profiles: *PowerProfiles,
config: *const Config,
// Don't match Wine/Proton infrastructure
const system_processes = [_][]const u8{
"steam.exe",
"services.exe",
"winedevice.exe",
"plugplay.exe",
"svchost.exe",
"explorer.exe",
"rpcss.exe",
"tabtip.exe",
"wineboot.exe",
"rundll32.exe",
"iexplore.exe",
"conhost.exe",
"crashpad_handler.exe",
"iscriptevaluator.exe",
"VC_redist.x86.exe",
"VC_redist.x64.exe",
"cmd.exe",
"REDEngineErrorReporter.exe",
"REDprelauncher.exe",
"SteamService.exe",
"UnityCrashHandler64.exe",
"start.exe",
};
pub fn init(allocator: std.mem.Allocator, power_profiles: *PowerProfiles, config: *const Config) ProfileManager {
return .{
.allocator = allocator,
.profiles = std.ArrayList(Profile).init(allocator),
.proton_profile = null,
.queued_profiles = std.ArrayList(*const Profile).init(allocator),
.power_profiles = power_profiles,
.config = config,
};
}
pub fn activateProfile(self: *ProfileManager, profile: *const Profile) !void {
if (self.active_profile == null) {
std.log.info("Activating profile: {s}", .{profile.name});
self.active_profile = profile;
if (profile.performance_mode and self.power_profiles.isPerformanceAvailable()) {
std.log.info("Enabling performance mode for profile: {s}", .{profile.name});
self.power_profiles.enablePerformanceMode();
}
const effective_mode = if (self.config.vcache_mode != .none)
self.config.vcache_mode
else
profile.vcache_mode;
vcache_setting.applyVCacheMode(effective_mode);
const effective_sched = if (self.config.scx_sched != .none)
self.config.scx_sched
else
profile.scx_sched;
const effective_scx_mode = if (self.config.scx_sched != .none)
self.config.scx_sched_props
else
profile.scx_sched_props;
scx_scheds.applyScheduler(self.allocator, effective_sched, effective_scx_mode);
} else {
std.log.info("Queueing profile: {s} (active: {s})", .{ profile.name, self.active_profile.?.name });
try self.queued_profiles.append(profile);
}
}
pub fn deactivateProfile(self: *ProfileManager, profile: *const Profile) !void {
if (self.active_profile == profile) {
std.log.info("Deactivating profile: {s}", .{profile.name});
self.active_profile = null;
if (profile.performance_mode) {
std.log.info("Disabling performance mode for profile: {s}", .{profile.name});
self.power_profiles.disablePerformanceMode();
}
vcache_setting.applyVCacheMode(.none);
scx_scheds.restorePreviousState(self.allocator);
if (self.queued_profiles.items.len > 0) {
const next_profile = self.queued_profiles.orderedRemove(0);
std.log.info("Activating next queued profile: {s}", .{next_profile.name});
try self.activateProfile(next_profile);
}
} else {
for (self.queued_profiles.items, 0..) |queued, i| {
if (queued == profile) {
std.log.info("Removing queued profile: {s}", .{profile.name});
_ = self.queued_profiles.orderedRemove(i);
break;
}
}
}
}
pub fn loadProfiles(self: *ProfileManager, oneshot: bool) !void {
if (oneshot) {
try self.profiles.append(Profile{
.name = try self.allocator.dupe(u8, "Hades3.exe"),
});
std.log.info("Loaded oneshot profile: Hades3.exe", .{});
try self.profiles.append(Profile{
.name = try self.allocator.dupe(u8, "Proton"),
});
std.log.info("Loaded oneshot profile: Proton", .{});
self.proton_profile = &self.profiles.items[1];
} else {
var profiles = try confloader.loadConfDir(Profile, self.allocator, "/usr/share/falcond/profiles");
defer profiles.deinit();
try self.profiles.appendSlice(profiles.items);
for (self.profiles.items) |*profile| {
if (std.mem.eql(u8, profile.name, "Proton")) {
self.proton_profile = profile;
std.log.info("Found Proton profile: {s}", .{profile.name});
break;
}
}
std.log.info("Loaded {d} profiles", .{self.profiles.items.len});
}
}
fn isProtonParent(_: *const ProfileManager, arena: std.mem.Allocator, pid: []const u8) !bool {
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const status_path = try std.fmt.bufPrint(&path_buf, "/proc/{s}/status", .{pid});
const file = std.fs.openFileAbsolute(status_path, .{}) catch |err| {
std.log.debug("Failed to open {s}: {}", .{ status_path, err });
return switch (err) {
error.AccessDenied, error.FileNotFound => false,
else => err,
};
};
defer file.close();
const content = try file.readToEndAlloc(arena, std.math.maxInt(usize));
const ppid_line = std.mem.indexOf(u8, content, "PPid:") orelse return false;
const line_end = std.mem.indexOfScalarPos(u8, content, ppid_line, '\n') orelse content.len;
const ppid_start = ppid_line + 5; // Length of "PPid:"
const ppid = std.mem.trim(u8, content[ppid_start..line_end], " \t");
const parent_cmdline_path = try std.fmt.bufPrint(&path_buf, "/proc/{s}/cmdline", .{ppid});
const parent_file = std.fs.openFileAbsolute(parent_cmdline_path, .{}) catch |err| {
std.log.debug("Failed to open parent cmdline {s}: {}", .{ parent_cmdline_path, err });
return switch (err) {
error.AccessDenied, error.FileNotFound => false,
else => err,
};
};
defer parent_file.close();
const parent_content = try parent_file.readToEndAlloc(arena, std.math.maxInt(usize));
return std.mem.indexOf(u8, parent_content, "proton") != null;
}
fn isProtonGame(self: *ProfileManager, arena: std.mem.Allocator, pid: []const u8, process_name: []const u8) !bool {
if (!std.mem.endsWith(u8, process_name, ".exe")) return false;
for (system_processes) |sys_proc| {
if (std.mem.eql(u8, process_name, sys_proc)) {
return false;
}
}
return try self.isProtonParent(arena, pid);
}
pub fn matchProcess(self: *ProfileManager, arena: std.mem.Allocator, pid: []const u8, process_name: []const u8) !?*const Profile {
const is_exe = std.mem.endsWith(u8, process_name, ".exe");
var match: ?*const Profile = null;
for (self.profiles.items) |*profile| {
const is_match = profile != self.proton_profile and profile.matches(process_name);
if (is_match) {
std.log.info("Matched profile {s} for process {s}", .{ profile.name, process_name });
match = profile;
break;
}
}
const should_check_proton = match == null and
is_exe and
self.proton_profile != null;
if (should_check_proton) {
const is_system = for (system_processes) |sys_proc| {
if (std.mem.eql(u8, process_name, sys_proc)) break true;
} else false;
if (!is_system) {
const is_proton = try self.isProtonParent(arena, pid);
if (is_proton) {
std.log.info("Found Proton game: {s}", .{process_name});
match = self.proton_profile;
}
}
}
return match;
}
pub fn deinit(self: *ProfileManager) void {
if (self.active_profile) |profile| {
if (profile.performance_mode) {
self.power_profiles.disablePerformanceMode();
}
}
for (self.profiles.items) |*profile| {
self.allocator.free(profile.name);
}
self.queued_profiles.deinit();
self.profiles.deinit();
}
};

View File

@ -0,0 +1,37 @@
const std = @import("std");
const confloader = @import("../config/confloader.zig");
const types = @import("types.zig");
const Profile = types.Profile;
pub fn loadProfiles(allocator: std.mem.Allocator, profiles: *std.ArrayList(Profile), proton_profile: *?*const Profile, oneshot: bool) !void {
if (oneshot) {
try profiles.append(Profile{
.name = try allocator.dupe(u8, "Hades3.exe"),
.scx_sched = .bpfland,
});
std.log.info("Loaded oneshot profile: Hades3.exe", .{});
try profiles.append(Profile{
.name = try allocator.dupe(u8, "Proton"),
.scx_sched = .none,
});
std.log.info("Loaded oneshot profile: Proton", .{});
proton_profile.* = &profiles.items[1];
} else {
var loaded_profiles = try confloader.loadConfDir(Profile, allocator, "/usr/share/falcond/profiles");
defer loaded_profiles.deinit();
try profiles.appendSlice(loaded_profiles.items);
for (profiles.items) |*profile| {
if (std.mem.eql(u8, profile.name, "Proton")) {
proton_profile.* = profile;
std.log.info("Found Proton profile: {s}", .{profile.name});
break;
}
}
std.log.info("Loaded {d} profiles", .{profiles.items.len});
}
}

View File

@ -0,0 +1,103 @@
const std = @import("std");
const types = @import("types.zig");
const matcher = @import("matcher.zig");
const Profile = types.Profile;
const PowerProfiles = @import("../clients/power_profiles.zig").PowerProfiles;
const Config = @import("../config/config.zig").Config;
const vcache_setting = @import("../clients/vcache_setting.zig");
const scx_scheds = @import("../clients/scx_scheds.zig");
pub const ProfileManager = struct {
comptime profiles_dir: []const u8 = "/usr/share/falcond/profiles",
allocator: std.mem.Allocator,
profiles: std.ArrayList(Profile),
proton_profile: ?*const Profile,
active_profile: ?*const Profile = null,
queued_profiles: std.ArrayList(*const Profile),
power_profiles: *PowerProfiles,
config: Config,
pub fn init(allocator: std.mem.Allocator, power_profiles: *PowerProfiles, config: Config) ProfileManager {
return .{
.allocator = allocator,
.profiles = std.ArrayList(Profile).init(allocator),
.proton_profile = null,
.queued_profiles = std.ArrayList(*const Profile).init(allocator),
.power_profiles = power_profiles,
.config = config,
};
}
pub fn deinit(self: *ProfileManager) void {
if (self.active_profile) |profile| {
self.deactivateProfile(profile) catch |err| {
std.log.err("Failed to deactivate profile: {}", .{err});
};
}
for (self.profiles.items) |profile| {
self.allocator.free(profile.name);
}
self.profiles.deinit();
self.queued_profiles.deinit();
}
pub fn activateProfile(self: *ProfileManager, profile: *const Profile) !void {
if (self.active_profile == null) {
std.log.info("Activating profile: {s}", .{profile.name});
self.active_profile = profile;
if (profile.performance_mode and self.power_profiles.isPerformanceAvailable()) {
std.log.info("Enabling performance mode for profile: {s}", .{profile.name});
self.power_profiles.enablePerformanceMode();
}
const effective_mode = if (self.config.vcache_mode != .none)
self.config.vcache_mode
else
profile.vcache_mode;
vcache_setting.applyVCacheMode(effective_mode);
const effective_sched = if (self.config.scx_sched == .none)
profile.scx_sched
else
self.config.scx_sched;
const effective_scx_mode = if (self.config.scx_sched == .none)
profile.scx_sched_props
else
self.config.scx_sched_props;
scx_scheds.applyScheduler(self.allocator, effective_sched, effective_scx_mode);
} else {
std.log.info("Queueing profile: {s} (active: {s})", .{ profile.name, self.active_profile.?.name });
try self.queued_profiles.append(profile);
}
}
pub fn deactivateProfile(self: *ProfileManager, profile: *const Profile) !void {
if (self.active_profile) |active| {
if (active == profile) {
std.log.info("Deactivating profile: {s}", .{profile.name});
if (profile.performance_mode and self.power_profiles.isPerformanceAvailable()) {
std.log.info("Disabling performance mode for profile: {s}", .{profile.name});
self.power_profiles.disablePerformanceMode();
}
vcache_setting.applyVCacheMode(.none);
try scx_scheds.deactivateScheduler(self.allocator);
self.active_profile = null;
if (self.queued_profiles.items.len > 0) {
const next_profile = self.queued_profiles.orderedRemove(0);
try self.activateProfile(next_profile);
}
}
}
}
pub fn matchProcess(self: *ProfileManager, arena: std.mem.Allocator, pid: []const u8, process_name: []const u8) !?*const Profile {
return matcher.matchProcess(self.profiles.items, self.proton_profile, arena, pid, process_name, self.config);
}
};

View File

@ -0,0 +1,110 @@
const std = @import("std");
const types = @import("types.zig");
const Profile = types.Profile;
const Config = @import("../config/config.zig").Config;
// Don't match Wine/Proton infrastructure
// const system_processes = [_][]const u8{
// "steam.exe",
// "services.exe",
// "winedevice.exe",
// "plugplay.exe",
// "svchost.exe",
// "explorer.exe",
// "rpcss.exe",
// "tabtip.exe",
// "wineboot.exe",
// "rundll32.exe",
// "iexplore.exe",
// "conhost.exe",
// "crashpad_handler.exe",
// "iscriptevaluator.exe",
// "VC_redist.x86.exe",
// "VC_redist.x64.exe",
// "cmd.exe",
// "REDEngineErrorReporter.exe",
// "REDprelauncher.exe",
// "SteamService.exe",
// "UnityCrashHandler64.exe",
// "start.exe",
// };
pub fn isProtonParent(arena: std.mem.Allocator, pid: []const u8) !bool {
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const status_path = try std.fmt.bufPrint(&path_buf, "/proc/{s}/status", .{pid});
const file = std.fs.openFileAbsolute(status_path, .{}) catch |err| {
std.log.debug("Failed to open {s}: {}", .{ status_path, err });
return switch (err) {
error.AccessDenied, error.FileNotFound => false,
else => err,
};
};
defer file.close();
const content = try file.readToEndAlloc(arena, std.math.maxInt(usize));
const ppid_line = std.mem.indexOf(u8, content, "PPid:") orelse return false;
const line_end = std.mem.indexOfScalarPos(u8, content, ppid_line, '\n') orelse content.len;
const ppid_start = ppid_line + 5; // Length of "PPid:"
const ppid = std.mem.trim(u8, content[ppid_start..line_end], " \t");
const parent_cmdline_path = try std.fmt.bufPrint(&path_buf, "/proc/{s}/cmdline", .{ppid});
const parent_file = std.fs.openFileAbsolute(parent_cmdline_path, .{}) catch |err| {
std.log.debug("Failed to open parent cmdline {s}: {}", .{ parent_cmdline_path, err });
return switch (err) {
error.AccessDenied, error.FileNotFound => false,
else => err,
};
};
defer parent_file.close();
const parent_content = try parent_file.readToEndAlloc(arena, std.math.maxInt(usize));
return std.mem.indexOf(u8, parent_content, "proton") != null;
}
pub fn isProtonGame(process_name: []const u8, config: Config) bool {
if (!std.mem.endsWith(u8, process_name, ".exe")) return false;
for (config.system_processes) |sys_proc| {
if (std.mem.eql(u8, process_name, sys_proc)) {
return false;
}
}
return true;
}
pub fn matchProcess(profiles: []Profile, proton_profile: ?*const Profile, arena: std.mem.Allocator, pid: []const u8, process_name: []const u8, config: Config) !?*const Profile {
const is_exe = std.mem.endsWith(u8, process_name, ".exe");
var match: ?*const Profile = null;
for (profiles) |*profile| {
const is_match = profile != proton_profile and profile.matches(process_name);
if (is_match) {
std.log.info("Matched profile {s} for process {s}", .{ profile.name, process_name });
match = profile;
break;
}
}
const should_check_proton = match == null and
is_exe and
proton_profile != null;
if (should_check_proton) {
const is_system = for (config.system_processes) |sys_proc| {
if (std.mem.eql(u8, process_name, sys_proc)) break true;
} else false;
if (!is_system) {
const is_proton = try isProtonParent(arena, pid);
if (is_proton) {
std.log.info("Found Proton game: {s}", .{process_name});
match = proton_profile;
}
}
}
return match;
}

View File

@ -0,0 +1,25 @@
const std = @import("std");
const vcache_setting = @import("../clients/vcache_setting.zig");
const scx_scheds = @import("../clients/scx_scheds.zig");
pub const Profile = struct {
name: []const u8,
performance_mode: bool = false,
scx_sched: scx_scheds.ScxScheduler = .none,
scx_sched_props: ?scx_scheds.ScxSchedModes = null,
vcache_mode: vcache_setting.VCacheMode = .cache,
pub fn matches(self: *const Profile, process_name: []const u8) bool {
const is_match = std.ascii.eqlIgnoreCase(self.name, process_name);
if (is_match) {
std.log.info("Found match: {s} for process {s}", .{ self.name, process_name });
}
return is_match;
}
};
pub const CacheEntry = struct {
pid: u32,
timestamp: i64,
is_proton: bool,
};

View File

@ -2,7 +2,7 @@
set -e
VERSION="1.0.0"
VERSION="1.0.1"
source ./pika-build-config.sh