Running Arma Reforger on Linux is not as simple as clicking a green "Play" button and hoping for the best. The game's architecture, its translation pipeline, and the Linux kernel's own performance subsystems interact in ways that are rarely documented in a single place. Getting meaningful frame times -- not just raw averages, but the stable, low-variance performance that makes a military simulation playable -- requires understanding what is actually happening beneath the surface, from the Bohemia Interactive Enfusion engine's threading model all the way down to kernel scheduler tick rates and PCIe BAR allocation.

This guide goes under the hood. It is for people who are not satisfied with "just install ProtonGE and you're done" -- because that advice is incomplete, sometimes misleading, and does not explain why things work or break.

The Architecture You Are Dealing With

Before tuning anything, you need to understand what you are tuning. Arma Reforger is the first production game shipped on Bohemia Interactive's Enfusion engine. The engine is built in C++ at its core with an Enforce scripting layer on top, uses up-to-date rendering tech and multi-threading wherever possible, and ships exclusively as a DirectX 12 title on PC. There is no Vulkan path from Bohemia.

That last fact shapes everything. On Linux, you are not running a native binary. You are running a Windows executable inside a compatibility layer -- specifically Valve's Proton, which passes DirectX 12 calls through VKD3D-Proton, a translation library that converts D3D12 API calls into Vulkan API calls at runtime. Worth noting clearly: VKD3D-Proton handles DirectX 12 exclusively. DX9, DX10, and DX11 titles route through DXVK instead. This distinction matters for understanding which environment variables and tools apply to Reforger versus other games you might run through Proton at the same time. Reforger is DX12 only, so everything in this guide concerns the VKD3D-Proton path. VKD3D-Proton was originally forked from the Wine project's vkd3d library and has been substantially rewritten primarily by Hans-Kristian Arntzen, a Norwegian graphics programmer who holds an M.Sc. in digital signal processing from the Norwegian University of Science and Technology (NTNU) in Trondheim.arntzen-software.no/about He is Founder and CEO of Arntzen Software AS, a Trondheim-based consulting firm specializing in 3D graphics and engine programming, which he established in October 2018 (Norwegian business registry CIN 921652518) -- approximately two months before his formal December 2018 departure from Arm Norway AS, where he spent four years (August 2014 -- December 2018) working on mobile Mali GPU technology. Arm Norway is the Trondheim design center descended from Falanx Microsystems AS, a company spun off from NTNU in 1998 and acquired by ARM Holdings in 2006; it is the organization responsible for the Mali GPU line.Wikipedia / Arm Norway Among Arntzen's other notable open-source projects are SPIRV-Cross (a SPIR-V reflection and cross-compilation library now used across the industry) and Fossilize (a Vulkan pipeline state serialization tool hosted under the ValveSoftware GitHub organization). Both his GitHub profiles (HansKristian-Work and Themaister) identify him as "CEO & Founder of Arntzen Software AS." He is frequently described in Linux gaming press as a "Valve developer" -- Phoronix, for example, routinely uses that framing. This is imprecise: Arntzen Software AS is an independent consulting company, and Arntzen operates as an independent contractor under contract for Valve on VKD3D-Proton and related projects such as Fossilize (hosted under the ValveSoftware GitHub organization). No Valve employment is listed in his biography. The Arntzen Software website describes the company plainly: "Arntzen Software AS is run and owned by Hans-Kristian Arntzen."arntzen-software.no Every rendering command the Enfusion engine issues to the GPU passes through this translation layer before reaching your graphics driver.

Context

This is not a negligible overhead. GPU command submission in D3D12 has different synchronization semantics than Vulkan, and bridging those semantics requires work. A thread that Enfusion expects to be a lightweight GPU submission thread becomes a thread that must also run shader compilation and Vulkan pipeline management. Understanding this is critical for knowing which kernel and Proton settings actually matter.

Step One: Get the Right Proton Version

The single largest performance variable for Linux Reforger users is which version of VKD3D-Proton is bundled in your Proton installation. This is not marketing material -- there are multiple game-specific bug fixes and performance workarounds documented in the VKD3D-Proton release notes that directly affect Reforger.

VKD3D-Proton 2.13, released on June 21, 2024, fixed two distinct Reforger-specific MSAA rendering bugs. The release notes document: "Correctly expose MSAA features for depth-stencil. Fixes Arma Reforger" and separately "Fix bugs in MSAA resolve implementation when dealing with custom resolve formats. Fixes Arma Reforger."v2.13 release Both issues affected the game's anti-aliasing paths and produced rendering artifacts in different scenarios. More significantly, VKD3D-Proton 2.14.1 added a no_upload_hvv workaround specifically for Arma Reforger. The 2.14.1 release notes state: "add a no_upload_hvv workaround for Arma Reforger to workaround weird asset loading behavior."v2.14.1 release The VKD3D-Proton README defines no_upload_hvv precisely: it "Blocks any attempt to use host-visible VRAM (large/resizable BAR) for the UPLOAD heap."vkd3d-proton README The per-game application of this flag means Reforger's unusual asset loading patterns were triggering problematic host-visible VRAM usage during upload heap allocation. VKD3D-Proton applies this workaround automatically by executable name, so no user action is required. GitHub issue #2419 (March 2025) definitively confirmed that following Reforger's 1.3 update, the underlying game-side behavior changed and the workaround is no longer necessary: the reporter verified that running with VKD3D_CONFIG=skip_application_workarounds after the 1.3 update produced no texture-popping regression, whereas before the update it did.GitHub issue #2419 If you are on Reforger 1.3 or later, it is worth testing whether skipping application workarounds improves performance. VKD3D_CONFIG accepts a comma-separated list of flags and can be combined with other options -- for example, VKD3D_CONFIG=skip_application_workarounds,no_staggered_submit -- without conflict. (Note: do not combine with dxr -- that flag has been the default since 2.11 and is redundant.) The flag is documented in the README as "Skips all application workarounds. For debugging purposes."vkd3d-proton README Note that "all application workarounds" means exactly that: this flag disables every per-game fix that VKD3D-Proton applies by executable name, not just the no_upload_hvv workaround. If Reforger receives a game update that introduces a new VKD3D-Proton workaround in a future release, skip_application_workarounds would disable that too. Use it as a diagnostic flag and verify game stability before treating it as a permanent launch option. A second VKD3D_CONFIG flag worth knowing for performance troubleshooting is no_staggered_submit, introduced in VKD3D-Proton 2.14 (December 2024) -- not 3.0 as some community guides incorrectly state. The 2.14 release notes document its context precisely: "Rewrite queue submission logic to deal better with difficult submission patterns such as FSR3 3.1 Frame Generation. On implementations with only one graphics queue, vkd3d-proton will now attempt to do basic software scheduling of GPU work. This may regress GPU performance in some other cases and VKD3D_CONFIG=no_staggered_submit is a way to disable this code path."v2.14 release The "one graphics queue" qualifier refers to the number of Vulkan graphics queues the driver exposes, not to total hardware queue families. Many discrete desktop GPUs expose only a single graphics queue in Vulkan even if they have multiple compute queues. If you experience unexplained GPU throughput drops or frame time instability after any 2.14-or-later Proton update -- not just 3.0 -- VKD3D_CONFIG=no_staggered_submit is the first flag to try. It has been present and available since 2.14.

VKD3D-Proton 3.0, released on November 17, 2025, was a major release with several changes relevant to Reforger users beyond the DXBC backend rewrite. The rewrite was authored by Philip Rebohle (known as @doitsujin), the creator of DXVK and a Valve-sponsored contributor -- Wikipedia's DXVK article documents that "in 2018, the developer was sponsored by Valve to work on the project full-time in order to advance compatibility of the Linux version of Steam with Windows games." The 3.0 release notes state: "@doitsujin rewrote the entire DXBC backend, replacing our legacy vkd3d-shader path. DXVK and vkd3d-proton now share the same DXBC frontend."v3.0 release This rewrite fixes a broad class of bugs and missing features in the legacy vkd3d-shader path that were leaving some DX12 titles completely broken or running only on fallback code paths, including Red Dead Redemption 2 in D3D12 mode.

3.0 also added AMD FSR4 support via AGS WMMA intrinsics through the VK_KHR_cooperative_matrix and VK_KHR_shader_float8 Vulkan extensions. The 3.0 release notes are precise about scope: "the default 'official' build of vkd3d-proton only exposes this feature when the native VK_KHR_shader_float8 is properly supported, i.e. RDNA4+ only."v3.0 release If you have an RX 9070 XT or other RDNA4 card, FSR4 is available in Reforger through the standard Proton 3.0+ build with no additional configuration. Older AMD GPUs (RDNA2, RDNA3) require a source build with emulation flags at significant performance cost, which is not enabled by default. NVIDIA users: FSR4 is AMD-specific and will not work on your hardware via this path.

3.0 also added support for AMD Anti-Lag via VKD3D_CONFIG=amd_anti_lag when the driver exposes the AMD_anti_lag Vulkan extension, which is available in RADV on supported hardware. The release notes note that "the current implementation does not take frame-gen into account,"v3.0 release meaning if you use FSR3 frame generation alongside Anti-Lag in 3.0, the latency benefits may be incomplete. For Reforger without frame generation, this is not a concern. AMD GPU users on 3.0 or later can add VKD3D_CONFIG=amd_anti_lag to their launch options to reduce input latency if RADV exposes the extension. One additional housekeeping note: VKD3D_CONFIG=dxr, which some older guides recommend adding to launch options, has been the default since VKD3D-Proton 2.11 (November 2023). The 2.11 release notes state: "VKD3D_CONFIG=dxr is default now, and no longer needed."v2.11 release If it appears in your launch options from an old guide, remove it -- it is redundant. 3.0 also removed DXVK_FRAME_RATE support (see the Frame Rate Limiter section below for what this means practically).

The 3.0 release was followed by two immediate bugfix releases worth knowing about. On November 18, 2025 -- one day after 3.0 -- version 3.0a corrected what the release notes call "a silly performance regression in the new unified image layout path."v3.0a release Then on December 10, 2025, version 3.0b addressed a follow-on problem in the same area. The 3.0b release notes state: "Fix silly regression in synchronization when VK_KHR_unified_image_layouts is not supported."v3.0b release This matters for users on graphics drivers that do not yet expose the VK_KHR_unified_image_layouts extension, which is a relatively recent Vulkan extension -- older Mesa and NVIDIA driver versions may not support it. Valve's official Proton releases have already updated to 3.0b. If your Proton build is between 3.0 and 3.0b and you run an older graphics driver, you may experience frame synchronization problems that do not reproduce on Windows. The minimum recommended build for Reforger on Linux is now 3.0b or any build incorporating it.

Practical Action

Use Proton Experimental or GE-Proton rather than a pinned stable Proton version. GE-Proton (community builds by GloriousEggroll) typically incorporates VKD3D-Proton releases faster than Valve's official builds. At minimum, you want VKD3D-Proton 3.0b or newer for the fullest bug-fix coverage -- 3.0b corrected synchronization regressions that affect users with older graphics drivers, and Valve's official Proton releases have already shipped it. To set your Proton version per-game in Steam: right-click Arma Reforger, select Properties, navigate to the Compatibility tab, and check "Force the use of a specific Steam Play compatibility tool."

Your Steam launch options for Reforger on Linux may include the -d3d12 flag. This is a community-documented option that appears in ProtonDB reports and Linux gaming guides -- it is not explicitly listed in GE-Proton release notes for Reforger specifically, so treat it as a "try it and see" addition rather than a documented requirement. Some community reports indicate that under certain GE-Proton builds the engine may not correctly enter its DX12 initialization path without it. If you are using Proton Experimental rather than GE-Proton, this flag is generally not necessary, and Bohemia's official Linux guidance does not require it:

Steam launch options
gamemoderun %command% -d3d12

The BattlEye Problem and the Launcher Fix

Reforger ships with BattlEye anti-cheat enabled by default. BattlEye supports Proton as a platform, but only when game developers explicitly opt in. Bohemia Interactive has not opted Reforger into the Proton BattlEye Runtime integration that works for titles like Arma 3. The result is that ArmaReforger_BE.exe -- the BattlEye launcher executable -- crashes on Linux under Proton. The BattlEye service itself, not the launcher, is what communicates with servers. The community-documented fix redirects Steam's launch command away from the crashing BE launcher executable to the underlying Steam executable directly:

Steam launch options -- BattlEye launcher bypass
echo "%command%" | sed 's/ArmaReforger_BE.exe/ArmaReforgerSteam.exe/' | sh

This uses shell substitution to replace the BattlEye launcher executable with the plain Steam executable before launch. The Bohemia Interactive community wiki documents this fix and notes that despite the name "bypass," you can still connect to BattlEye-secured servers and play normally after applying it: "While it seems to get around BattleEye requirements, one can still connect to BattleEye-secured servers and play as normal."BI community wiki This is because what fails on Linux is the launcher executable, not the underlying BattlEye service that talks to game servers. Without this launch option the game will crash to desktop on most Linux systems.

Note on server compatibility

Community reports broadly confirm BattlEye-secured server access continues to work with this fix. However, behavior may vary across Proton versions and future game updates could change this. If you encounter a server kick citing BattlEye after a game update, verify the launch option syntax is still correct and that ArmaReforger_BE.exe has not been renamed in the game's install directory.

ESYNC, FSYNC, and Thread Synchronization

Reforger under Proton is heavily threaded. The Enfusion engine runs separate threads for simulation, rendering, audio, AI, and network replication. All of these threads communicate using Windows synchronization primitives -- mutexes, semaphores, events -- that Wine must emulate on Linux. Without optimization, every such primitive requires a round-trip to Wine's server process (wineserver), which is a significant CPU bottleneck in multi-threaded workloads.

Proton ships with two approaches to eliminate this bottleneck: ESYNC and FSYNC. ESYNC (eventfd-based synchronization) replaces wineserver round-trips with Linux eventfd objects. FSYNC (futex-based synchronization) does the same but with futex system calls, which can be faster because futexes allow threads to skip a kernel syscall entirely in the uncontended case -- when no other thread is competing for the lock, the operation completes in userspace via an atomic compare-and-swap without entering the kernel at all. A technical writeup by Zebediah Figura at Collabora states that with ESYNC and FSYNC, "most operations which are understood to comprise one system call on Windows now also comprise one system call (to the Linux kernel) on Wine. The actual performance improvement is quite substantial, depending on the application."collabora.com

Both are enabled by default in Proton when the system supports them. FSYNC requires Linux 5.16 or newer, which is when the futex_waitv syscall was merged into the mainline kernel. The term "futex2" appears in older documentation and community guides referring to the broader effort to redesign futex interfaces, but for gaming purposes what matters is simply that futex_waitv is present -- which it has been in every mainline kernel since 5.16. Modern Proton builds dropped support for the old out-of-tree fsync patches long ago and require the mainline interface exclusively. On any reasonably current distribution kernel you do not need to do anything to enable FSYNC -- Proton detects it automatically. Note on the performance mechanism: a futex operation requires a kernel syscall only when a thread must actually block (contended case); in the uncontended case the lock is acquired entirely in userspace via atomic operations. This is why FSYNC outperforms the wineserver approach in threaded games, where the contended case is relatively rare compared to the volume of lock operations. What matters is knowing when to disable them for troubleshooting. If you experience crashes or unusual hangs that persist across Proton versions, ESYNC or FSYNC are worth ruling out:

Steam launch options -- ESYNC/FSYNC troubleshooting
# Disable ESYNC only
PROTON_NO_ESYNC=1 %command%

# Disable FSYNC only
PROTON_NO_FSYNC=1 %command%

# Disable both (fall back to wineserver -- slower but maximally compatible)
PROTON_NO_ESYNC=1 PROTON_NO_FSYNC=1 %command%
Warning

Do not disable both ESYNC and FSYNC for normal play. The performance cost of falling back to wineserver-based synchronization in a heavily-threaded title is substantial. These are diagnostic flags only. The Bohemia wiki recommends disabling ESYNC specifically for thread-related hang issues on Arma 3 under Proton; the same logic applies to Reforger if you hit similar symptoms.BI wiki

CPU Governor and C-State Tuning

Linux ships with a CPU frequency scaling subsystem called CPUFreq. The governor in use determines how quickly and aggressively your CPU ramps its clock speed in response to workload. The default governor varies by distribution and CPU driver. Systems using the generic acpi-cpufreq driver often default to ondemand or schedutil. Systems using intel_pstate in active mode (most modern Intel CPUs with Hardware P-states enabled) expose only powersave and performance, where powersave is the default and behaves similarly to schedutil -- dynamically scaling based on utilization hints. AMD systems using amd-pstate on Zen 2 and newer CPUs behave similarly -- the Linux kernel documentation confirms that amd-pstate supports Zen 2 and Zen 3 processors, and from kernel 6.5 onward it became the default driver for supported hardware.kernel.org/amd-pstate You can check your current governor with cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor. All reactive governors share the same fundamental problem for gaming: they react to CPU load after the fact -- they see utilization spike, then raise frequency. In a game with tight frame timing requirements, this reaction lag translates directly into frame time variance. The scheduler detects the load from a compute-heavy game frame after the frame has already started executing at sub-optimal frequency. This applies equally to ondemand, schedutil, and powersave in reactive mode.

The performance governor eliminates this entirely by statically holding the CPU at its maximum frequency. The Linux kernel documentation on CPUFreq governors notes that the performance governor sets the CPU statically to the highest frequency within the borders of scaling_min_freq and scaling_max_freq.kernel.org One precision note: scaling_max_freq is not always your hardware's turbo ceiling. Some distributions and power management tools cap it below the hardware maximum at install time. Before trusting that the performance governor is running at full speed, verify the actual ceiling with cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq and compare it against cpupower frequency-info output. Set it with:

terminal
$ sudo cpupower frequency-set -g performance

To make this persistent across reboots on systemd-based systems, enable the cpupower service and edit /etc/default/cpupower:

/etc/default/cpupower
GOVERNOR="performance"

A related concern is C-states. Modern CPUs have deep sleep states (C6, C7, etc.) that trade power for latency. Transitioning out of a deep C-state takes microseconds, which is undetectable in office workloads but noticeable in game workloads with tight frame budgets. For Intel systems, constrain C-states via the kernel boot parameter intel_idle.max_cstate=1, which prevents the processor from entering sleep states deeper than C1. For AMD, the processor.max_cstate=1 boot parameter exists but its reliability varies by kernel version and ACPI implementation -- on some AMD platforms it is ignored when the AMD-P ACPI CPUFreq driver takes precedence. The most reliable approach on AMD is to disable deep C-states directly in BIOS/UEFI, which bypasses any kernel-driver interaction entirely. Add the Intel parameter to your GRUB command line in /etc/default/grub, then run sudo update-grub and reboot:

/etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="... intel_idle.max_cstate=1"

Memory Subsystem Tuning: Hugepages, Swappiness, and the TLB

Arma Reforger loads large amounts of world geometry, textures, and entity state into memory. The Linux virtual memory subsystem handles all of this through page tables -- structures that map virtual addresses to physical RAM. Every memory access that misses the Translation Lookaside Buffer (TLB) requires a page table walk, which costs cycles.

Transparent Huge Pages (THP) is a Linux kernel feature that automatically promotes 4KB pages to 2MB pages for large allocations. Larger pages mean fewer TLB entries are needed to cover the same address space, reducing TLB miss rates. Check your current THP setting and set it to always:

terminal
# Check current THP setting
$ cat /sys/kernel/mm/transparent_hugepage/enabled

# Enable hugepages immediately
$ echo always | sudo tee /sys/kernel/mm/transparent_hugepage/enabled

To persist the THP always setting across reboots, add the kernel boot parameter in /etc/default/grub, then run sudo update-grub and reboot. This is the correct persistence mechanism for THP -- not a sysctl entry:

/etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="... transparent_hugepage=always"
Warning -- two different hugepage systems

Transparent Huge Pages (THP) and explicit hugepages (vm.nr_hugepages) are completely separate kernel mechanisms. A common error in gaming guides is to add vm.nr_hugepages = 512 to /etc/sysctl.d/ under a "THP" heading. That sysctl controls explicit (static, pre-allocated) hugepages used by hugetlbfs, typically for databases and VMs. It has no effect on THP, which is what games and Proton benefit from. Setting vm.nr_hugepages = 512 would permanently reserve 1GB of RAM for a mechanism Proton does not use, while doing nothing to enable THP persistence. THP is controlled via /sys/kernel/mm/transparent_hugepage/enabled at runtime and via the kernel command line parameter transparent_hugepage=always at boot -- not via sysctl.

Equally important is swappiness. On Linux, vm.swappiness controls how aggressively the kernel moves memory pages to swap. A high value means the kernel will swap pages even when RAM is available, which causes latency spikes when those pages are needed again. For gaming, set it to a very low value. The correct value is 1 rather than 0: on kernels 3.5 and later, vm.swappiness=0 does not fully disable swapping -- it means "avoid swap unless the system is critically out of memory and OOM-killing is the alternative." A value of 1 means "swap only under extreme memory pressure" while still allowing the kernel to reclaim anonymous pages when necessary, avoiding a scenario where it refuses to swap at all and triggers an OOM kill instead. If you have 16GB or more, you may also consider disabling swap entirely for the session:

terminal
# Set swappiness low (persistent: add to /etc/sysctl.d/99-gaming.conf)
$ sudo sysctl -w vm.swappiness=1

# Optional: disable swap entirely for the session
$ sudo swapoff -a

# Re-enable when done gaming
$ sudo swapon -a

NUMA Balancing: The Silent Frame Time Thief

Non-Uniform Memory Access (NUMA) is relevant even on single-CPU systems with modern AMD Ryzen processors, because Ryzen's chiplet architecture means memory latency varies depending on which memory controller a core's request lands on. The Linux kernel's AutoNUMA feature attempts to migrate memory pages closer to the cores that access them -- a good idea in theory, but in practice it introduces page faults and TLB shootdowns that produce frame time spikes.

This tuning is particularly high-value on AMD Ryzen systems specifically. Ryzen CPUs partition cores across CCDs (Core Chiplet Dies) with a measurable latency penalty when a core accesses memory that is "homed" to the other CCD's memory controller. AutoNUMA's page migration attempts to correct this dynamically -- but each migration itself triggers a TLB shootdown across all cores, which is exactly the kind of periodic, unpredictable latency that produces frame time spikes. Disabling AutoNUMA prevents the migrations and accepts the slightly less-optimal average memory locality in exchange for eliminating the jitter. On Intel systems with a single monolithic die, the benefit is less pronounced but the recommendation still holds for high-thread-count gaming workloads where kernel migration activity can compete with render threads.

NVIDIA's own performance tuning documentation for Grace-based server hardware notes that AutoNUMA can significantly reduce GPU-heavy application performance due to the additional page-faults that are introduced.NVIDIA docs That documentation targets Grace server hardware rather than desktop GPUs; the citation is used here for the underlying principle -- page migration triggering TLB shootdowns is a hardware-agnostic behavior -- not as evidence about desktop gaming specifically. Disable AutoNUMA during gaming sessions:

terminal (add to /etc/sysctl.d/99-gaming.conf for persistence)
$ sudo sysctl -w kernel.numa_balancing=0
Warning

This setting is system-wide and persistent if written to /etc/sysctl.d/99-gaming.conf. If your machine also runs virtual machines, databases, or other workloads with NUMA-sensitive memory access patterns, disabling AutoNUMA globally may hurt those workloads. Consider applying it only for gaming sessions via the one-time sysctl -w command and reverting with sudo sysctl -w kernel.numa_balancing=1 when done.

IRQ Affinity and the irqbalance Problem

Linux uses a daemon called irqbalance to distribute hardware interrupts across CPU cores. The goal is load balancing, but irqbalance can move GPU driver interrupts, network card interrupts, and storage interrupts onto the same cores that your game's render thread is running on, creating competition and jitter.

For a dedicated gaming session, stopping irqbalance and manually assigning interrupts gives more predictable behavior:

terminal
# Stop irqbalance for the session
$ sudo systemctl stop irqbalance

# Check current interrupt assignments
$ cat /proc/interrupts

# Identify your GPU and NIC interrupt lines by their name in the last column
# NVIDIA GPU interrupts appear as: nvidia or nv_*
# (xhci_hcd entries are USB host controller interrupts -- not GPU interrupts)
# AMD GPU interrupts appear as: amdgpu, drm
# NVMe interrupts appear as: nvme0q*, nvme1q*
# Network card interrupts appear as: eno*, eth*, ens*, or your NIC driver name

# Pin IRQ to CPU core 0 using a hexadecimal bitmask -- CHANGE the IRQ number to your actual GPU IRQ from /proc/interrupts above
# smp_affinity uses hex: 1=CPU0, 2=CPU1, 4=CPU2, 8=CPU3, 3=CPU0+CPU1, etc.
$ echo 1 | sudo tee /proc/irq/45/smp_affinity
# ^ Replace 45 with your actual GPU IRQ number

Reading /proc/interrupts output: each line shows IRQ number, per-CPU counts, type, and device name. Search the rightmost column for your GPU driver name (e.g., nvidia or amdgpu) and your network card name to find the IRQ numbers you want to pin away from your game's render cores. A note on a common point of confusion: xhci_hcd entries in /proc/interrupts are USB host controller interrupts generated by USB peripherals (including USB audio interfaces), not GPU interrupts. If you use a USB headset or audio device, you will see xhci_hcd entries that have nothing to do with the GPU -- do not confuse them with nvidia or amdgpu lines. Pin GPU driver IRQs to a dedicated core, and pin network IRQs to a different dedicated core from both the GPU and the game thread cores. The goal is that no interrupt handler will preempt a render or physics thread mid-frame.

Warning

This is advanced configuration and the correct assignment depends entirely on your hardware topology. Use lstopo from the hwloc package to understand your CPU's physical layout before making these assignments. Restart irqbalance when done gaming to avoid missing interrupt handling on normal workloads.

The VKD3D_FRAME_RATE Frame Limiter

One of the most Linux-specific and least-understood performance issues with Reforger is GPU-driven grass flickering. Community research confirmed in August 2025 that simply avoiding 100% GPU usage -- ideally via a frame limiter -- resolves the grass flicker issue, which appears to be triggered by the GPU hitting saturation under VKD3D-Proton's submission model. The Steam Community thread documenting this is titled "To those with grass flicker on Linux (FIX)," posted by user Klikard on August 25, 2025.Steam thread A corresponding VKD3D-Proton GitHub issue filed in June 2024 independently documents the same root cause: "In Arma Reforger rocks and foliage will completely glitch out when around 100% GPU usage."GitHub issue #2031 The Klikard thread's edit history is notable: it was originally posted recommending DXVK_FRAME_RATE, then updated in December 2025 to replace it with VKD3D_FRAME_RATE after the 3.0 removal -- confirming in community practice the version history documented in this guide. A separate community report from October 2025 found that switching to Proton 10 beta resolved the issue even at high GPU load, suggesting that later Proton builds may include driver-level improvements to the underlying submission behavior. This report is cited from community discussion threads rather than official documentation and should be treated as an anecdotal data point pending broader confirmation.GitHub issue #2031

The frame rate limiter variable VKD3D_FRAME_RATE was introduced in VKD3D-Proton 2.14 (December 13, 2024), alongside DXVK_FRAME_RATE.v2.14 release The 3.0 release (November 2025) subsequently removed DXVK_FRAME_RATE -- the release notes state: "Remove DXVK_FRAME_RATE to align with DXVK's removal. Only VKD3D_FRAME_RATE remains (at least for now)."v3.0 release If you are on any Proton or GE-Proton build released after December 2024, VKD3D_FRAME_RATE is the correct variable. DXVK_FRAME_RATE no longer works in VKD3D-Proton 3.0 or later -- this applies specifically to DX12 titles running through VKD3D-Proton. The variable remains valid in DXVK itself for DX9, DX10, and DX11 titles, so if you run Arma 3 (which uses DX11) alongside Reforger, DXVK_FRAME_RATE still applies to Arma 3 while VKD3D_FRAME_RATE is the correct choice for Reforger. Set a frame rate cap via launch options, adjusting the value to your monitor's refresh rate:

Steam launch options
VKD3D_FRAME_RATE=120 gamemoderun %command% -d3d12

Capping to your monitor's refresh rate prevents the GPU from running ahead of the display. When the GPU is asked to render faster than the display can consume frames, work queues up in VKD3D-Proton's submission pipeline; that deep queue, under saturation, creates the specific submission pattern that triggers the flickering artifacts. A frame rate cap keeps the submission queue shallow and prevents the GPU from hitting the saturation threshold. This also reduces heat, reduces power consumption, and is the prevailing community explanation for why this fix works -- though the root cause has not been formally documented by Bohemia or Valve. If you are seeing asset corruption or unusual stutter on first load after updates, also clear the shader cache:

Wayland Users: Forced V-Sync in Fullscreen (Compositor-Dependent)

If you are running Reforger under a Wayland compositor (which is the default session type on Fedora, Ubuntu, and several other major distributions as of 2025), be aware of a known VKD3D-Proton behavior: in fullscreen mode, some Wayland compositors' present model can force V-Sync regardless of your in-game V-Sync setting, capping you to your display's refresh rate even when you have explicitly disabled V-Sync. This behavior is compositor-specific -- it does not apply universally to all Wayland sessions, and behavior differs between KDE Plasma, GNOME, Sway, and other compositors. It is tracked in VKD3D-Proton GitHub issue #2639, filed September 2025, where a reporter confirmed that DX12 games remain locked to 60 FPS under V-Sync in fullscreen on Wayland/Sway while the equivalent DX11 path via DXVK runs uncapped.GitHub issue #2639

Two workarounds, ordered by reliability:

1. Use gamescope. Gamescope is Valve's Vulkan-based micro-compositor, originally developed for the Steam Deck but fully supported on desktop Linux. The Gamescope GitHub README describes it as getting "game frames through Wayland by way of Xwayland" and using "DRM/KMS to directly flip game frames to the screen, even when stretching or when notifications are up, removing another copy."gamescope GitHub Because gamescope creates its own isolated Xwayland environment for the game and presents the final output to the host compositor as a single surface, the host compositor's forced V-Sync behavior does not reach the game's swapchain. This means VKD3D_FRAME_RATE and your in-game V-Sync settings apply correctly. Add gamescope to your Steam launch options before %command%, specifying your display resolution and refresh rate. VKD3D_FRAME_RATE is set slightly below the gamescope refresh rate (-r) to avoid tearing at the refresh boundary -- adjust both values to match your display:

gamescope -w 2560 -h 1440 -r 144 -f -- VKD3D_FRAME_RATE=120 gamemoderun %command%

2. Use windowed or borderless-windowed mode. Switching away from exclusive fullscreen breaks the forced V-Sync path in the compositor and allows VKD3D_FRAME_RATE to control pacing. This is the simpler fix if you do not want to install gamescope, but it may introduce desktop compositor overhead that gamescope avoids.

If running under X11 or XWayland (without gamescope), this limitation does not apply. Check your session type with echo $XDG_SESSION_TYPE.

Additional Latency Tuning: VKD3D_SWAPCHAIN_LATENCY_FRAMES

VKD3D_SWAPCHAIN_LATENCY_FRAMES has been available since VKD3D-Proton 2.8 (December 2022), when the swapchain was fully rewritten. The v2.8 release notes document it directly: "VKD3D_SWAPCHAIN_LATENCY_FRAMES=n allows user to force a specific amount of latency."v2.8 release It is not a new variable -- what changed in 2.14 was the default. VKD3D-Proton had previously used an internal heuristic that preferred 2 frames of latency; the v2.14 release notes explicitly state this was removed: "Remove old heuristic that preferred 2 frames of latency depending on BufferCount used. The default on DXGI is 3, and using 2 caused some performance issues in various games with GPU starvation, especially on Deck. VKD3D_SWAPCHAIN_LATENCY_FRAMES is still available as an override to force a tighter default."v2.14 release The practical consequence: if you are on 2.14 or later and prefer tighter latency, you now need to set the override explicitly. If you use a frame limiter (via VKD3D_FRAME_RATE) and still experience input lag, setting VKD3D_SWAPCHAIN_LATENCY_FRAMES=2 restores the tighter pre-2.14 default. Conversely, if you are seeing GPU starvation or utilization drops, leave the override unset or try the DXGI default of 3. This is a diagnostic lever, not a universal recommendation. Add it to your Steam launch options alongside your frame limiter: VKD3D_SWAPCHAIN_LATENCY_FRAMES=2 VKD3D_FRAME_RATE=144 gamemoderun %command%.

terminal -- clear shader cache (run after major Reforger updates)
# Reforger Steam App ID: 1874880 (client), 1874900 (dedicated server)
$ rm -rf ~/.cache/vulkan
$ rm -rf ~/.local/share/Steam/steamapps/shadercache/1874880

Warming the Shader Cache Proactively

Clearing the shader cache solves stale-cache problems but introduces a new one: the first session after clearing will experience compilation stalls as VKD3D-Proton encounters and compiles each shader pipeline for the first time. These produce the characteristic 30-60ms frame time spikes in MangoHud while the cache builds.

The practical mitigation is to warm the cache intentionally before a serious multiplayer session. After clearing, or after a major game update, launch a solo session or an empty server and spend 10-15 minutes moving through varied terrain -- open fields, forests, buildings, vehicles, and explosions all trigger different shader pipelines. By the time you join a populated server, the cache is warm and the compilation stalls have already occurred in a context where they do not matter. This is not a workaround unique to Linux; Windows users with DirectX Shader Cache disabled experience the same behavior. On Linux, however, the cache warm-up is more important because VKD3D-Proton compiles pipelines lazily rather than through Bohemia's pre-compiled shader bundles.

ReBAR and VRAM Allocation

Resizable BAR (Base Address Register) allows the CPU to access the full GPU framebuffer directly, rather than being limited to the historical 256MB BAR window. For large games like Reforger that stream many assets, ReBAR can meaningfully improve GPU memory access patterns from the CPU side.

VKD3D-Proton 2.13 refined ReBAR handling for upload heaps. The release notes state: "Now, only > 8 GB cards will get it. On 8 GB cards, we were regularly hitting the upper limits of what the GPU could hold in VRAM, and using ReBAR would be detrimental to performance since there was risk of more important memory being demoted to system memory."v2.13 release This means the threshold is strictly greater than 8GB -- an 8GB card does not qualify; 10GB, 12GB, and 16GB cards do. If you have 10GB, 12GB, or more VRAM, ensure ReBAR is enabled in your BIOS/UEFI under the "Above 4G Decoding" and "Resizable BAR" settings, then verify:

terminal
# NVIDIA: check BAR size
$ nvidia-smi -q | grep -i bar

# AMD: check resource entry (replace XX:00.0 with your GPU PCI address)
$ lspci | grep VGA
$ cat /sys/bus/pci/devices/0000:XX:00.0/resource

If the first BAR entry shows your full VRAM size rather than 256MB, ReBAR is active. For NVIDIA, look for a line like Bar1: 16384 MiB (or matching your card's VRAM) -- if it reads Bar1: 256 MiB, ReBAR is not active and you need to re-check your BIOS settings. For AMD, the first resource line in the output will show a memory range whose size equals your full VRAM when ReBAR is enabled, versus a fixed 256M window when it is not.

Server-Side Configuration: The Linux Dedicated Server

If you are running an Arma Reforger dedicated server on Linux rather than playing the client, the optimization surface is different but equally important. The server binary is a native Linux executable (Steam App ID 1874900), so there is no Proton translation layer involved.

Before tuning anything else, check your file descriptor limit. Reforger's server -- particularly with mods or workshop content enabled -- opens a large number of file handles for asset streaming, log files, and network connections simultaneously. The default Linux limit of 1024 open files per process is frequently too low under player load, producing silent connection failures or asset load errors that do not obviously point to their root cause. Check and raise the limit:

terminal -- check and raise file descriptor limit
# Check current soft/hard limits
$ ulimit -Sn && ulimit -Hn

# Raise for the current session
$ ulimit -n 65536

To make this permanent for the server user, add to /etc/security/limits.conf:

/etc/security/limits.conf
armaserver  soft  nofile  65536
armaserver  hard  nofile  65536

If you are running the server via systemd, the service unit is the right place instead -- add LimitNOFILE=65536 to the [Service] block shown later in this section.

Bohemia's official documentation makes clear that the -maxFPS parameter is critical: it is heavily recommended to limit server FPS and prevent using all available resources.BI wiki The guidance from multiple hosting providers and official documentation aligns around 60 FPS as a sensible cap for general use. A well-structured Linux server launch script should look like this:

start-reforger.sh
#!/bin/bash
while true; do
    ./ArmaReforgerServer \
        -config /home/armaserver/config.json \
        -profile /home/armaserver/profile \
        -maxFPS 60 \
        -nothreads $(( $(nproc) / 2 - 1 )) \
        -cpuCount $(nproc) \
        -logStats 60 \
        -skipIntro \
        -noSplash
    echo "Server stopped. Restarting in 15 seconds..."
    sleep 15
done

The -nothreads parameter tells the server how many worker threads to create for parallel task processing. This should not exceed your physical core count and is typically best set to physical_cores - 1 to leave headroom for OS and network threads. Note that nproc returns the count of logical CPUs (hyperthreads included) -- on a system with 4 physical cores and hyperthreading enabled, $(nproc) returns 8. For -nothreads, you want physical core count minus one; since hyperthreading is typically enabled, $(( $(nproc) / 2 - 1 )) gives physical cores minus one, which matches the script above. This formula assumes SMT (hyperthreading) is enabled. If you have disabled SMT in BIOS for security or latency reasons, nproc already returns physical cores, and the formula would produce a value one lower than intended -- use $(( $(nproc) - 1 )) instead on SMT-disabled systems. The -cpuCount parameter informs the engine of total available logical CPUs and can safely be set to $(nproc) as shown.

The critical performance-affecting parameters under gameProperties in your config.json are:

Note on -cpuCount

The -cpuCount parameter is not listed in Bohemia's official server documentation and its behavior may change between game versions. It is included here as a community-documented option; verify it against the current Bohemia wiki before deploying to production.

A production-grade config.json skeleton for a 32-player server:

config.json
{
  "bindAddress": "0.0.0.0",
  "bindPort": 2001,
  "publicPort": 2001,
  "game": {
    "name": "My Reforger Server",
    "maxPlayers": 32,
    "visible": true,
    "gameProperties": {
      "serverMaxViewDistance": 2000,
      "serverMinGrassDistance": 50,
      "networkViewDistance": 1500,
      "fastValidation": true,
      "battlEye": true
    }
  },
  "operating": {
    "lobbyPlayerSynchronise": true,
    "aiLimit": 20
  }
}

For the server process, consider running it with real-time scheduling priority. This should only be used on dedicated server hardware, not on a desktop system running other workloads concurrently, as it will starve those processes. SCHED_RR priorities range from 1 (lowest) to 99 (highest). Priority 50 is a conservative middle value that gives the server process reliable scheduling without monopolizing the CPU for real-time contexts. Standard Linux userspace daemons run under SCHED_OTHER (CFS), not SCHED_RR, so there is no competing real-time work from userspace on a typical headless server -- the practical ceiling concern is avoiding interference with kernel interrupt handlers and RCU callbacks, which operate outside the standard scheduler priority system. Priority 50 leaves adequate headroom for these:

terminal -- real-time scheduling
# SCHED_RR (round-robin real-time) at priority 50
$ sudo chrt -r 50 ./ArmaReforgerServer -config config.json -maxFPS 60

For a systemd service unit, the equivalent configuration:

/etc/systemd/system/reforger.service
[Unit]
Description=Arma Reforger Dedicated Server
After=network.target

[Service]
Type=simple
User=armaserver
ExecStart=/home/armaserver/ArmaReforgerServer -config config.json -maxFPS 60
Restart=on-failure
RestartSec=15
CPUSchedulingPolicy=rr
CPUSchedulingPriority=50
# Nice= only affects SCHED_OTHER (CFS). Under SCHED_RR it has no scheduling effect.
# Priority is controlled solely by CPUSchedulingPriority above. Remove if preferred.
Nice=-10
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

I/O Scheduler: Your SSD Is Not Being Used Optimally by Default

Linux uses I/O schedulers to manage how block device requests are queued and dispatched. The default scheduler on many distributions for NVMe SSDs is mq-deadline or bfq (Budget Fair Queueing). BFQ in particular adds per-process fairness tracking that imposes latency overhead on a single-process workload like Reforger's, where one process issues the vast majority of I/O requests and there is no meaningful benefit from inter-process fairness accounting. The overhead is not primarily a throughput issue -- NVMe hardware saturates either way -- but a latency predictability issue: BFQ's fairness bookkeeping introduces timing jitter on each I/O dispatch that adds variance to asset streaming operations.

For NVMe SSDs, the correct scheduler is none, which bypasses queue scheduling entirely and lets the hardware's own internal queue management handle ordering:

terminal
# Set scheduler immediately (replace nvme0n1 with your device from lsblk)
$ echo none | sudo tee /sys/block/nvme0n1/queue/scheduler

To make this persistent, create a udev rule in /etc/udev/rules.d/60-scheduler.rules. The second rule also applies none to any SATA SSDs (rotational=0 identifies them as solid-state). Spinning hard drives should use mq-deadline or bfq, not none:

/etc/udev/rules.d/60-scheduler.rules
ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="none"
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="none"

Dirty Page Write-Back Tuning (Server)

If you are running a dedicated server with -logStats 60 or any mod content that writes frequently to disk, the kernel's dirty page flush behavior is worth tuning. By default, the kernel allows a large percentage of RAM to accumulate as dirty (unwritten) pages before flushing, which can cause brief but noticeable I/O stalls when the flush threshold is finally crossed. For servers, tighten these values in /etc/sysctl.d/99-gaming.conf:

/etc/sysctl.d/99-gaming.conf (server additions)
# Start flushing dirty pages when they exceed 5% of RAM
vm.dirty_background_ratio = 5
# Force-flush dirty pages if they reach 10% of RAM (prevents large stall spikes)
vm.dirty_ratio = 10

The defaults are typically dirty_background_ratio=10 and dirty_ratio=20. Tightening them means the kernel flushes more frequently in smaller batches, trading occasional small overhead for the elimination of the large periodic stall. On a desktop client this is rarely noticeable, but on a server writing continuous log stats it can smooth out the simulation tick timing.

GameMode: Automation for the Tuning Above

Feral Interactive's gamemode project automates several of the kernel and CPU tuning steps above. When a game is launched with gamemoderun, GameMode temporarily switches the CPU governor to performance, adjusts process scheduling priority, and optionally applies GPU performance mode settings. It reverts all changes when the game exits.

terminal -- install gamemode
# Fedora/RHEL
$ sudo dnf install gamemode

# Debian/Ubuntu
$ sudo apt install gamemode

# Arch
$ sudo pacman -S gamemode
Note

GameMode does not do everything outlined in this guide -- it does not handle NUMA balancing, hugepages, IRQ affinity, or I/O scheduler tuning. But it handles the CPU governor switch reliably and is safe to use on any system. Think of it as the baseline layer, with everything else in this guide built on top of it.

One important overlap to be aware of: if you have manually set the CPU governor to performance using cpupower frequency-set before launching the game, GameMode's automatic governor switch is redundant -- it will apply the same change and revert it on exit, potentially leaving your governor on its pre-game setting after the session. For permanent performance governor setups (via /etc/default/cpupower), consider removing the governor management from GameMode's config so it does not interfere with your persistent setting. If you are using GameMode as your only governor management tool, no change is needed.

Where GameMode becomes genuinely powerful is its custom_scripts feature in gamemode.ini. You can configure GameMode to automatically run any shell commands on game start and revert them on game exit, which means the NUMA, sysctl, and IRQ changes above can all be automated without making them permanently system-wide. Create or edit ~/.config/gamemode.ini:

Practical Action -- sudoers setup required first

The sudo commands in GameMode's custom scripts run non-interactively -- there is no terminal to type a password into. Without a targeted sudoers entry, the sysctl commands will silently fail. Add this entry with sudo visudo before configuring the scripts below: yourusername ALL=(ALL) NOPASSWD: /usr/bin/sysctl. On older distributions sysctl may live at /usr/sbin/sysctl instead -- verify with which sysctl and use that path. This scopes passwordless sudo to only the sysctl binary, rather than opening unrestricted root access.

~/.config/gamemode.ini
[custom]
# Run on game start
start=sudo sysctl -w kernel.numa_balancing=0 && sudo sysctl -w vm.swappiness=1
# Run on game exit -- revert to defaults
end=sudo sysctl -w kernel.numa_balancing=1 && sudo sysctl -w vm.swappiness=60

Monitoring Your Progress: What Good Looks Like

Tuning without measurement is superstition. Use MangoHud to overlay performance metrics while playing:

Steam launch options -- with MangoHud
MANGOHUD=1 gamemoderun %command% -d3d12

MangoHud's default overlay shows frame time (not just FPS, which is the more honest metric), GPU utilization, VRAM usage, CPU frequency, and CPU utilization per core. What you are looking for is not the highest average frame rate but the lowest frame time variance. A game running at 80 FPS average with frame times varying between 8ms and 25ms will feel worse than a game running at 65 FPS with frame times consistently between 12ms and 14ms.

The default MangoHud overlay is a reasonable starting point, but for Reforger diagnosis specifically you want a configuration that exposes the frame timing graph, GPU clock, VRAM pressure, and CPU clock per core simultaneously. MangoHud reads its config from ~/.config/MangoHud/MangoHud.conf and optionally from a game-specific file at ~/.config/MangoHud/wine-ArmaReforgerSteam.conf (the name is case-sensitive, omits the .exe extension, and must match the actual running executable -- which is ArmaReforgerSteam.exe, not ArmaReforger.exe).MangoHud GitHub A configuration tuned for Reforger performance analysis:

~/.config/MangoHud/wine-ArmaReforgerSteam.conf
# Frame timing graph -- the primary indicator for tuning quality
fps
frametime
frame_timing

# GPU metrics -- watch for saturation (100%) triggering foliage artifacts
gpu_stats
gpu_temp
gpu_core_clock
gpu_mem_clock
vram
gpu_load_change

# CPU metrics -- per-core frequency shows governor behavior in real time
cpu_stats
cpu_temp
cpu_mhz

# System RAM -- large Reforger maps push memory pressure significantly
ram

# wine/Proton layer indicator -- confirms VKD3D-Proton is active
wine
winesync

# Layout
position=top-left
font_size=16
background_alpha=0.5
toggle_hud=Shift_R+F12
fps_sampling_period=500

The winesync field is particularly useful: it displays the active synchronization method (FSYNC, ESYNC, or wineserver), which confirms at a glance whether FSYNC is actually running. The cpu_mhz field shows per-core clock frequency in real time -- if your governor is supposed to be on performance but you see cores dropping to 800MHz during gameplay, something is overriding it.

How to Actually Read the MangoHud Frame-Time Graph

This guide makes extensive use of the phrase "frame time variance" and describes spike patterns throughout the troubleshooting section. This is where those descriptions become concrete. Enable the frame_timing graph in your MangoHud config -- either via the config file above or by adding MANGOHUD_CONFIG=fps_sampling_period=500,frame_timing=1,gpu_load_change=1 to your Steam launch options. The graph appears as a scrolling waveform beneath the FPS counter.

What you are looking at: The Y-axis represents frame delivery time in milliseconds. The graph scrolls left continuously, so the rightmost point is the most recent frame. A flat line near the bottom of the graph is the target -- it means every frame takes approximately the same amount of time to render and deliver. The X-axis is time, not frame count: spikes have both height (how bad the hitch was) and width (how long it lasted).

The 16.67ms / 33.33ms reference lines: At 60 FPS, each frame must complete in 16.67ms. At 30 FPS, 33.33ms. MangoHud's frame timing graph auto-scales its Y-axis, so these values are not always at a fixed position on screen -- but you can set a fixed scale with frame_timing_target=16.67 in your MangoHud config to keep the 60 FPS line at the midpoint. Any spike above that line at 60 FPS is a frame that was late -- a visible hitch if it exceeds roughly 30ms.

Five patterns and what each means for Reforger:

One critical reading note: MangoHud's frame_timing graph displays the time to deliver a frame from the application's perspective -- the interval between successive vkQueuePresentKHR calls. It does not measure the time between the frame appearing on screen and the previous frame appearing on screen (present-to-present latency as seen by the display). Under Wayland with forced V-Sync, these two values can diverge: the application delivers frames at 120 FPS but the display only presents them at 60 Hz, and MangoHud's graph shows the 8.3ms delivery interval while actual display cadence is 16.67ms. This is exactly the situation documented in VKD3D-Proton issue #2639 and is why the gamescope workaround described in the Wayland section is the more reliable fix -- it ensures the frame delivery path MangoHud is watching is also the path that reaches the display.

When MangoHud tells you something is wrong but does not tell you what, the next step is Proton's own debug log. PROTON_LOG=1 writes a combined Wine, VKD3D-Proton, and DXVK log to $HOME/steam-1874880.log (using Reforger's App ID). The Proton GitHub readme documents this variable as "a convenience method for dumping a useful debug log."Proton GitHub For VKD3D-Proton-specific output, VKD3D_DEBUG=warn limits the log to warnings and errors, which is more useful than the default. Do not use VKD3D_DEBUG=trace in production -- it produces gigabytes of output and will itself cause performance problems. Use these for diagnosis only:

Steam launch options -- diagnostic logging
# Combined Proton log (Wine + VKD3D-Proton + DXVK) -- written to ~/steam-1874880.log
PROTON_LOG=1 %command%

# VKD3D-Proton warnings/errors only (add to launch options alongside PROTON_LOG)
PROTON_LOG=1 VKD3D_DEBUG=warn %command%

# Redirect Proton log to a specific directory
PROTON_LOG=1 PROTON_LOG_DIR=/home/yourusername/proton-logs %command%
Note on log file size

With PROTON_LOG=1, log files are overwritten on each launch, not appended. If you need to preserve a log from a specific crash, copy it before relaunching the game. The log for Reforger will always be named steam-1874880.log in your home directory unless you override PROTON_LOG_DIR.

Frame time spikes above 30ms correspond to perceptible hitches. The pattern of the spikes tells you what is causing them -- and each cause points to a different section of this guide:

Enabling Diagnostic Logging When Things Break

If you are experiencing crashes or rendering problems you cannot diagnose from MangoHud alone, Proton and VKD3D-Proton both expose logging interfaces. These produce verbose output and should not be left enabled for normal play -- they add CPU overhead and produce multi-gigabyte log files quickly. Use them only for a targeted session to capture a crash or artifact, then disable them:

Steam launch options -- diagnostic logging
# Enable Proton log output (writes to /tmp/steam-1874880.log)
PROTON_LOG=1 PROTON_LOG_DIR=/tmp %command%

# Enable VKD3D-Proton warnings and errors only (add alongside PROTON_LOG)
VKD3D_DEBUG=warn %command%

# For shader compilation problems specifically
VKD3D_SHADER_DEBUG=warn %command%

The PROTON_LOG output captures Wine/Proton layer activity including DLL loading, registry access, and the synchronization subsystem. The VKD3D_DEBUG=warn level captures Vulkan API warnings and errors without the full trace verbosity of trace. If a crash is VKD3D-Proton specific (game runs on Windows, crashes reproducibly on Linux at the same point), the VKD3D log is the right starting point. If the crash is nondeterministic or involves the launcher, PROTON_LOG is more useful.

The Bigger Picture

What makes Linux gaming on a title like Reforger philosophically interesting is that you are running a Windows application, designed for a Windows API, on a Unix kernel, through a compatibility layer that translates one graphics API to another, while simultaneously asking the Linux kernel's CPU scheduler, memory allocator, I/O stack, and interrupt routing subsystem to not get in the way of any of it.

Every layer in that stack makes decisions autonomously based on general-purpose heuristics. The CPU governor assumes you might want power savings. The NUMA balancer assumes memory locality matters more than stability. The I/O scheduler assumes multiple competing processes need fair access. The irqbalance daemon assumes interrupt distribution is more important than core isolation. All of those assumptions are wrong for a gaming workload, and this guide is, at its core, about overriding them with workload-specific knowledge.

The engineers at Bohemia Interactive who wrote Enfusion's threading model did not have your kernel configuration in mind. Your job as a Linux gamer or server operator is to reduce the friction in all the layers between their work and the actual hardware. That is not a workaround. That is how operating systems are supposed to be used.

Quick Reference Summary

Client-side Linux:

client quick reference
# Steam launch options (requires VKD3D-Proton 3.0b+ for full bug-fix coverage)
# Replace 144 with your monitor's refresh rate
VKD3D_FRAME_RATE=144 gamemoderun %command% -d3d12

# With MangoHud frame-time monitoring enabled
MANGOHUD=1 MANGOHUD_CONFIG=fps_sampling_period=500,frame_timing=1,gpu_load_change=1 VKD3D_FRAME_RATE=144 gamemoderun %command% -d3d12

# AMD GPU only (3.0+): Anti-Lag for reduced input latency
VKD3D_CONFIG=amd_anti_lag VKD3D_FRAME_RATE=144 gamemoderun %command% -d3d12

# Wayland: use gamescope to fix forced V-Sync lock (replace -w/-h/-r with your resolution/refresh rate)
# VKD3D_FRAME_RATE is set slightly below -r to avoid tearing at the refresh boundary -- adjust both to suit your display
gamescope -w 2560 -h 1440 -r 144 -f -- VKD3D_FRAME_RATE=120 gamemoderun %command% -d3d12

# Troubleshooting: disable staggered submit if GPU throughput drops after 2.14+ upgrade
VKD3D_CONFIG=no_staggered_submit VKD3D_FRAME_RATE=144 gamemoderun %command% -d3d12

# CPU governor (set before gaming)
$ sudo cpupower frequency-set -g performance

# Memory (persistent via /etc/sysctl.d/99-gaming.conf)
vm.swappiness=1
kernel.numa_balancing=0
# THP: add transparent_hugepage=always to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub

# I/O scheduler (NVMe)
$ echo none | sudo tee /sys/block/nvme0n1/queue/scheduler

# Shader cache clear (after major Reforger updates)
$ rm -rf ~/.cache/vulkan
$ rm -rf ~/.local/share/Steam/steamapps/shadercache/1874880

Server-side Linux:

server quick reference
./ArmaReforgerServer \
    -config config.json \
    -maxFPS 60 \
    -nothreads $(( $(nproc) / 2 - 1 )) \
    -cpuCount $(nproc) \
    -logStats 60

# Server process priority
$ sudo chrt -r 50 [launch command]

Sources and Further Reading