Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8b33a52
feat(macos): mount fuse3 backend on macFUSE
dlicudi May 31, 2026
ede963a
feat(macos): unify FUSE unmount + verify ortho union mount
dlicudi May 31, 2026
edb65a8
fix(macos): page-cache virtual DDS so X-Plane mmap doesn't crash
dlicudi Jun 1, 2026
1b38889
fix(macos): force-unmount on shutdown so mount isn't left stale
dlicudi Jun 3, 2026
de10fb6
fix(publisher): macOS portability for archive splitting
dlicudi Jun 11, 2026
2d1d11e
fix(xplane): macOS location for X-Plane install reference file
dlicudi Jun 11, 2026
150933f
fix(cli): auto-detect Custom Scenery in run command
dlicudi Jun 11, 2026
cd40b34
fix(macos): auto-recover stale FUSE mount at startup
dlicudi Jun 12, 2026
dd4128e
docs: macOS is now a supported platform
dlicudi Jun 12, 2026
d4275a3
fix(macos): share virtual-DDS open flags across both filesystems
dlicudi Jun 21, 2026
d5924a8
fix(macos): unify mount detection so package status works on macOS
dlicudi Jun 21, 2026
6deeacd
fix(macos): pin fuse3 to warn so per-syscall logging can't flood
dlicudi Jun 21, 2026
ebcc739
feat(macos): detect total RAM via sysctl hw.memsize
dlicudi Jun 21, 2026
73691df
feat(macos): detect storage type via df + diskutil
dlicudi Jun 21, 2026
14f681b
feat(macos): populate diagnostics hardware/OS/network on macOS
dlicudi Jun 21, 2026
adc7916
test(macos): promote mount spike to macOS-gated macFUSE smoke test
dlicudi Jul 10, 2026
69d07a1
test(cli): cover Custom Scenery path resolution precedence in run
dlicudi Jul 10, 2026
07ec9dc
feat(setup): flag hardware detection failures instead of silent defaults
dlicudi Jul 10, 2026
ad35f91
docs(macos): add install guide and document page-cache visibility tra…
dlicudi Jul 10, 2026
abe1642
docs(macos): repoint fuse3 references to samsoir/fuse3 master
dlicudi Jul 26, 2026
f265899
test(prefetch): make transition throttle ramp tests deterministic
dlicudi Jul 27, 2026
85e6c53
Merge branch 'develop/0.5.0' into feat/macos-port-clean
samsoir Jul 30, 2026
1433590
test(hardware): assert real sysctl RAM on macOS via try_detect
dlicudi Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `StateAggregator` - Combines multiple position sources with accuracy-based selection
- `PositionModel` - Persistent position model refined by best available data
- `WebApiAdapter` - Connects to X-Plane Web API (REST + WebSocket) for position and sim state
- `InferenceAdapter` - Position inference from FUSE file access patterns
- `InferenceAdapter` - Position inference from FUSE file access patterns (macOS caveat: virtual DDS files use the kernel page cache there, so FUSE only sees cold first-reads — steady-state position tracking is effectively Web API-only; see `docs/dev/fuse-filesystem.md`)
- `FlightPathHistory` - Position history for track derivation
- Position sources: Web API (10m), ManualReference (100m), SceneInference (100km)
- Higher accuracy wins; stale high-accuracy can be beaten by fresh lower-accuracy
Expand Down Expand Up @@ -439,8 +439,8 @@ matrix jobs are renamed.
## Platform Support

- **Linux**: ✅ Fully working (native FUSE)
- **macOS**: ✅ Working (requires [macFUSE](https://macfuse.io); Apple Silicon tested; fuse3 pinned to `samsoir/fuse3` `master` (upstream + `configurable-background` plus the macOS reply-resilience patch — upstream PR [Sherlock-Holo/fuse3#137](https://github.com/Sherlock-Holo/fuse3/pull/137) — comma-joined mount options, and reply-drop log demotion); install/approval guide in `docs/macos.md`)
- **Windows**: ⏳ Planned (requires Dokan/WinFSP)
- **macOS**: ⏳ Planned (requires macFUSE)

## References

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ See [How It Works](docs/how-it-works.md) for detailed architecture.
- **Multiple compression backends**: ISPC SIMD (default), pure-Rust software, or GPU-accelerated via wgpu compute shaders
- Real-time dashboard showing cache, download, and prefetch status
- Works with Ortho4XP-generated scenery
- Linux support
- Linux and macOS support

## Quick Start

Expand Down Expand Up @@ -178,7 +178,7 @@ Run `xearthlayer --help` for all options.
## Requirements

- **X-Plane 12**
- **Linux** with FUSE support
- **Linux** with FUSE support, or **macOS** with [macFUSE](https://macfuse.io) installed (see the [macOS guide](docs/macos.md))
- **Modern GPU** with 8GB VRAM or higher, 16GB+ recommended (for X-Plane rendering)
- **Fast Internet connection** for streaming imagery, recommended 800Mbps downstream or better

Expand Down
20 changes: 16 additions & 4 deletions docs/dev/fuse-filesystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,19 +247,31 @@ The regex pattern for recognition:

| Operation | Behavior |
|-----------|----------|
| `open` | Virtual DDS inodes: `FOPEN_DIRECT_IO`; Real passthrough files: default flags |
| `open` | Virtual DDS inodes: `VIRTUAL_DDS_OPEN_FLAGS` (platform-dependent, see below); Real passthrough files: default flags |
| `read` | For DDS: serve from cache/generated; For others: read from source |
| `release` | Clean up handles |

### Direct I/O for Virtual DDS Files
### Open Flags for Virtual DDS Files

`Fuse3OrthoUnionFS` implements `open()` to set `FOPEN_DIRECT_IO` on virtual DDS inodes. This bypasses the kernel page cache for generated textures:
Both filesystems set `VIRTUAL_DDS_OPEN_FLAGS` (single source of truth in
`fuse/fuse3/shared.rs`) on virtual DDS inodes in `open()`. The value is
platform-dependent:

**Linux: `FOPEN_DIRECT_IO`** -- bypasses the kernel page cache for generated textures:

- **Full observability** -- every X-Plane DDS read goes through the FUSE handler, visible to `FuseLoadMonitor`, `SceneTracker`, and `DdsAccessEvent`
- **No stale data** -- page cache cannot serve outdated DDS data after provider changes or cache clears
- **Reduced kernel memory** -- no page cache duplication of data already in the moka memory cache
- Real passthrough files use default kernel caching (unchanged)

**macOS: `0` (page cache enabled)** -- direct I/O breaks X-Plane's `mmap`-based
texture loading under macFUSE (`EXC_BAD_ACCESS` in the sim's texture loader),
so generated textures use normal kernel caching. Consequences:

- **Reduced observability** -- once the kernel caches a DDS file, repeat reads never reach FUSE. `SceneTracker`, `DdsAccessEvent`, and FUSE-load calibration only see the *cold first read* of each tile; `InferenceAdapter` position inference degrades to "tiles seen once since mount", so steady-state position tracking is effectively **Web API-only** on macOS
- **Prefetch is unaffected** in practice: the adaptive prefetcher is driven by Web API telemetry, and cold reads (the ones that matter for generation) always reach FUSE
- Stale-data risk is acceptable: generated DDS content for a given tile/provider is immutable within a run

### Synthesized DDS Attributes

For on-demand DDS files, we synthesize file attributes:
Expand Down Expand Up @@ -512,7 +524,7 @@ The DDS generation system uses a job/task framework:
### Operating Systems

- Linux: Native FUSE support (✅ fully working)
- macOS: Requires macFUSE (⏳ planned)
- macOS: Requires [macFUSE](https://macfuse.io) (✅ working, Apple Silicon tested — see [macOS guide](../macos.md))
- Windows: Requires WinFSP (⏳ planned)

### Scenery Compatibility
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ See [How It Works](how-it-works.md) for a detailed architectural explanation.
## Prerequisites

- **X-Plane 12** (X-Plane 11 may work but is untested)
- **Linux** (Windows and macOS support planned)
- **Linux** with FUSE support, or **macOS** with [macFUSE](https://macfuse.io) — see the [macOS guide](macos.md) (Windows support planned)
- **Internet connection** for streaming imagery
- Basic familiarity with the command line

Expand Down
96 changes: 96 additions & 0 deletions docs/macos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# XEarthLayer on macOS

XEarthLayer supports macOS (Apple Silicon tested) using [macFUSE](https://macfuse.io) to provide the FUSE virtual filesystem that streams textures to X-Plane.

## Requirements

- macOS 12 or later (tested on Apple Silicon; Intel Macs should work but are untested)
- [macFUSE](https://macfuse.io) 5.x or later
- X-Plane 12

## Installing macFUSE

macFUSE ships as a kernel extension (kext), so installation involves a one-time
security approval that is stricter on Apple Silicon Macs.

### 1. Install the package

Download the installer from [macfuse.io](https://macfuse.io) and run it, or use Homebrew:

```bash
brew install --cask macfuse
```

### 2. Approve the kernel extension

**Apple Silicon Macs** must first allow third-party kernel extensions:

1. Shut down the Mac completely.
2. Press and **hold** the power button until "Loading startup options" appears, then choose **Options** to boot into Recovery.
3. Open **Utilities → Startup Security Utility**, select your startup disk, and choose **Reduced Security** with **"Allow user management of kernel extensions from identified developers"** checked.
4. Restart back into macOS.

**All Macs** then approve the extension:

1. Open **System Settings → Privacy & Security**.
2. Under Security you will see a message that system software from developer **"Benjamin Fleischer"** (the macFUSE developer) was blocked — click **Allow** (macOS may ask you to **Enable System Extensions** and restart).
3. Restart when prompted.

After the restart, verify macFUSE is active — the mount helper should exist:

```bash
ls /usr/local/lib/libfuse* /Library/Filesystems/macfuse.fs 2>/dev/null
```

If the approval prompt never appeared, re-run the macFUSE installer after completing the Recovery steps.

### 3. Build and run XEarthLayer

Building from source works exactly as on Linux:

```bash
make release
make install # installs to ~/.local/bin
xearthlayer setup
```

The setup wizard auto-detects your X-Plane installation, total memory (via `sysctl`), and storage type (via `diskutil`) to suggest sensible cache settings.

## Platform Behavior Notes

### Texture caching uses the kernel page cache

On Linux, XEarthLayer serves generated DDS textures with direct I/O so every
read from X-Plane is visible to the streaming service. On macOS, direct I/O
interacts badly with X-Plane's memory-mapped texture loading (it can crash the
simulator), so generated textures are served through the normal kernel page
cache instead.

This is safe and transparent, with one side effect: once a texture is cached by
the kernel, repeat reads never reach XEarthLayer. Position inference from file
access patterns (`InferenceAdapter`) therefore only sees the *first* read of
each tile, so in steady flight XEarthLayer relies on the X-Plane **Web API**
for aircraft position. The Web API connection is automatic and requires no
configuration — just make sure X-Plane's Web API is not disabled. Prefetching
and adaptive scenery loading work normally through it.

### Stale mount recovery

If XEarthLayer is killed without unmounting (crash, force-quit), macFUSE leaves
a stale mount behind and the next run would fail with *"Device not configured
(os error 6)"*. XEarthLayer detects this at startup and force-unmounts the
stale mountpoint automatically. If you ever need to do it manually:

```bash
umount "/path/to/X-Plane 12/Custom Scenery/zzXEL_ortho"
```

## Troubleshooting

| Symptom | Fix |
|---------|-----|
| `mount_macfuse: the file system is not available (255)` | macFUSE kext not approved — redo the approval steps above |
| "System extension blocked" notification | System Settings → Privacy & Security → Allow, then restart |
| No approval option appears (Apple Silicon) | Boot into Recovery and enable Reduced Security with kernel extension management first |
| "Device not configured (os error 6)" on mount | Stale mount from a previous run — recovered automatically at startup; manual fix: `umount <mountpoint>` |
| macFUSE stops working after a macOS upgrade | Re-run the macFUSE installer and re-approve the extension |
74 changes: 62 additions & 12 deletions xearthlayer-cli/src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,18 @@ pub fn run(args: RunArgs) -> Result<(), CliError> {
}

// Get Custom Scenery path (where mounts go)
let custom_scenery_path = config
.packages
.custom_scenery_path
.clone()
.or_else(|| config.xplane.scenery_dir.clone())
.ok_or_else(|| {
CliError::Config(
"No Custom Scenery path configured. \
Run 'xearthlayer init' or set packages.custom_scenery_path in config.ini"
.to_string(),
)
})?;
let custom_scenery_path = resolve_custom_scenery_path(
config.packages.custom_scenery_path.clone(),
config.xplane.scenery_dir.clone(),
|| xearthlayer::config::detect_custom_scenery().ok(),
)
.ok_or_else(|| {
CliError::Config(
"No Custom Scenery path configured. \
Run 'xearthlayer init' or set packages.custom_scenery_path in config.ini"
.to_string(),
)
})?;

if !custom_scenery_path.exists() {
return Err(CliError::Config(format!(
Expand Down Expand Up @@ -400,6 +400,20 @@ fn raise_fd_limit() {
}
}

/// Resolve the Custom Scenery path from configuration, falling back to
/// auto-detection of the X-Plane installation.
///
/// Precedence: `packages.custom_scenery_path` > `xplane.scenery_dir` >
/// auto-detect. Auto-detection is only attempted when both config values
/// are unset.
fn resolve_custom_scenery_path(
custom_scenery_path: Option<PathBuf>,
scenery_dir: Option<PathBuf>,
detect: impl FnOnce() -> Option<PathBuf>,
) -> Option<PathBuf> {
custom_scenery_path.or(scenery_dir).or_else(detect)
}

/// Display warning if configuration file needs upgrade.
///
/// Checks if the user's config.ini is missing settings from the current version
Expand Down Expand Up @@ -440,3 +454,39 @@ fn check_config_upgrade_warning() {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn explicit_custom_scenery_path_wins() {
let resolved = resolve_custom_scenery_path(
Some(PathBuf::from("/configured")),
Some(PathBuf::from("/scenery-dir")),
|| panic!("auto-detect must not run when config is set"),
);
assert_eq!(resolved, Some(PathBuf::from("/configured")));
}

#[test]
fn scenery_dir_used_when_custom_scenery_path_unset() {
let resolved =
resolve_custom_scenery_path(None, Some(PathBuf::from("/scenery-dir")), || {
panic!("auto-detect must not run when scenery_dir is set")
});
assert_eq!(resolved, Some(PathBuf::from("/scenery-dir")));
}

#[test]
fn auto_detect_used_when_both_config_values_unset() {
let resolved = resolve_custom_scenery_path(None, None, || Some(PathBuf::from("/detected")));
assert_eq!(resolved, Some(PathBuf::from("/detected")));
}

#[test]
fn none_when_nothing_configured_and_detection_fails() {
let resolved = resolve_custom_scenery_path(None, None, || None);
assert_eq!(resolved, None);
}
}
22 changes: 17 additions & 5 deletions xearthlayer-cli/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,23 @@ impl CliError {
CliError::Serve(_) => {
eprintln!();
eprintln!("Common issues:");
eprintln!(" 1. FUSE not installed: sudo apt install fuse (Linux)");
eprintln!(" 2. Permissions: You may need to add your user to 'fuse' group");
eprintln!(
" 3. Mountpoint in use: Try unmounting with: fusermount -u <mountpoint>"
);
#[cfg(target_os = "macos")]
{
eprintln!(" 1. macFUSE not installed: download from https://macfuse.io");
eprintln!(
" 2. macFUSE not approved: allow it under System Settings > \
Privacy & Security, then restart"
);
eprintln!(" 3. Stale mount from a previous run: umount \"<mountpoint>\"");
}
#[cfg(not(target_os = "macos"))]
{
eprintln!(" 1. FUSE not installed: sudo apt install fuse (Linux)");
eprintln!(" 2. Permissions: You may need to add your user to 'fuse' group");
eprintln!(
" 3. Mountpoint in use: Try unmounting with: fusermount -u <mountpoint>"
);
}
}
CliError::Publish(_) => {
eprintln!();
Expand Down
3 changes: 2 additions & 1 deletion xearthlayer/src/config/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ pub struct PackagesSettings {
/// Local directory for installed packages (default: ~/.xearthlayer/packages).
pub install_location: Option<PathBuf>,
/// X-Plane Custom Scenery directory for overlay symlinks.
/// If None, auto-detects from xplane.scenery_dir or ~/.x-plane/x-plane_install_12.txt
/// If None, auto-detects from xplane.scenery_dir or the platform's
/// X-Plane install reference file (see [`crate::xplane::paths`]).
pub custom_scenery_path: Option<PathBuf>,
/// Automatically install overlay packages when installing ortho for same region.
pub auto_install_overlays: bool,
Expand Down
Loading