Skip to content

mstits/Quake-Rust-Metal

Repository files navigation

Metal Quake

A port of Quake to native Apple technologies.

A technical proof of concept exploring the rebuilding of a classic rendering and input engine entirely on Apple-native frameworks.

About this repository — a Rust learning exercise. This builds on Metal-Quake and extends it as another exercise to better learn Rust development: the engine's game logic (the server, physics, the QuakeC VM, the client, model/world code) is being incrementally ported from C to Rust and validated bit-for-bit against the C original by a differential test harness, while the Metal/Apple rendering layer described below stays intact.

metal-quake.mp4

What This Is

Metal Quake maps id Software's original 1996 Quake engine onto native Apple technologies. No SDL. No OpenGL. The C engine and Metal renderer carry no third-party dependencies; the Rust port adds one — jemalloc as the global allocator for the engine build.

This is an active work-in-progress that acts as a testbed for Apple platform APIs inside of an existing C codebase.

Current State & Observations

  • Rendering: The Metal path tracer is the sole world renderer — the 1996 software rasterizer has been removed. BSP geometry and dynamic lights are path-traced in Metal, and the path tracer writes an RT depth texture (_pRTDepthTexture). Particles and sprites render in dedicated Metal passes (textured/colored billboards) depth-tested against that RT depth texture so they occlude against the ray-traced world correctly. The legacy 8-bit vid.buffer now carries only the 2D overlay — HUD, console, menu — drawn by the Rust draw/sbar/console/screen modules, and is composited over the path tracer by chroma-key. A hardware Mesh Shader (MTL::MeshRenderPipelineDescriptor) path exists for traditional rasterization of high-poly clusters.
  • Parallel Encoding: The main render loop utilizes dispatch_apply to split the encoding of heavy GPU compute tasks (Raytracing, Denoising) and Render tasks (Compositing, UI) across multiple Apple Silicon P-cores concurrently. Thread-safe GPU synchronization is handled via MTLSharedEvent.
  • Input Robustness: Mouse look exclusively relies on raw CGEvent deltas and programmatic cursor warping, forcefully establishing window focus to survive system-level event hijacking (such as Cmd+Tab or macOS screenshot overlays).
  • Post-Processing: 12-stage GPU fragment shader pipeline with CRT scanlines, Liquid Glass HUD, SSAO, EDR/HDR, ACES tonemapping, depth of field, bloom, and underwater warp — all hot-toggleable from the in-game Video Options menu.
  • GPU Denoising: MPSImageGaussianBlur (with bilateral à-trous fallback) runs entirely on the GPU against the RT output texture — no CPU round-trip. A trained Real-ESRGAN model is not yet loaded; the MPSGraph upscaler path currently performs bilinear 4× as a stand-in, ready to swap in trained weights without touching call sites.

Rust Core Migration

Alongside the Metal renderer, the engine's game logic has been incrementally ported from C to Rust — a parallel effort to learn Rust on a real, demanding codebase. The engine is now almost entirely Rust: 57 modules in rust/quakecore compiled into libquakecore.a and linked into the same binary, validated bit-for-bit against the frozen C original by a differential test harness (27 differential suites, fuzzed and compared on .to_bits() equality). The C version of a module is retired only after its Rust replacement passes byte-exact parity. Only 2 compiled core .c files remainconsole.c and host.c, the irreducible shims (the variadic printf families and setjmp landing pad that stable Rust can't express, forwarding to #[no_mangle] Rust helpers). The entire render front-end is Rust, ported parity-first: r_light (lighting), r_part (the particle system, rand-stream exact), r_sprite (frame groups + the 5-type billboard math), r_sky (the live half; the dead software compositors were deleted), r_efrag (static-entity fragment linking), and finally r_main+r_misc (frame setup, PVS marking, R_RenderView orchestration, and the shared render globals vid_metal.cpp links against) — each slice proven bit-exact against its frozen C over synthetic inputs before retirement. The platform LAN-driver table (net_macos.c) moved into Rust as #[no_mangle] statics — it was pure data whose function pointers already resolved to Rust.

flowchart LR
    subgraph Spine[Compiled C + Apple layer · native by design]
        Host[host.c + console.c shims<br/>variadic printf · setjmp landing pad]
        Apple[Metal RT · PostFX · CoreAudio · net_apple UDP driver]
    end

    subgraph Core[libquakecore.a — Rust · 57 modules]
        Sim[Server + physics<br/>sv_main / phys / move / user · world]
        Client[Client<br/>cl_main / parse / input / demo / tent]
        VM[QuakeC VM<br/>pr_exec · pr_edict · pr_cmds]
        Parse[Parsers / utils<br/>qcommon · model · wad · crc · math]
        Snd[Sound<br/>snd_mix mixer · snd_mem loader · snd_dma driver]
        Rfe[Render front-end<br/>r_main · r_misc · r_light · r_part · r_sprite · r_sky · r_efrag]
        Infra[cmd · cvar · host · keys · console · draw · screen · view · sbar · menu · zone · chase · net_main/dgrm/loop/vcr]
    end

    subgraph Diff[Differential harness · rust/difftest]
        Cref[frozen C reference]
        Cmp[bit-compare every ported fn]
    end

    Host --> Sim & Client & VM & Snd & Infra & Rfe
    Rfe -. camera/PVS · stages particles/sprites + 2D overlay .-> Apple
    Apple -. calls .-> Snd & Sim
    Core -. each fn .-> Cref --> Cmp
Loading

Where it stands (the PLAN.md phases):

Phase Scope Status
0 — Scaffolding build seam · bindgen/cbindgen FFI · diff harness
1 — Pure leaves mathlib, crc
2 — Untrusted parsers qcommon MSG/SZ, cl_parse, model, wad
3 — QuakeC VM pr_exec, pr_edict, pr_cmds
4 — Simulation pivot world, sv_move, sv_phys + the edict arena moved into Rust ownership
beyond — spine + client + net + sound cmd, cvar, host, keys, console, draw, sbar, screen, view, menu; the client (cl_main/input/parse/demo/tent); the net drivers (net_main/dgrm/loop/vcr); the whole sound system (snd_dma/mix/mem)
renderer — rasterizer kill software rasterizer deleted; world rendered solely by the Metal path tracer; particles + sprites moved to dedicated Metal passes

What the port unlocks (per PLAN.md): a memory-safe engine — malicious .bsp/.mdl/.wav/packets and untrusted QuakeC become clean, bounds-checked errors instead of memory corruption; a fearlessly parallel simulation — the edict arena is now Rust-owned, and SV_Move collision scales ~14× across cores, bit-identical to single-threaded (demo-safe — see PIVOT.md); and a sandboxed QuakeC VM. The Metal/Apple layer below stays native.


Feature Status

Features are categorized honestly:

  • Shipped — Compiled, linked, and actively running in the game loop every frame
  • In Motion — Built but disabled for stability, or partially integrated
  • Planned — Design intent only, not compiled into binary
Layer Apple Framework Status What It Does
Rendering Metal Shipped Metal device, texture pipeline, path-traced world + Metal particle/sprite passes, chroma-key compositor over a 2D overlay
Ray Tracing Metal RT Shipped BLAS from BSP geometry, RT intersection, dynamic GI + emissive surfaces
Post-Processing Metal Fragment Shipped CRT scanlines, Liquid Glass HUD, SSAO, EDR/HDR, ACES tonemapping, DoF, bloom
Upscaling MetalFX Shipped Temporal (640→1280, Halton jitter + RT depth/motion) — presented in the composite
Legacy Audio Core Audio Shipped Lock-free ring buffer, async pull model
Spatial Audio PHASE Shipped Physically-modeled sound with BSP triangle-mesh occluder, per-environment distance model (water/slime/lava/air), raw PCM float32 conversion
Mouse Input CGEvent Shipped Raw delta input, continuous cursor warping, robust focus survival
Keyboard Carbon / NSEvent Shipped Full key mapping
Controllers GameController Shipped DualSense + Xbox — sticks, triggers, D-pad, weapon-aware adaptive triggers (reprogrammed on weapon switch), optional gyro aim, low-battery warnings
Threading GCD Shipped dispatch_apply parallel command buffer encoding and BSP leaf marking
Networking Network.framework Shipped UDP driver with NWConnection/NWListener, Multipath UDP, per-packet sender endpoint extraction, pending-connection queue for UDP_CheckNewConnections
UI SwiftUI Shipped NSPanel launcher overlay, full settings bridge to engine cvars
Settings Sync UserDefaults Shipped Cross-syncs @AppStorage values to engine variables; all 28 struct fields round-trip to id1/metal_quake.cfg on engine start/stop
SVGF Denoise Compute Shipped r_svgf cvar: mode 1 temporal reprojection, mode 2 full variance-guided (RG16F moments + R16F variance + dedicated svgfVariance kernel)
Frame Interpolation MetalFX In Motion MTLFXFrameInterpolator encode pass runs (r_frameinterp); the synthesized frame is not yet presented
BLAS Split Metal RT Shipped r_rt_split_blas cvar: world BLAS cached per map + entity BLAS per frame + 2-instance IAS with per-instance metadata offsets for correct TriTexInfo lookup
ReSTIR DI Compute Shipped r_restir cvar: CPU builds emissive-triangle list from 8×8 atlas-grid sampling; shader does 4-candidate RIS plus temporal (motion-reprojected) and spatial (neighbor) reservoir reuse persisted in ping-pong device buffers — true ReSTIR, converging with one shadow ray per pixel
Argument Buffers Metal Shipped 6 RT device pointers (vertices, indices, triTexInfos, dynLights, instanceOffsets, emissiveTris) wrapped in a single MTLArgumentEncoder buffer at slot 5 with per-resource useResource: annotation
PostFX Function Constants Metal Shipped 5 [[function_constant]] toggles (SSAO, CRT, Liquid Glass, chromatic aberration, high-contrast HUD); a settings-mask-keyed pipeline cache builds + selects the specialized variant each frame so disabled stages are dead-code-eliminated, not runtime-branched
GPU Denoising MPS / MPSGraph Shipped Bilateral à-trous (primary) + MPSImageGaussianBlur (opt-in via MQ_MPS_DENOISE=1 env)
CoreML Upscaler MLModel + ANE Shipped MLModel.modelWithContentsOfURL: loads MQ_RealESRGAN.mlmodelc and runs on ANE at 320×240 baked input; MPSGraph (bilinear + unsharp conv) fallback for other sizes
MetalFX Temporal MetalFX Shipped 640×480 → 1280×960 upscale with Halton(2,3) jitter + RT depth + motion vectors
MTLResidencySet Metal Shipped macOS 15+ pins atlas + meshlet buffers as resident via MQ_Residency.m shim
Mesh Shaders Metal 3.1 Shipped High-poly BSP clustering with Object-Shader frustum culling + distance-based LOD
BLAS Refit Metal RT Shipped refitAccelerationStructure on stable-topology frames (3-5× faster than rebuild)
Shader Caching MTLBinaryArchive Shipped Zero-stutter implicit caching; serialized to disk on VID_Shutdown
Crash Reporting MetricKit Shipped MXMetricManager subscriber writes JSON payloads to ~/Library/Application Support/MetalQuake/
SharePlay GroupActivities Shipped QuakeGroupActivity: GroupActivity with MQSharePlayManager observer; auto-connect on incoming session joins
Game Center GameKit Shipped Authentication sheet presented on the game window; achievement + leaderboard submission on intermission
OS Integration Game Mode Shipped Bundled .app with LSApplicationCategoryType + NSGameMode for doubled Bluetooth polling + GPU priority

Performance

Measure with the bench harness — it forces fullscreen, which matters: windowed presents go through the WindowServer compositor, which paces nextDrawable to the panel's adaptive refresh (~100 Hz), so every windowed timedemo reads ~99 fps regardless of engine cost.

./tests/bench_timedemo.sh                              # 3 runs each of demo1/2/3
MQ_BENCH_ARGS="+r_rt_split_blas 0" ./tests/bench_timedemo.sh 5 demo1   # A/B a cvar

Representative fullscreen results (M4, 640×480 internal RT → MetalFX temporal upscale, all effects active): ~150-175 fps across demo1/demo2/demo3. The frame is presentation/GPU-latency bound — engine CPU cost per frame is ~0.3 ms (r_gpuprofile 1 prints the full split: BuildRTX phases, semaphore/drawable waits, CPU encode, and true GPU time per command buffer).

Per-frame acceleration-structure work is the headline economy: the world BLAS is cached per map (r_rt_split_blas 1, the default), only the small entity BLAS refits per frame, and no CPU wait exists anywhere in the build path — the legacy whole-scene rebuild cost ~6.5-6.9 ms/frame of the same budget.

Note

Benchmarks reflect the engine at full tilt: Path-Traced GI, Neural Denoising, MetalFX Temporal Upscaling, ACES Tonemapping, CRT Scanlines, and Parallel Command Encoding all active.


Architecture

The Metal path tracer is the sole world renderer. R_RenderView gathers particles and sprites for their dedicated Metal passes and draws the 2D overlay (HUD / console / menu) into the 8-bit vid.buffer; a Metal compute kernel path-traces the world. A fragment shader composites the two by chroma-key (palette index 254 reveals the path tracer) and runs the 12-stage PostFX pipeline.

flowchart TB
    subgraph Launcher[SwiftUI Launcher]
        MQ[MQSettingsStore AppStorage]
    end

    subgraph Engine[Engine core — Rust libquakecore.a + thin C shims]
        Host[Host_Frame]
        Physics[SV_Physics · Rust-owned edicts]
        RenderView[R_RenderView gathers particles/sprites + 2D overlay → vid.buffer]
    end

    subgraph RT[Metal RT path]
        Build[BuildRTXWorld]
        IAS[Instance AS unified / split]
        Kernel[raytraceMain compute]
        SVGF[SVGF reproject + variance]
        Bilateral[Bilateral atrous]
        MFX[MetalFX Temporal + Frame Interp]
    end

    subgraph Passes[Metal billboard passes]
        Particles[Particle pass — depth-tested vs RT depth]
        Sprites[Sprite pass — depth-tested vs RT depth]
    end

    subgraph Composite[Composite + PostFX fragment]
        Chroma[Chromakey index 254]
        PostFX[12 stage PostFX]
    end

    subgraph Audio
        CA[Core Audio ring]
        PHASE[PHASE BSP occluder]
        Haptics[CHHaptic + DualSense]
    end

    subgraph Input
        NSE[NSEvent + FlagsChanged]
        CG[CGEvent mouse deltas]
        Ctrl[GCController + gyro]
    end

    MQ -->|bridge sync| Host
    Host --> Physics --> RenderView
    Host --> Build --> IAS --> Kernel --> SVGF --> Bilateral
    RenderView --> Particles & Sprites
    Bilateral --> Particles --> Sprites --> MFX

    RenderView -->|2D overlay| Chroma
    MFX --> Chroma
    Chroma --> PostFX --> Drawable[present drawable]

    Host --> CA
    Host --> PHASE
    Host --> Haptics

    NSE --> Host
    CG --> Host
    Ctrl --> Host
Loading

Deep dives: TECHNICAL.md covers acceleration-structure topology, SVGF data flow, argument buffers, ReSTIR DI, PHASE BSP occluder, the bridge/launcher design, and the complete cvar reference — each section has its own mermaid diagram.


Build

./build.sh
open build/Quake.app

Requirements:

  • Apple Silicon Mac (M1+)
  • macOS 14.0+ (Sonoma/Sequoia/Tahoe)
  • Xcode.app with Metal Toolchain: xcodebuild -downloadComponent MetalToolchain
  • id1/pak0.pak (user-provided — no game assets included)

New Cvars

Beyond Quake's classic console variables, the Apple Silicon port adds:

Cvar Default Description
vid_rtx 1 Toggle path-traced world
sv_parallel 1 0 serial · 1 hybrid parallel sim (default; gated by tests/verify_sim.sh) · 2 hybrid + per-frame serial-vs-parallel self-check
sv_statehash 0 Per-frame deterministic sim checksum (STATEHASH lines); 2 adds per-entity EDUMP
sv_simframes 0 Self-terminate a scripted run after N hashed frames (verify-sim harness)
sv_debris 0 QC bounce entities coming to rest leave permanent engine-owned debris chunks, simulated by the parallel set
sv_simprof 0 Once-a-second SV_Physics ms/frame readout
r_svgf 0 0 off · 1 temporal reprojection · 2 full variance-guided SVGF
r_frameinterp 0 MetalFX Frame Interpolation encode pass (macOS 15+)
r_rt_split_blas 1 Split world (cached per map) + entity BLAS with 2-instance IAS + offset indirection; 0 = legacy whole-scene BLAS
r_gpuprofile 0 60-frame avg perf split: BuildRTX phases + semaphore/drawable/encode/GPU times
r_restir 0 ReSTIR DI reservoir sampling over emissive world triangles
vid_vsync 0 CAMetalLayer.displaySyncEnabled
vid_fullscreen 0 Toggle native fullscreen via -[NSWindow toggleFullScreen:]
joy_gyro_enabled 0 Add controller gyro deltas to view angles
joy_gyro_yaw 30.0 Gyro yaw sensitivity
joy_gyro_pitch 20.0 Gyro pitch sensitivity
joy_sensitivity 1.0 Global stick / gyro sensitivity scale
showfps 0 Top-right FPS overlay (0.25 s smoothing window)

Console commands: mq_info dumps hardware + feature state, dumpcvars lists every registered cvar.

Environment: MQ_MPS_DENOISE=1 opts into the GPU Gaussian denoise path over the default bilateral à-trous. MQ_SIGN_IDENTITY="Developer ID Application: …" selects a real code-signing identity instead of the ad-hoc - that build.sh uses by default.

The in-game Video Options menu exposes the main toggles live; the SwiftUI launcher mirrors the full set via @AppStorage and writes id1/metal_quake.cfg on Apply.

Caution

This repository contains no proprietary game assets. You must provide your own id1/pak0.pak.


Project Structure

Metal_Quake/
├── src/core/                     # id Tech 1 engine core — only 2 compiled .c files left (the console + host shims); the rest stay on disk as the diff-test reference
├── rust/                         # Rust port — links into the same binary as libquakecore.a
│   ├── quakecore/                #   ~51 ported engine modules (spine · sim · client · QuakeC VM · net · sound · parsers)
│   ├── difftest/                 #   differential parity harness — frozen C reference vs Rust
│   └── bench/                    #   parallel-sim benchmark (SV_Move ~14× across cores)
├── src/macos/                    # Apple platform layer + variadic/setjmp shims (32 files)
│   ├── sys_macos.m               # macOS system layer + event loop
│   ├── vid_metal.cpp             # Metal rendering, PostFX pipeline, 12-item menu, GCD parallel dispatch
│   ├── mq_particles.c            # CPU→GPU bridge for the Metal particle pass
│   ├── mq_sprites.c              # CPU→GPU bridge for the Metal sprite pass
│   ├── d_stubs.c                 # no-op D_* device layer (software raster deleted)
│   ├── com_va.c / pr_runerror.c # variadic vsprintf shims (not stable-Rust expressible)
│   ├── host_native.c            # platform-glue host functions forwarding to Rust
│   ├── rt_shader.metal           # RT intersection + GI compute kernel
│   ├── Metal_Renderer_Main.cpp   # Settings init/save/load lifecycle
│   ├── Metal_Settings.h          # MetalQuakeSettings struct definition
│   ├── MQ_MeshShaders.metal      # Object/mesh/fragment pipeline (M3+)
│   ├── MQ_LiquidGlass.metal      # Refractive glass compositor
│   ├── MQ_PHASE_Audio.m          # PHASE spatial audio with dynamic float32 buffers
│   ├── MQ_CoreML.m               # MPSGraph denoiser + upscaler
│   ├── MQ_Ecosystem.m            # Game Center + SharePlay + Accessibility
│   ├── MetalQuakeLauncher.swift  # SwiftUI launcher
│   ├── net_apple.cpp             # Network.framework UDP driver (Multipath)
│   ├── snd_coreaudio.cpp         # Core Audio ring buffer
│   ├── in_gamecontroller.mm      # GameController + DualSense Adaptive Triggers + Core Haptics
│   ├── GCD_Tasks.m               # Parallel dispatch utilities
│   └── Sys_Tahoe_Input.mm        # Unified input architecture
├── metal-cpp/                    # Vendored Apple metal-cpp headers
├── build.sh                      # Single-command build (clang, arm64) → build/Quake.app
└── id1/                          # Game data (user-provided)

Controller Mapping

Full gamepad support for DualSense, Xbox, and MFi controllers:

Button Action
Right Trigger Fire (Adaptive Resistance on DualSense)
Left Trigger / A Jump
Y / Right Bumper Next weapon
Left Bumper Previous weapon
B Swim down
X Use / Interact
Menu Pause (Escape)
Left Stick Move
Right Stick Look
D-pad Move (alternate)

Core Haptics & DualSense Adaptive Triggers

Every weapon has a distinct haptic profile and adaptive trigger resistance tuned for its feel:

Weapon Trigger Pull (DualSense) Haptic Rumble Feel
Axe None Sharp thud
Shotgun Heavy pull, sudden break Medium punch
Super Shotgun Heavy pull, sudden break Heavy double-tap
Nailgun Continuous machine-gun vibration Light rapid
Super Nailgun Continuous machine-gun vibration Medium rapid
Grenade Launcher Max resistance Deep thump
Rocket Launcher Max resistance Heavy kick
Lightning Gun Smooth, constant resistance Sustained buzz

Damage feedback scales proportionally. Nearby explosions produce distance-attenuated low-frequency feedback.


License

GPLv2 — Fork of the Quake source code originally released by id Software.

Quake is a registered trademark of id Software / ZeniMax Media / Microsoft.