diff --git a/flake.lock b/flake.lock index 8f2f2e35d..5999137c9 100644 --- a/flake.lock +++ b/flake.lock @@ -1,26 +1,6 @@ { "nodes": { - "nixpkgs": { - "locked": { - "lastModified": 1751290770, - "narHash": "sha256-u4s8yKAqTzPGY3vTcDyAIet11uXaNCM//93/0O0NlbA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "0620a50e9a847851bf802c59a4202552ed79b821", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-25.05-small", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "nixpkgs": "nixpkgs" - } - } + "root": {} }, "root": "root", "version": 7 diff --git a/flake.nix b/flake.nix index a0ac1fb81..b7a44a874 100644 --- a/flake.nix +++ b/flake.nix @@ -139,6 +139,7 @@ gigabyte-b650 = import ./gigabyte/b650; gmktec-nucbox-g3-plus = import ./gmktec/nucbox/g3-plus; google-pixelbook = import ./google/pixelbook; + gpd-duo = import ./gpd/duo; gpd-micropc = import ./gpd/micropc; gpd-p2-max = import ./gpd/p2-max; gpd-pocket-3 = import ./gpd/pocket-3; diff --git a/gpd/duo/default.nix b/gpd/duo/default.nix new file mode 100644 index 000000000..eb4aa73a3 --- /dev/null +++ b/gpd/duo/default.nix @@ -0,0 +1,60 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.hardware.gpd.duo; +in +with lib; +{ + imports = [ + ./duo-specific + ../../common/pc/laptop + ../../common/pc/ssd + ../../common/hidpi.nix + ]; + + options = { + hardware.gpd.duo.preventWakeOnAC = mkOption { + type = types.bool; + default = false; + description = '' + Stop the system waking from suspend when the AC is plugged + in. The catch: it also disables waking from the keyboard. + + See: + https://community.frame.work/t/tracking-framework-amd-ryzen-7040-series-lid-wakeup-behavior-feedback/39128/45 + ''; + }; + }; + + config = { + # Workaround applied upstream in Linux >=6.7 (on BIOS 03.03) + # https://github.com/torvalds/linux/commit/a55bdad5dfd1efd4ed9ffe518897a21ca8e4e193 + services.udev.extraRules = + mkIf (versionOlder config.boot.kernelPackages.kernel.version "6.7" && cfg.preventWakeOnAC) + '' + # Prevent wake when plugging in AC during suspend. Trade-off: keyboard wake disabled. See: + # https://community.frame.work/t/tracking-framework-amd-ryzen-7040-series-lid-wakeup-behavior-feedback/39128/45 + ACTION=="add", SUBSYSTEM=="serio", DRIVERS=="atkbd", ATTR{power/wakeup}="disabled" + ''; + + # Replace 'left' with 'right' or 'inverted' as needed + # Fixes DUO stupid inverted display at boot + # Enable kernel module for your graphics (adjust if needed) + boot.kernelModules = [ "amdgpu" ]; + + # Set the eDP-1 panel video parameters for display rotation + boot.kernelParams = mkBefore [ + "video=eDP-1:2880x1800,panel_orientation=upside_down" + "video=DP-3:2880x1800" + "i2c_touchscreen_props=GXTP7380:touchscreen-inverted-x:touchscreen-inverted-y" + ]; + + hardware.gpd.duo.audioEnhancement.rawDeviceName = + mkDefault "alsa_output.pci-0000_c1_00.6.analog-stereo"; + }; +} diff --git a/gpd/duo/duo-specific/amd.nix b/gpd/duo/duo-specific/amd.nix new file mode 100644 index 000000000..4c1c43b51 --- /dev/null +++ b/gpd/duo/duo-specific/amd.nix @@ -0,0 +1,28 @@ +{ lib, config, ... }: +with lib; +{ + imports = [ + ../../../common/cpu/amd + ../../../common/cpu/amd/pstate.nix + ../../../common/gpu/amd + ]; + + boot.kernelParams = + [ + # There seems to be an issue with panel self-refresh (PSR) that + # causes hangs for users. + # + # https://community.frame.work/t/fedora-kde-becomes-suddenly-slow/58459 + # https://gitlab.freedesktop.org/drm/amd/-/issues/3647 + "amdgpu.dcdebugmask=0x210" + ] + # Workaround for SuspendThenHibernate: https://lore.kernel.org/linux-kernel/20231106162310.85711-1-mario.limonciello@amd.com/ + ++ optionals (versionOlder config.boot.kernelPackages.kernel.version "6.8") [ + "rtc_cmos.use_acpi_alarm=1" + ]; + + # AMD has better battery life with PPD over TLP: + # https://community.frame.work/t/responded-amd-7040-sleep-states/38101/13 + services.power-profiles-daemon.enable = mkDefault true; + services.tlp.enable = mkDefault false; +} diff --git a/gpd/duo/duo-specific/audio.nix b/gpd/duo/duo-specific/audio.nix new file mode 100644 index 000000000..d384c2730 --- /dev/null +++ b/gpd/duo/duo-specific/audio.nix @@ -0,0 +1,391 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.hardware.gpd.duo.audioEnhancement; +in +with lib; +{ + options = { + hardware.gpd.duo.audioEnhancement = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Create a new audio device called "DUO Speakers", + which applies sound tuning before sending the audio out to the speakers. + This option requires PipeWire and WirePlumber. + + The filter chain includes the following: + - Pyschoacoustic bass enhancement + - Loudness compensation + - Equalizer + - Slight compression + + This option has been optimised for the Framework Laptop 13 AMD 7040 series, but should work on all models. + + Before applying, ensure the speakers are set to 100%, + because the volumes compound and the raw speaker device will be hidden by default. + + You might also need to re-select the default output device. + + In some cases, the added bass will vibrate the keyboard cable leading to a rattling sound, + a piece of foam can be used to mitigate this. + ''; + }; + + hideRawDevice = mkOption { + type = types.bool; + default = true; + description = '' + Hide the raw speaker device. + This option is enabled by default, because keeping the raw speaker device can lead to volume conflicts. + ''; + }; + + rawDeviceName = mkOption { + type = types.str; + example = "alsa_output.pci-0000_c1_00.6.analog-stereo"; + description = '' + The name of the raw speaker device. This will vary by device. + You can get this by running `pw-dump | grep -C 20 pci-0000`. + ''; + }; + }; + }; + + config = mkIf cfg.enable ( + let + outputName = cfg.rawDeviceName; + prettyName = "Duo Speakers"; + + # These are pre-made decibel to linear value conversions, since Nix doesn't have pow(). + # Use the formula `10 ** (db / 20)` to calculate. + db = { + "-18.1" = 0.1244514611771385; + "-5.48" = 0.5321082592667942; + "-4.76" = 0.5780960474057181; + "8.1" = 2.5409727055493048; + "-36" = 1.5848931924611134e-2; + }; + + json = pkgs.formats.json { }; + + # The filter chain, heavily inspired by the asahi-audio project: https://github.com/AsahiLinux/asahi-audio + filter-chain = json.generate "filter-chain.json" { + "node.description" = prettyName; + "media.name" = prettyName; + "filter.graph" = { + nodes = [ + # Psychoacoustic bass extension, + # it creates harmonics of the missing bass to fool our ears into hearing it. + { + type = "lv2"; + plugin = "https://chadmed.au/bankstown"; + name = "bassex"; + control = { + bypass = 0; + amt = 1.2; + sat_second = 1.3; + sat_third = 2.5; + blend = 1.0; + ceil = 200.0; + floor = 20.0; + }; + } + # Loudness compensation, + # it ensures that the sound profile stays consistent across different volumes. + { + type = "lv2"; + plugin = "http://lsp-plug.in/plugins/lv2/loud_comp_stereo"; + name = "el"; + control = { + enabled = 1; + input = 1.0; + fft = 4; + }; + } + # 8-band equalizer, + # it tries to lessen frequencies where the laptop might resonate, + # and tries to make the frequency curve more pleasing; + # this is the "Lappy McTopface" profile (https://github.com/ceiphr/ee-framework-presets) + # further tuned for the Framework Laptop 13 AMD 7040 series + # and might need some tuning on other models. + { + type = "lv2"; + plugin = "http://lsp-plug.in/plugins/lv2/para_equalizer_x8_lr"; + name = "fw13eq"; + control = { + mode = 0; + react = 0.2; + zoom = db."-36"; + + fl_0 = 101.0; + fml_0 = 0; + ftl_0 = 5; + gl_0 = db."-18.1"; + huel_0 = 0.0; + ql_0 = 4.36; + sl_0 = 0; + wl_0 = 4.0; + + fl_1 = 451.0; + fml_1 = 0; + ftl_1 = 1; + gl_1 = db."-5.48"; + huel_1 = 3.125e-2; + ql_1 = 2.46; + sl_1 = 0; + wl_1 = 4.0; + + fl_2 = 918.0; + fml_2 = 0; + ftl_2 = 1; + gl_2 = db."-4.76"; + huel_2 = 6.25e-2; + ql_2 = 2.44; + sl_2 = 0; + wl_2 = 4.0; + + fl_3 = 9700.0; + fml_3 = 0; + ftl_3 = 1; + gl_3 = db."8.1"; + huel_3 = 9.375e-2; + ql_3 = 2.0; + sl_3 = 0; + wl_3 = 4.0; + + fr_0 = 101.0; + fmr_0 = 0; + ftr_0 = 5; + gr_0 = db."-18.1"; + huer_0 = 0.0; + qr_0 = 4.36; + sr_0 = 0; + wr_0 = 4.0; + + fr_1 = 451.0; + fmr_1 = 0; + ftr_1 = 1; + gr_1 = db."-5.48"; + huer_1 = 3.125e-2; + qr_1 = 2.46; + sr_1 = 0; + wr_1 = 4.0; + + fr_2 = 918.0; + fmr_2 = 0; + ftr_2 = 1; + gr_2 = db."-4.76"; + huer_2 = 6.25e-2; + qr_2 = 2.44; + sr_2 = 0; + wr_2 = 4.0; + + fr_3 = 9700.0; + fmr_3 = 0; + ftr_3 = 1; + gr_3 = db."8.1"; + huer_3 = 9.375e-2; + qr_3 = 2.0; + sr_3 = 0; + wr_3 = 4.0; + }; + } + # Compressors. The settings were taken from the asahi-audio project. + { + type = "lv2"; + plugin = "http://lsp-plug.in/plugins/lv2/mb_compressor_stereo"; + name = "woofer_bp"; + control = { + mode = 0; + ce_0 = 1; + sla_0 = 5.0; + cr_0 = 1.75; + al_0 = 0.725; + at_0 = 1.0; + rt_0 = 100; + kn_0 = 0.125; + cbe_1 = 1; + sf_1 = 200.0; + ce_1 = 0; + cbe_2 = 0; + ce_2 = 0; + cbe_3 = 0; + ce_3 = 0; + cbe_4 = 0; + ce_4 = 0; + cbe_5 = 0; + ce_5 = 0; + cbe_6 = 0; + ce_6 = 0; + }; + } + { + type = "lv2"; + plugin = "http://lsp-plug.in/plugins/lv2/compressor_stereo"; + name = "woofer_lim"; + control = { + sla = 5.0; + al = 1.0; + at = 1.0; + rt = 100.0; + cr = 15.0; + kn = 0.5; + }; + } + ]; + + # Now, we're chaining together the modules instantiated above. + links = [ + { + output = "bassex:out_l"; + input = "el:in_l"; + } + { + output = "bassex:out_r"; + input = "el:in_r"; + } + + { + output = "el:out_l"; + input = "fw13eq:in_l"; + } + { + output = "el:out_r"; + input = "fw13eq:in_r"; + } + { + output = "fw13eq:out_l"; + input = "woofer_bp:in_l"; + } + { + output = "fw13eq:out_r"; + input = "woofer_bp:in_r"; + } + { + output = "woofer_bp:out_l"; + input = "woofer_lim:in_l"; + } + { + output = "woofer_bp:out_r"; + input = "woofer_lim:in_r"; + } + ]; + + inputs = [ + "bassex:in_l" + "bassex:in_r" + ]; + outputs = [ + "woofer_lim:out_l" + "woofer_lim:out_r" + ]; + + # This makes pipewire's volume control actually control the loudness comp module + "capture.volumes" = [ + { + control = "el:volume"; + min = -47.5; + max = 0.0; + scale = "cubic"; + } + ]; + }; + "capture.props" = { + "node.name" = "audio_effect.laptop-convolver"; + "media.class" = "Audio/Sink"; + "audio.channels" = "2"; + "audio.position" = [ + "FL" + "FR" + ]; + "audio.allowed-rates" = [ + 44100 + 48000 + 88200 + 96000 + 176400 + 192000 + ]; + "device.api" = "dsp"; + "node.virtual" = "false"; + + # Lower seems to mean "more preferred", + # bluetooth devices seem to be ~1000, speakers seem to be ~2000 + # since this is between the two, bluetooth devices take over when they connect, + # and hand over to this instead of the speakers when they disconnect. + "priority.session" = 1500; + "priority.driver" = 1500; + "state.default-volume" = 0.343; + "device.icon-name" = "audio-card-analog-pci"; + }; + "playback.props" = { + "node.name" = "audio_effect.laptop-convolver"; + "target.object" = outputName; + "node.passive" = "true"; + "audio.channels" = "2"; + "audio.allowed-rates" = [ + 44100 + 48000 + 88200 + 96000 + 176400 + 192000 + ]; + "audio.position" = [ + "FL" + "FR" + ]; + "device.icon-name" = "audio-card-analog-pci"; + }; + }; + + configPackage = + (pkgs.writeTextDir "share/wireplumber/wireplumber.conf.d/99-laptop.conf" '' + monitor.alsa.rules = [ + { + matches = [{ node.name = "${outputName}" }] + actions = { + update-props = { + audio.allowed-rates = [44100, 48000, 88200, 96000, 176400, 192000] + } + } + } + ] + + node.software-dsp.rules = [ + { + matches = [{ node.name = "${outputName}" }] + actions = { + create-filter = { + filter-path = "${filter-chain}" + hide-parent = ${boolToString cfg.hideRawDevice} + } + } + } + ] + + wireplumber.profiles = { + main = { node.software-dsp = "required" } + } + '') + // { + passthru.requiredLv2Packages = with pkgs; [ + lsp-plugins + bankstown-lv2 + ]; + }; + in + { + services.pipewire.wireplumber.configPackages = [ configPackage ]; + + # Pipewire is needed for this. + services.pipewire.enable = mkDefault true; + } + ); +} diff --git a/gpd/duo/duo-specific/bluetooth.nix b/gpd/duo/duo-specific/bluetooth.nix new file mode 100644 index 000000000..01a167b2d --- /dev/null +++ b/gpd/duo/duo-specific/bluetooth.nix @@ -0,0 +1,56 @@ +# There is apparently a bug that affects Framework computers that causes black +# screen on resume from sleep or hibernate with kernel version 6.11. Framework +# have published a workaround; this applies that workaround. +# +# https://fosstodon.org/@frameworkcomputer/113406887743149089 +# https://github.com/FrameworkComputer/linux-docs/blob/main/hibernation/kernel-6-11-workarounds/suspend-hibernate-bluetooth-workaround.md#workaround-for-suspendhibernate-black-screen-on-resume-kernel-611 +{ + config, + lib, + pkgs, + ... +# TODO: drop this if linux 6.11 goes EOL +}: +with lib; +mkIf + ( + (config.boot.kernelPackages.kernelAtLeast "6.11") && (config.boot.kernelPackages.kernelOlder "6.12") + ) + { + systemd.services = { + bluetooth-rfkill-suspend = { + description = "Soft block Bluetooth on suspend/hibernate"; + before = [ "sleep.target" ]; + unitConfig.StopWhenUnneeded = true; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.util-linux}/bin/rfkill block bluetooth"; + ExecStartPost = "${pkgs.coreutils}/bin/sleep 3"; + RemainAfterExit = true; + }; + wantedBy = [ + "suspend.target" + "hibernate.target" + "suspend-then-hibernate.target" + ]; + }; + + bluetooth-rfkill-resume = { + description = "Unblock Bluetooth on resume"; + after = [ + "suspend.target" + "hibernate.target" + "suspend-then-hibernate.target" + ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.util-linux}/bin/rfkill unblock bluetooth"; + }; + wantedBy = [ + "suspend.target" + "hibernate.target" + "suspend-then-hibernate.target" + ]; + }; + }; + } diff --git a/gpd/duo/duo-specific/default.nix b/gpd/duo/duo-specific/default.nix new file mode 100644 index 000000000..2b5eaef36 --- /dev/null +++ b/gpd/duo/duo-specific/default.nix @@ -0,0 +1,20 @@ +{ lib, pkgs, ... }: +with lib; +{ + imports = [ + ../../../common/cpu/amd/raphael/igpu.nix + ./bluetooth.nix + ./amd.nix + ./audio.nix + ./power + ]; + + # Fix TRRS headphones missing a mic + # https://community.frame.work/t/headset-microphone-on-linux/12387/3 + boot.extraModprobeConfig = mkIf (versionOlder pkgs.linux.version "6.6.8") '' + options snd-hda-intel model=dell-headset-multi + ''; + + # Needed for desktop environments to detect/manage display brightness + hardware.sensor.iio.enable = mkDefault true; +} diff --git a/gpd/duo/duo-specific/power/default.nix b/gpd/duo/duo-specific/power/default.nix new file mode 100644 index 000000000..eb5a5d0a2 --- /dev/null +++ b/gpd/duo/duo-specific/power/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./ppt.nix + ./power-saving.nix + ]; +} diff --git a/gpd/duo/duo-specific/power/power-saving.nix b/gpd/duo/duo-specific/power/power-saving.nix new file mode 100644 index 000000000..9c534040e --- /dev/null +++ b/gpd/duo/duo-specific/power/power-saving.nix @@ -0,0 +1,18 @@ +{ lib, config, ... }: +with lib; +let + cfg = config.hardware.gpd.duo.powerManagement; +in +{ + options.hardware.gpd.duo.powerManagement = { + enable = mkEnableOption "Enable power-profiles-daemon and disable TLP for the GPD Duo" // { + # Default increase PPT to the BIOS default when power adapter plugin to increase performance. + default = true; + }; + }; + + config = mkIf cfg.enable { + services.power-profiles-daemon.enable = mkDefault true; + services.tlp.enable = mkForce false; + }; +} diff --git a/gpd/duo/duo-specific/power/ppt.nix b/gpd/duo/duo-specific/power/ppt.nix new file mode 100644 index 000000000..4efb73f22 --- /dev/null +++ b/gpd/duo/duo-specific/power/ppt.nix @@ -0,0 +1,62 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; +let + cfg = config.hardware.gpd.ppt; +in +{ + options.hardware.gpd.ppt = { + enable = mkEnableOption "Enable PPT control for device by ryzenadj." // { + # Default increase PPT to the BIOS default when power adapter plugin to increase performance. + default = true; + }; + + adapter = { + fast-limit = mkOption { + description = "Fast PTT Limit(milliwatt) when power adapter plugin."; + default = 35000; + type = types.ints.unsigned; + }; + slow-limit = mkOption { + description = "Slow PTT Limit(milliwatt) when power adapter plugin."; + default = 32000; + type = types.ints.unsigned; + }; + stapm-limit = mkOption { + description = "Stapm PTT Limit(milliwatt) when power adapter plugin."; + default = 28000; + type = types.ints.unsigned; + }; + }; + + battery = { + fast-limit = mkOption { + description = "Fast PTT Limit(milliwatt) when using battery."; + default = 24000; + type = types.ints.unsigned; + }; + slow-limit = mkOption { + description = "Slow PTT Limit(milliwatt) when using battery."; + default = 22000; + type = types.ints.unsigned; + }; + stapm-limit = mkOption { + description = "Stapm PTT Limit(milliwatt) when using battery."; + default = 22000; + type = types.ints.unsigned; + }; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = [ pkgs.ryzenadj ]; + services.udev.extraRules = '' + SUBSYSTEM=="power_supply", KERNEL=="ADP1", ATTR{online}=="1", RUN+="${getExe pkgs.ryzenadj} --stapm-limit ${toString cfg.adapter.stapm-limit} --fast-limit ${toString cfg.adapter.fast-limit} --slow-limit ${toString cfg.adapter.slow-limit}" + SUBSYSTEM=="power_supply", KERNEL=="ADP1", ATTR{online}=="0", RUN+="${getExe pkgs.ryzenadj} --stapm-limit ${toString cfg.battery.stapm-limit} --fast-limit ${toString cfg.battery.fast-limit} --slow-limit ${toString cfg.battery.slow-limit}" + ''; + }; +}