jevsnes.git / nix / mesen.nix

Mesen 2, launched with a set of settings.json keys ENFORCED.

Mesen owns settings.json: it rewrites all ~220 KB of it (key maps, per-system config, window state) on startup and exit, so the file cannot be a store symlink. What can be declared is the handful of keys this project depends on, merged in before every launch. Everything else in the file stays Mesen's.

None of these keys has a command-line equivalent that works:

  • Video.UseSoftwareRenderer is read in the MainWindow constructor (UI/Windows/MainWindow.axaml.cs:81), before the command line is parsed, so a switch arrives after the renderer is already chosen. The GPU renderer gives a BLACK game surface with working audio whenever Mesa falls back to llvmpipe, which is what an ordinary WSLg session on this host gets (measured 2026-09-20, mesen 2.1.1; the same build renders correctly with the two variables the devshell sets — see flake.nix). The software renderer works in both cases.
  • Debug.ScriptWindow.AllowIoOsAccess / AllowNetworkAccess gate LuaSocket, default false, and have no switch at all (UI/Config/Debugger/ScriptWindowConfig.cs:29-30). Network needs I/O on as well (Core/Debugger/ScriptingContext.cpp:73).

THE SEED, when no settings file exists yet

  • ConfigUpgrade = 1 is ConfigUpgradeHint.FirstRun, the only value for which Configuration.InitializeDefaults populates controller mappings. A bare {} deserialises as 0 (Uninitialized) and leaves every port UNMAPPED.
  • A settings file existing at all is what suppresses the first-run wizard — including under --testRunner, which otherwise hangs on it (UI/Program.cs:56-76).

Mesen writes the file with a UTF-8 BOM, which jq rejects, hence the sed.

35{
36  lib,
37  writeShellApplication,
38  writeText,
39  mesen,
40  jq,
41  gnused,
42  coreutils,
43  # Attrset deep-merged over settings.json before every launch.
44  enforce,
45}:
46let
47  enforced = writeText "mesen-enforced.json" (builtins.toJSON enforce);
48  seed = writeText "mesen-seed.json" (
49    builtins.toJSON (
50      lib.recursiveUpdate {
51        ConfigUpgrade = 1;
52        DefaultKeyMappings = "Xbox, ArrowKeys";
53      } enforce
54    )
55  );
56in
57writeShellApplication {
58  # Same name as the upstream binary, which is deliberately NOT also put on
59  # PATH: there is then no unwrapped `Mesen` to launch by mistake.
60  name = "Mesen";
61  runtimeInputs = [
62    jq
63    gnused
64    coreutils
65  ];
66  text = ''
67    settings="''${XDG_CONFIG_HOME:-$HOME/.config}/Mesen2/settings.json"
68    strip_bom() { sed '1s/^\xEF\xBB\xBF//' "$settings"; }
69
70    if [ ! -s "$settings" ]; then
71      mkdir -p "$(dirname "$settings")"
72      cat ${seed} > "$settings"
73    elif ! strip_bom | jq -e --slurpfile e ${enforced} '(. * $e[0]) == .' > /dev/null; then
74      tmp="$(mktemp "$settings.XXXXXX")"
75      trap 'rm -f "$tmp"' EXIT
76      strip_bom | jq --slurpfile e ${enforced} '. * $e[0]' > "$tmp"
77      mv "$tmp" "$settings"
78      trap - EXIT
79    fi
80
81    exec ${mesen}/bin/Mesen "$@"
82  '';
83}