Skip to content

Latest commit

 

History

History
411 lines (337 loc) · 42.4 KB

File metadata and controls

411 lines (337 loc) · 42.4 KB

Technical Notes

Purpose

This file is the maintainer-facing companion to README.md. Keep user installation guidance in the README and use this document for repo structure, release flow, and implementation conventions.

Project Constraints

FnQuake3 exists to modernize Quake III Arena without losing the properties that make it a long-lived engine target:

  1. Retail Quake III Arena compatibility stays intact.
  2. Demo playback compatibility stays intact.
  3. Performance regressions need a clear reason and measurement.
  4. Platform additions should not silently narrow the supported matrix.

Compatibility-sensitive areas include:

  • demo parsing and recording
  • network protocol behavior
  • filesystem search order and pak loading
  • VM ABI and bytecode execution
  • renderer defaults that affect demo output or deterministic behavior

Repository Layout

  • code/: engine and platform code.
  • docs/: technical docs, upstream reference material, and README templates.
  • version/: shared project version metadata.
  • scripts/: repo-local automation for docs and release packaging.
  • .install/: tracked distribution docs plus generated manifests and package archives.
  • .tmp/: ignored scratch workspace for temporary outputs.

Versioning

The canonical metadata lives in version/fnq3_version.h.

That header feeds:

Current policy:

  • Tagged releases use semantic version tags in the form vX.Y.Z.
  • Manual release runs produce unique version/date/commit tags (e.g. 0.1.0.42-20240403-abc12345) and matching package prefixes without a channel word.
  • The base version in fnq3_version.h should always represent the next intended stable release line.
  • Pending release-note material lives in docs/fnquake3/CHANGELOG.md. Keep the Unreleased section current as work lands; GitHub release entries are the durable published history.
  • Use scripts/changelog.py to extract, clean up, promote, or reset the pending changelog section.

Typical changelog helper usage:

python scripts/changelog.py section --version Unreleased
python scripts/changelog.py section --version Unreleased --clean
python scripts/changelog.py cleanup --version Unreleased
python scripts/changelog.py prepare-release --version 0.1.0 --date 2026-04-25
python scripts/changelog.py clear-unreleased

Docs Flow

The user-facing docs are generated from templates:

Refresh them with:

python scripts/generate_docs.py

That command rewrites:

Release Packaging

The packaging entry point is scripts/release.py. Manual release CI orchestration lives in scripts/manual_release.py.

Typical local usage:

python scripts/manual_release.py summary
python scripts/release.py --channel manual --artifact-root <downloaded-artifacts-dir>
python scripts/release.py --channel release --artifact-root <downloaded-artifacts-dir> --ref-name v0.1.0 --glx-proof-root <reviewed-glx-proof-root>

The script:

  1. refreshes generated docs
  2. stages each platform artifact under .tmp/release/
  3. filters build-system byproducts, debug symbols, editor files, caches, and temporary files out of the staged package
  4. injects only the shared package docs needed by players and maintainers
  5. writes versioned .zip archives into .install/packages/
  6. emits .install/release-manifest.json and .install/SHA256SUMS.txt

Release ZIPs use sorted member names, fixed timestamps, normalized file modes, and atomic replacement so unchanged staged content produces stable checksums. Packaging rejects symbolic links, unsafe cross-platform member names, and overlapping artifact/staging/output roots before any recursive cleanup; it also removes stale ZIPs from the package output directory so wildcard publishing cannot attach an older build.

Manual release publishing builds GitHub release details from the pending changelog, commits, and changed-file summary. If COPILOT_GITHUB_TOKEN is configured, the workflow uses GitHub Copilot release-note cleanup with .github/release-notes-instructions.md; otherwise it falls back to the repo-local GitHub Models prompt in scripts/manual_release.py. After a release is created from a branch, CI resets docs/fnquake3/CHANGELOG.md back to an empty categorized Unreleased template and commits that reset to the release branch.

CI Notes

.github/workflows/release.yml owns main-branch build validation and manual release publishing. .github/workflows/issue-triage.yml owns automated new-issue triage, with maintainer tuning documented in ISSUE_TRIAGE.md.

Expected behavior:

  • pull requests build only
  • main pushes validate the main branch without publishing a release
  • manual workflow_dispatch runs publish a new version/date/commit release for the selected ref
  • published GitHub releases upload archives whose names match the release tag identity
  • Linux release artifacts build inside an Ubuntu 20.04 userspace and run scripts/check_elf_glibc.py --max-glibc 2.31 before upload so hosted runner image changes do not raise the packaged glibc requirement unexpectedly.

Renderer-focused verification lives beside the release packaging flow:

  • docs/fnquake3/FOG_RENDERING.md records the shared analytic fog curve, optional global map-sidecar layer, GLx/Vulkan/RTX execution changes, fixed-function fallback, performance effects, and volumetric-fog compatibility boundary.
  • docs/fnquake3/LIQUID_RENDERING.md documents the opt-in liquid scene capture and overlay, GLx/Vulkan/RTX parity contract, client visual impulse feed, compatibility boundaries, screen-space limitations, and possible higher tiers.
  • docs/fnquake3/RTX_PARITY.md defines relevant Vulkan parity without diluting RTX's identity: native primary-world rays and closest-hit lighting are authoritative, while raster supplies depth, permitted fallback, and the exact overlay for content not yet RT-owned. It records per-map authored-sun selection and unit conversion, native ray-shadowed q3map_surfaceLight emitters, capability-gated RGBA16F scene/bloom storage, final SDR output/capture encoding, intentional secondary-view/entity/translucency limits, strict required-mode failure, and the RTX hardware-promotion matrix.
  • docs/fnquake3/GLX_FINAL_CONTRACT.md is the accepted target ADR for the final GLx replacement renderer: stable C ABI, GLx-owned draw behavior, five product tiers, deterministic pass order, and a scene-linear color pipeline.
  • docs/fnquake3/GLX_COLORSPACE_AUDIT.md records the audited sRGB/linear texture classes, framebuffer-sRGB policy, blending expectations, and screenshot capture color space for GLx color-pipeline work.
  • docs/fnquake3/DLIGHT_SHADOWMAP_ROADMAP.md tracks the GLx/Vulkan dynamic-light shadow-map roadmap, current implementation status, test command, launch helper, and RenderDoc checkpoints.
  • docs/fnquake3/GLX_PROOF_CORPUS.md is the official GLx screenshot/timedemo scene corpus referenced by gate manifests, performance baselines, CI gate-plan artifacts, and release manifests.
  • docs/fnquake3/GLX_VISUAL_DOSSIER.md defines the generated review dossier written beside every GLx sweep manifest, including pipeline flowcharts, backend-state overlays, histograms, false-color sidecars, parity diffs, and driver-tier matrices.
  • docs/fnquake3/GLX_PROMOTION.md and docs/fnquake3/GLX_ROLLBACK_PACKAGE.md preserve the historical GLx promotion and rollback policy. The transition is complete: current public renderer selectors are exactly glx, vk, and rtx, and release packaging rejects renderer modules with any other selector name.
  • .github/workflows/glx-verification.yml builds deterministic GLx logic tests, generates dry-run GLx RC gate artifacts, exposes manual self-hosted GLx runtime sweeps, and runs a scheduled mainline rc-parity sweep on configured self-hosted GPU runners.
  • .github/workflows/vulkan-verification.yml builds modular and static Vulkan and RTX configurations, verifies RTX source/shader/runtime-harness contracts, generates dry-run Vulkan sweep and focused RTX smoke artifacts from scripts/vk_runtime_sweep.py and scripts/rtx_runtime_smoke.py, and exposes manual self-hosted VK and rtx-smoke GPU gates.

Dry-run renderer gate artifacts are planning evidence only. Blocking release evidence requires non-dry-run runtime artifacts on the documented platform matrix with retail baseq3 assets. Tagged release packaging requires --glx-proof-root; the release script revalidates passing rc-smoke, rc-parity, and rc-proof manifests for windows-x64 and linux-x86_64 before it writes the release manifest. Each blocking manifest must carry passing versioned rendererSwitchEvidence for the generated renderer_switch run, including the keep-window CL_Vid_Restart path, every expected map/round/step screenshot transition, GLx diagnostics, and GLx performance samples. The rc-parity and rc-proof manifests must also carry passing versioned worldProofEvidence proving the selected stock/high-geometry/lightmap/fog/visibility world maps, GLx screenshot histograms, static-world draw/index counters, zero static packet misses/fallbacks/errors, and lightmap/fog path evidence. The rc-proof manifest must carry passing versioned materialProofEvidence proving material-stage/tcgen corpus tags, GLx screenshot histograms, material renderer readiness, compile/program activity, zero material failures or unsupported plans, parameter-block fingerprints, required stream-material feature counters, and forbidden screen-map/video-map guards for the conservative proof surface. The rc-proof manifest must also carry passing versioned dynamicProofEvidence proving dynamic entity, first-person weapon, dynamic-light stream/ownership, and planar-shadow corpus coverage with required stream-category/feature counters, tier-support evidence, screenshots or timedemos for the selected dynamic scenes, and zero stream/category fallbacks. The rc-proof manifest must also carry passing versioned postProofEvidence proving greyscale and render-scale corpus coverage with found GLx screenshots, histograms, ready FBO state, positive postprocess frame/screenshot counters, render-scale dimension evidence, no minimized output, direct-final post shader diagnostics, and a valid color contract. The staged rc-stress material proof additionally covers animated-image, screen-map, and video-map stage flags, staged rc-stress dynamic proof covers particle, transient-poly, mark/decal, and beam counters, and staged rc-stress post proof keeps greyscale/render-scale evidence active before those content-sensitive paths can be considered for conservative defaults. Historical promotion proof also required versioned ownershipProofEvidence; current releases use GLx as the default and preserve those artifacts only as verification history. The GLx runtime sweep applies built-in global and per-tier performance budgets by default; use --performance-budget only to add reviewed runner-specific thresholds.

Shadowmapping

Shadowmapping in FnQuake3 is a renderer-local lighting layer. It may improve how maps, effects, and authored light cues read on screen, but it must not change retail demo playback, protocol behavior, VM execution, filesystem search order, map loading, or game-side entity state. Treat every shadowmapping change as visual and compatibility-sensitive at the same time: the visuals are optional, while the render order, asset acceptance, and fallback behavior must stay predictable.

The subsystem is organized around three atlas families:

  • Point-light cubemap atlas: transient gameplay lights, renderer-injected test lights, and point-capable sidecar/static lights. Each selected light owns six atlas faces, and the planner trades light count against face resolution.
  • Directional CSM atlas: sky-sun shadows derived from the active world's q3map_sun, q3map_sunExt, or q3map_sunExt2 shader metadata. Cascades are split, laterally snapped, depth-expanded, rendered, cached where valid, and sampled by a receiver pass after the atlas is published.
  • 2D spotlight atlas: sidecar spot lights and surfacelight proxy lights, including large planar emitters that need representative cone projection, per-light tile sizing, and bounded atlas fill. The backend caches the rendered atlas against a plan/world-caster signature so unchanged frames — and the repeated per-view spot pass on portal/mirror frames — skip the clear and re-render, while entity casters force a redraw.

shadowManager_t is the per-view owner for shadow planning. Front-end scene work collects candidates, rejects invalid or low-value work, assigns atlas regions, records publication state, and writes an ordered pass schedule into the draw command. GLx and Vulkan backends then consume that schedule for depth-only atlas producer passes and sample only manager-published atlas generations in lighting or receiver passes. Backend-global atlas readiness is a compatibility fallback, not the canonical source of truth.

r_shadowCorrectness 1 is the first-pass diagnostic mode for shadow-map contract work. It is intentionally default-off and cheat-gated. When enabled, GLx and Vulkan force the minimal correctness path: point-light shadows only, one selected dynamic light, hard filtering, no spot atlas, no CSM producer or receiver pass, and no alpha-tested shadow casters. Use it to validate raw light-space projection, depth output, compare direction, bias separation, and backend resource readiness before reintroducing filtering, spot/CSM work, or multi-light atlas pressure. While enabled, the end-of-frame diagnostics emit shadow correctness records for the point-shadow atlas: backend depth convention, clear/compare state, publication generation, face viewports and scissors, API-space viewports and scissors, cache/render status, and the full projection/model matrices used to write each face.

The shadow coordinate contract is intentionally explicit:

  • Shadow atlas plans use a top-left tile origin. viewParms_t stores the producer viewport in the renderer's historical lower-left convention by writing atlasHeight - atlasY - tileSize.
  • GLx consumes that lower-left viewParms_t viewport directly. Dynamic-light and CSM sampling compensate by addressing atlas rows from the lower-left texture convention.
  • Vulkan converts the same lower-left viewParms_t viewport back to top-left VkViewport/VkRect2D coordinates, negates the clip-space Y projection row before MVP upload, and samples dynamic-light atlas rows directly. Vulkan CSM still samples 1.0 - light_coord.z because the receiver reconstructs a light-space atlas coordinate from world space rather than from the producer viewport.
  • GLx point and spot shadow depths are forward OpenGL depth ([-1, 1] clip, clear 1.0, GL_LEQUAL). Vulkan point and spot shadow depths are zero-to-one clip depths and currently follow the backend's reversed-depth build (clear 0.0, VK_COMPARE_OP_GREATER_OR_EQUAL). Vulkan CSM remains a forward-depth producer pass (clear 1.0, VK_COMPARE_OP_LESS_OR_EQUAL) and carries the compare direction as a receiver shader mode.

Bias is also a four-part contract, not one interchangeable tuning knob. Receiver bias (r_dlightShadowBias / r_csmShadowBias) is applied only while sampling and is clamped by angle and texel footprint where the shader has that information. Constant caster depth bias and slope-scaled caster bias are applied only while rendering depth producers (glPolygonOffset on GLx, vkCmdSetDepthBias on Vulkan). Caster normal bias is a separate vertex offset before rasterization, using the point/spot light-to-vertex vector or the negated CSM sunlight direction. r_shadowCorrectness 1 prints all four dynamic-light bias values separately so a capture can distinguish receiver compare tuning from producer raster tuning.

Filtering is reintroduced only after the hard-shadow path is known-good. The shared filter contract is: hard mode is one effective center sample represented by four zero-offset taps in the receiver shader, 2x2 PCF uses four half-texel taps, and four-tap Poisson PCF uses the shared 0.25/0.75 inner/outer offset pair. GLx and Vulkan both derive dynamic-light and CSM filter offsets from the same helper, while r_shadowCorrectness 1 still forces the effective filter to hard and reports the requested filter, effective filter, tap count, and offsets in the correctness summary. Ordinary dynamic-light shadow debug output reports the same effective filter fields for production captures.

Keep the GLx and Vulkan paths mechanically aligned. Shared rules include angle-aware caster normal bias, texel-limited receiver bias, bounded filter selection, cascade atlas snapping, atlas publication before sampling, and explicit fallbacks when there is no world, no active sky sun, no atlas, or no valid cascade. For CSM, the light direction is an incoming sunlight vector for planning, while the caster normal-bias helper needs the vector from light to vertex; GLx and Vulkan must both use the negated tr.csm.lightDirection in that path. A sign drift here can produce healthy-looking planner telemetry and still leave the final scene visually unshadowed.

CSM cascade planning intentionally snaps only the atlas-facing light-space axes, not the light-depth axis. Quantizing depth can make static world shadows appear to shimmer or partially remap even when model shadows are aligned. The depth bounds are expanded beyond the receiver slice so world geometry just outside the visible split can still cast into it, while the X/Y atlas footprint stays texel-snapped for stable sampling. Surface culling also keeps a two-texel margin around cascade bounds so edge casters do not pop as the camera crosses snap thresholds.

CSM caster and receiver passes must walk the sorted draw-surface list in the same order as the main backend, changing entity state inline when the sort key changes. Do not regroup CSM work by first collecting entities and then rescanning surfaces per entity: that can change batch/state lifetime from the main draw order and has caused partial or unstable Vulkan world-shadow output even when individual model shadows appear aligned.

r_csmDebug 1 emits both the compact csm shadows summary and one csm cascade line per active cascade. The cascade lines are the Round 6 coordinate evidence: split near/far, atlas tile, renderer-space viewport, API-space viewport, sample-Y rule, clip-Y rule, depth convention, compare rule, light-space bounds, origin, and texel size. GLx reports native sample Y, native clip Y, OpenGL [-1, 1] clip depth, and lequal forward-depth CSM. Vulkan reports inverted CSM sample Y, flipped clip Y, [0, 1] clip depth, and the same forward lequal CSM producer/receiver compare. Keep these lines in sync with the CSM producer and receiver shader contract when changing cascade math.

r_dlightShadowDebug 1, r_spotShadowDebug 1, or r_csmDebug 1 also emits shadow atlas contract lines for the point, spot, and CSM atlases. These Round 7 lines report whether the atlas is active, tile size, atlas dimensions, record count, fill pressure, filter padding in texels, UV clamp inset, sampler wrap policy, and deterministic allocation policy. Point lights allocate by priority with dlightIndex tie-breaks, spot lights allocate by priority with source/source-index tie-breaks, and CSM allocates by cascade index. Current PCF kernels never sample beyond one texel, and the receiver shaders clamp UVs inside the tile before applying taps, so the logged clamp must remain at least as large as the logged filter pad.

The GLx and Vulkan runtime sweep analyzers derive a shadowProfile object from the same debug stream only after the correctness contracts above are present and passing. It records raster work, sampler pressure, pass orchestration, and CPU timing buckets from the dlight, spot, CSM, atlas, and manager samples. Treat profileReady: true as permission to start looking for cost reductions in captures or vendor profilers; a failed profile means the correctness evidence is still incomplete or unstable and should be fixed first.

CSM producer and receiver projection must stay a single contract. GLx writes OpenGL-style atlas depth directly from the planned light-space bounds; Vulkan converts the same projection to Vulkan clip space by negating the full clip-Y row (projectionMatrix[1], [5], [9], and [13]) before MVP upload. The [13] term is important because CSM orthographic projections store the atlas-vertical light-space offset there. Receiver shaders then sample the published atlas with light_coord.y as atlas X. GLx samples light_coord.z as atlas Y because its FBO texture coordinates use the same bottom-left convention as the producer viewport. Vulkan samples 1.0 - light_coord.z because the rendered depth image is addressed with a top-left image origin after the clip-Y conversion. Keep that difference local to the Vulkan CSM receiver shader, and clamp the PCF center inside the cascade tile to avoid cross-cascade taps.

Documentation and evidence are split by audience. Player-facing controls live in docs/DISPLAY.md. The maintainer implementation ledger lives in DLIGHT_SHADOWMAP_ROADMAP.md, with the broader historical plan in docs-dev/plans/2026-06-03-vk-shadowmapping.md. Before enabling real shadow maps by default, require non-dry-run GLx and Vulkan runtime evidence with retail assets, passing release-gate summaries, reviewed screenshots or diffs, and RenderDoc inspection notes for atlas contents, receiver sampling, and combined point/spot/CSM scheduling.

Useful focused checks for ordinary shadowmapping changes:

python tests\shadow_correctness_source_tests.py
python tests\dlight_shadow_bias_tests.py
python tests\shadow_manager_source_tests.py
python tests\vulkan\vk_runtime_sweep_tests.py
python tests\glx\glx_runtime_sweep_tests.py
meson compile -C .tmp\meson-dlight fnquake3_glx_x86_64 fnquake3_vk_x86_64

Audio Backend Notes

  • AUDIO_ENGINE.md is the detailed architecture and source-layout reference for the modern audio engine.
  • The default client audio path is the OpenAL backend selected by s_backend openal.
  • s_backend legacy keeps the original Quake III mixer/device backend available as a fallback path.
  • Client audio now lives under code/client/audio: the stable S_* facade is at the module root, the original mixer is in legacy/, codecs are in codecs/, the OpenAL backend is in openal/, and reusable policy/sidecar headers used by tools and tests are in shared/.
  • OpenAL headers are provided through the Meson openal-soft subproject fallback or a system OpenAL development package. The client still loads the OpenAL runtime dynamically so startup can fall back to the legacy mixer when the runtime is unavailable.
  • On Windows the runtime search prefers OpenAL Soft over the legacy Creative router: executable-directory OpenAL32.dll/soft_oal.dll, the packaged runtime path, a system soft_oal.dll, then the system OpenAL32.dll last. Startup warns when the loaded runtime has no OpenAL Soft extensions, and EFX filter types are probed so runtimes that reject high-pass/band-pass (the router's "Generic Software" driver) degrade tone filters to low-pass instead of silently dropping occlusion/underwater muffling.
  • The runtime reporting cvar is s_backendActive. Device selection for the OpenAL backend uses s_alDevice.
  • The OpenAL backend also exposes s_alReverb, s_alOcclusion, s_alReverbGain, and s_alOcclusionStrength for the environmental spatial layer. Reverb enablement is latched because the EFX reverb slot is created at backend init. The backend prefers AL_EFFECT_EAXREVERB (LF decay, echo, and modulation parameters, including the underwater warble) and falls back to AL_EFFECT_REVERB when the runtime rejects it; s_info reports which effect is active. Listener environment changes blend EFX preset parameters and per-source wet/tone values over a short transition, with the active-to-target environment visible in the spatial debug overlay and s_alDebugDump. Occlusion traces feed a smoothed per-voice target; direct-path attenuation is kept separate from tone-filter sweeps so wall transitions do not zipper. A liquid boundary between source and listener raises the smoothed occlusion floor so cross-water sounds muffle through the same pipeline. The per-voice EFX filters are intentionally limited to low-pass, high-pass, and band-pass presets chosen by source class and occlusion/environment state.
  • Doppler on the OpenAL backend is native two-body OpenAL doppler: s_doppler, s_alDopplerFactor, and s_alDopplerSpeed drive alDopplerFactor/alSpeedOfSound live (no restart), looping sources supply game velocities, and listener velocity is derived from respatialize deltas with teleport rejection, clamping, and smoothing. When EFX is present the listener is calibrated with AL_METERS_PER_UNIT (1 unit = 1 inch) and positional sources apply AL_AIR_ABSORPTION_FACTOR from s_alAirAbsorption for distance-based high-frequency air absorption.
  • The backend enforces the legacy audibility horizon over the clamped OpenAL distance models: positional voices fade across the last stretch of the legacy maximum range, loops beyond it become virtual voices that return their OpenAL source/filters to the pool until back in range, inaudible one-shot starts are skipped, and one-shot eviction prefers the least audible voice before age.
  • The WAV codec validates the RIFF format tag and accepts 8/16/24-bit integer PCM, 32-bit IEEE float, and WAVE_FORMAT_EXTENSIBLE wrappers; 24-bit and float sources are decoded to 16-bit at load/stream time so both backends keep their 8/16-bit contract.
  • Modern OpenAL startup requests are exposed as latched cvars: s_alHrtf, s_alHrtfId, s_alOutputMode, s_alDistanceModel, s_alFrequency, s_alRefresh, s_alMonoSources, s_alStereoSources, s_alOutputLimiter, and s_alSpatializeStereo. Context creation first tries requested modern attributes, then standard source/frequency hints, then default attributes before the outer backend fallback can select legacy. Keep s_info as the canonical place to compare requested values against active runtime/device values. When ALC_SOFT_device_clock is available, s_info should refresh the clock/latency query live so latency diagnostics are current instead of just an init-time snapshot.
  • Runtime OpenAL device ergonomics are intentionally conservative. The backend polls ALC_CONNECTED when ALC_EXT_disconnect is available, reports disconnect state in s_info, and uses ALC_SOFT_reopen_device for s_alRecoverDevice and s_alAutoRecover live recovery attempts. If live reopen/reset is unsupported or fails, snd_restart remains the deterministic full rebuild path. s_alConfigHints is a diagnostic command only; it should point users at OpenAL Soft config-file options such as stereo/HRTF mode, resampler, period, limiter, and surround decoder settings without moving those library-global policies into engine cvars.
  • OpenAL enumeration is available through s_alListDevices and s_alListHrtfs. The HRTF command uses the live OpenAL device when possible and otherwise opens the requested/default device temporarily for diagnostics.
  • Mono world sounds use true OpenAL positional sources driven by Quake listener/source coordinates. They use the active standard OpenAL distance model with reference distance 80, max distance 1330, and rolloff 1. Keep local/UI/announcer, raw/music streams, and authored multi-channel samples non-spatial. Two-channel world samples also stay direct by default and may only enter positional routing through the opt-in s_alSpatializeStereo compatibility switch when AL_SOFT_source_spatialize is available. Stereo and surround samples/streams should request AL_DIRECT_CHANNELS_SOFT when AL_SOFT_direct_channels is available, and prefer AL_REMIX_UNMATCHED_SOFT when AL_SOFT_direct_channels_remix is available so unmatched authored channels are folded into narrower output layouts. AL_EXT_MCFORMATS gates native quad/5.1/6.1/7.1 PCM submission; runtimes without it must keep playing authored surround content through the stereo downmix fallback.
  • The OpenAL backend reads sound/fnq3-weapon-sounds.sndshd as a small idTech4-style sound shader subset. Supported tuning keys include minDistance, maxDistance, volume, volumeDb, shakes, reverb, wetLevel, frequencyShift, and raw sound/... sample entries. Shader distance scaling clamps to reference scale 0.5-2.0 (minDistance up to 160) and range scale 0.5-3.0 (maxDistance up to about 4000); range scale divides the OpenAL rolloff factor, so long-throw sounds also attenuate more gently through the mid field, and the audibility horizon extends with it. The shipped baseq3 and missionpack files live under pkg/<game>/sound/, are packed into FnQuake3-pkg.fnz, and cover standard Quake III Arena plus Team Arena weapon samples with combat-forward gain (+2.3 to +5 dB), full-volume radii up to 160 units, and long carry (roughly 1900-3400 units) plus modest wet-send scaling. Weapon-class tone shaping preserves direct-path highs against the ambient environment cut so gunfire keeps its transient crack.
  • UHJ and B-Format are an opt-in authoring ceiling, not a reinterpretation of normal assets. Registered WAV samples with delimited filename tags such as uhj, uhj3, bformat2d, bformat3d, or ambisonic use AL_SOFT_UHJ or AL_EXT_BFORMAT buffer formats when available. Encoded sound-field samples do not request AL_DIRECT_CHANNELS_SOFT, because their channels are encoded sound-field components rather than authored speaker feeds. Unsupported encoded samples fall back to stereo: UHJ keeps the stereo-compatible first two channels, while B-Format uses the W/omni channel.
  • World voice property updates are batched with AL_SOFT_deferred_updates when available. Keep streaming queue updates outside that batch so music/raw buffer progress remains straightforward to reason about.
  • FNQ3_AUDIO_LOOPBACK_TESTS builds the deterministic audio test targets under tests/audio. fnq3_audio_zone_tests validates .azb runtime parsing, v1/v2 compatibility, zone priority selection, portal blend bounds, and invalid sidecar rejection; CTest registers it as fnq3_audio_zones. fnq3_audio_recovery_tests validates the device-loss policy without real hardware disconnects: poll cadence, retry suppression, one-shot warnings, reconnect notification, disabled auto-recovery, and force/skip decisions; CTest registers it as fnq3_audio_recovery. fnq3_audio_loopback_tests is a headless OpenAL Soft loopback harness that dynamically loads OpenAL, skips with exit code 77 when ALC_SOFT_loopback is unavailable, and otherwise verifies HRTF status visibility and mode switching, distance attenuation, direct-channel isolation, stereo/quad/5.1/6.1/7.1 speaker-layout routing where supported, UHJ/B-Format buffer acceptance where supported, idle silence, and EFX low-/high-/band-pass filters; CTest registers it as fnq3_audio_loopback.
  • AL_SOFT_source_latency is optional. When present, s_alDebugDump should use AL_SEC_OFFSET_LATENCY_SOFT for the selected OpenAL source so voice-level offset/latency diagnostics line up with the device-level clock/latency values printed by s_info.
  • s_alSourceClassDebug is a developer cvar for dump-only source-class aggregation. It should not affect source allocation, routing, filters, or playback state.
  • fnq3-audiozonesc builds the optional audio-zone sidecar compiler under code/tools/audiozones. It compiles maps/<mapname>.audiozones text files into little-endian maps/<mapname>.azb files with AABB zones, preset index, reverb gain, occlusion multiplier, LF/HF tone multipliers, transition time, priority, and a short debug name. It can also generate a first-pass sidecar directly from an IBSP v46/v47 map with --from-bsp, using BSP leaves, clusters, areas, draw surfaces, brushes, shader contents, and surface flags to classify room bounds, environment presets, material class metadata, generated portal hints, and per-portal blend tuning. --material-map <path> layers maintainer shader-pattern overrides into BSP classification; material votes are weighted by whether evidence came from visible draw surfaces, brush bodies, or brush sides, and coarsened zones recompute dominant material metadata from accumulated weights. Use --audit [--samples N] on generated sidecars before listening passes; it runs the client runtime parser, summarizes preset/material/portal/tuning coverage, warns about suspicious overlaps or portal patterns, reports deterministic lookup/profile timing, and emits confidence/anomaly scores for triage. Runtime loading uses normal FS_ReadFile search semantics, so sidecars can live loose or in packages without creating a new asset path.
  • Keep dedicated-server builds free of the OpenAL runtime dependency.

Audio Zone Sidecars

Audio zones are an optional polishing path, not a map requirement. Missing files, invalid files, disabled zones, and listener positions outside every authored zone must fall back to the generic listener-probe environment heuristics.

Runtime behavior:

  • Current map maps/foo.bsp maps to sidecar maps/foo.azb.
  • FnQuake3-pkg.fnz next to the executable has priority for packaged FnQ3 data-only sidecars. The loader tries game-dir-prefixed archive entries first, such as baseq3/maps/foo.azb, baseq3/sound/fnq3-weapon-sounds.sndshd, missionpack/maps/foo.azb, or missionpack/sound/fnq3-weapon-sounds.sndshd, then falls back to normal filesystem and pak search.
  • s_alAudioZones 1 enables sidecar loading; s_alAudioZones 0 forces generic heuristics.
  • The sidecar is rechecked when the active map path or the cvar state changes.
  • Higher priority wins for overlapping zones. Equal priorities prefer the smaller AABB so nested rooms override broader area zones naturally.
  • The selected zone overrides only the audio environment values. It does not affect collision, visibility, demos, protocol, VM behavior, entity state, or asset compatibility.
  • If a selected zone uses the outdoors or underwater preset, or a version 2 sidecar marks it with the matching flag, the corresponding environment flag is set so the existing tone-class logic keeps behaving consistently.
  • s_info, s_alDebugOverlay 2, and s_alDebugDump expose whether zones are active, which material metadata they carried, and which values they contributed.
  • The runtime accepts version 1, version 2, and version 3 .azb sidecars. Version 2 adds material class and portal metadata for generated maps; version 3 adds per-portal blend distance, minimum threshold, maximum crossfade, and blend-curve metadata. Material tuning is baked into the zone values by the compiler, and portal hints provide a bounded crossfade toward adjacent zones near generated boundaries. Version 2 portals inherit the default 192-unit smooth blend, 0.02 minimum threshold, and 0.45 maximum crossfade.
  • Generated BSP zones use negative priorities. Hand-authored overrides merged with fnq3-audiozonesc --from-bsp --merge maps/foo.audiozones maps/foo.bsp therefore win with their default priority 0, while still allowing broader generated fallback coverage.
  • fnq3-audiozonesc --audit --samples 32768 maps/foo.azb is the maintainer-facing preflight for large generated sidecars. Treat warnings, low confidence, and high anomaly scores as prompts for manual listening, merge overrides, or compiler tuning; --strict can make warnings fail in temporary sweep jobs.
  • python scripts/audio_zone_sweep.py --tool path/to/fnq3-audiozonesc --relative-root baseq3 --override-root baseq3 --output-root .tmp/audio-zone-sweeps/baseq3 --strict baseq3/maps is the bulk migration path for map estates. It preserves map-relative output names, merges matching .audiozones overrides, runs --audit on each generated sidecar, and emits JSON/CSV reports for review or CI artifacts. Use --dry-run first when validating a new map tree.
  • Standard Quake III Arena baseq3 arena-map sidecars are tracked under pkg/baseq3/maps/ and packed into FnQuake3-pkg.fnz as baseq3/maps/ entries for installs/releases. The standard and Team Arena weapon sound shaders are tracked under pkg/baseq3/sound/ and pkg/missionpack/sound/, and packed as baseq3/sound/fnq3-weapon-sounds.sndshd and missionpack/sound/fnq3-weapon-sounds.sndshd. Regenerate audio zones from a local retail baseq3 pak directory with python scripts/generate_standard_audio_zones.py --tool path/to/fnq3-audiozonesc path/to/Quake3/baseq3; the helper extracts BSPs into .tmp/ and writes only .azb sidecars back to the package source tree.
  • Optional global-fog sidecars are tracked as text pkg/baseq3/maps/*.fog and pkg/missionpack/maps/*.fog files and packaged at their matching virtual maps/ paths. They are visual-only input for the GLx, Vulkan, and RTX framebuffer renderers; q3tourney5 (Fatal Instinct) deliberately has no preset because it already carries native fog.
  • Material maps use one rule per line: shader/pattern material [preset name] [flag outdoor] [weight N]. Patterns are case-insensitive substrings unless they contain * or ?, where they become simple wildcards. Use them for custom shader packs whose names do not advertise their acoustic material.

The source format intentionally stays small, but it can express the version 2 and version 3 metadata needed for production overrides:

audiozones 1

zone "atrium" {
  bounds -512 -512 -64 512 512 384
  environment hall
  material stone
  flag outdoor
  reverbGain 1.10
  occlusionMultiplier 0.85
  lpfBias 0.95
  hpfBias 1.00
  transitionMs 900
  priority 10

  portal "hallway" {
    bounds 512 -128 -64 512 128 192
    openness 0.80
    blendDistance 128
    minBlend 0.03
    maxBlend 0.35
    curve ease-out
  }
}

Accepted environment names are small-room, room, stone-room, hallway, hall, outdoors, and underwater. bounds may be replaced by separate mins and maxs properties. directHF/wetHF and directLF/wetLF are available when a zone needs separate low-pass or high-pass bias instead of the combined lpfBias/hpfBias shortcuts. material accepts unknown, neutral, stone, metal, liquid, sky, or soft; flag outdoor, flag underwater, outdoor true, and underwater true set runtime environment flags. portal "<target>" { bounds ... openness ... } defines an explicit transition surface. Optional portal tuning accepts blendDistance, minBlend, maxBlend, and curve; curves are smooth, linear, ease-in, or ease-out. Merged override files keep authored materials, flags, portals, and portal tuning while clearing only the internal generated flag.

Audio Migration Expectations

  • Treat the modern audio work as client render-side only. It must not alter demo formats, network protocol behavior, filesystem search order, VM ABI behavior, or which sound assets/mods are accepted.
  • Preserve existing player controls and config behavior when adding OpenAL features. s_backend, s_backendActive, s_alDevice, s_alReverb, s_alOcclusion, s_alReverbGain, s_alOcclusionStrength, s_alAutoRecover, s_doppler, s_info, s_alDebugDump, s_alRecoverDevice, and s_alConfigHints are compatibility surfaces now, not throwaway diagnostics.
  • Keep new OpenAL startup cvars latched and request-oriented. The runtime may legitimately choose a different active HRTF state, output mode, source count, limiter state, frequency, or refresh rate; s_info should remain the canonical requested-vs-active report.
  • Keep fallback deterministic and observable: requested device first, system default device if the requested device cannot be opened, safer OpenAL context attributes next, then legacy backend fallback. Console warnings should explain denied or unsupported requests without treating normal OpenAL capability differences as fatal errors.
  • Keep mono world sounds positional and keep local/UI/announcer, raw/music streams, and stereo samples on the direct non-spatial path by default. s_alSpatializeStereo is an opt-in compatibility escape hatch for two-channel world samples only, and only on runtimes with AL_SOFT_source_spatialize; authored surround layouts must remain direct. UHJ/B-Format tags must stay explicit and additive so existing multichannel content is never silently reclassified.
  • Keep audio-zone sidecars optional and data-only. They may refine environmental rendering, but must not become required content or a gameplay contract.
  • For large map collections, prefer the audio-zone sweep script over one-off shell loops so generation, material-map usage, strict audit status, override usage, warnings, confidence/anomaly scores, and lookup-profile metrics land in reproducible JSON/CSV reports.
  • Update docs/AUDIO.md for player-facing defaults and troubleshooting, and update the README templates rather than hand-editing generated README outputs. After template changes, run python scripts/generate_docs.py.
  • Validate audio-facing migration changes with a normal client build. Run fnq3_audio_zones for sidecar parser/runtime changes and fnq3_audio_recovery for device-loss policy changes; when OpenAL Soft loopback is available, also run fnq3_audio_loopback so HRTF reporting, distance gain, direct stereo routing, idle silence, and EFX filters stay covered.

Naming

Active build, packaging, and distribution surfaces should use FnQuake3 naming consistently. Historical upstream references should only remain where they are part of provenance, copyright notices, or archived material.