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.
FnQuake3 exists to modernize Quake III Arena without losing the properties that make it a long-lived engine target:
- Retail Quake III Arena compatibility stays intact.
- Demo playback compatibility stays intact.
- Performance regressions need a clear reason and measurement.
- 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
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.
The canonical metadata lives in version/fnq3_version.h.
That header feeds:
- runtime version strings via
code/qcommon/q_shared.h - Windows resource metadata via
code/win32/win_resource.rc - Meson and Make version reporting
- documentation rendering
- manual and tagged release archive naming
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.hshould always represent the next intended stable release line. - Pending release-note material lives in
docs/fnquake3/CHANGELOG.md. Keep theUnreleasedsection current as work lands; GitHub release entries are the durable published history. - Use
scripts/changelog.pyto 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-unreleasedThe user-facing docs are generated from templates:
Refresh them with:
python scripts/generate_docs.pyThat command rewrites:
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:
- refreshes generated docs
- stages each platform artifact under
.tmp/release/ - filters build-system byproducts, debug symbols, editor files, caches, and temporary files out of the staged package
- injects only the shared package docs needed by players and maintainers
- writes versioned
.ziparchives into.install/packages/ - emits
.install/release-manifest.jsonand.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.
.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
mainpushes validate the main branch without publishing a release- manual
workflow_dispatchruns 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.31before 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.mdrecords 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.mddocuments 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.mddefines 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-shadowedq3map_surfaceLightemitters, 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.mdis 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.mdrecords 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.mdtracks the GLx/Vulkan dynamic-light shadow-map roadmap, current implementation status, test command, launch helper, and RenderDoc checkpoints.docs/fnquake3/GLX_PROOF_CORPUS.mdis 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.mddefines 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.mdanddocs/fnquake3/GLX_ROLLBACK_PACKAGE.mdpreserve the historical GLx promotion and rollback policy. The transition is complete: current public renderer selectors are exactlyglx,vk, andrtx, and release packaging rejects renderer modules with any other selector name..github/workflows/glx-verification.ymlbuilds deterministic GLx logic tests, generates dry-run GLx RC gate artifacts, exposes manual self-hosted GLx runtime sweeps, and runs a scheduled mainlinerc-paritysweep on configured self-hosted GPU runners..github/workflows/vulkan-verification.ymlbuilds modular and static Vulkan and RTX configurations, verifies RTX source/shader/runtime-harness contracts, generates dry-run Vulkan sweep and focused RTX smoke artifacts fromscripts/vk_runtime_sweep.pyandscripts/rtx_runtime_smoke.py, and exposes manual self-hosted VK andrtx-smokeGPU 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 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, orq3map_sunExt2shader 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_tstores the producer viewport in the renderer's historical lower-left convention by writingatlasHeight - atlasY - tileSize. - GLx consumes that lower-left
viewParms_tviewport directly. Dynamic-light and CSM sampling compensate by addressing atlas rows from the lower-left texture convention. - Vulkan converts the same lower-left
viewParms_tviewport back to top-leftVkViewport/VkRect2Dcoordinates, negates the clip-space Y projection row before MVP upload, and samples dynamic-light atlas rows directly. Vulkan CSM still samples1.0 - light_coord.zbecause 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, clear1.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_64AUDIO_ENGINE.mdis 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 legacykeeps the original Quake III mixer/device backend available as a fallback path.- Client audio now lives under
code/client/audio: the stableS_*facade is at the module root, the original mixer is inlegacy/, codecs are incodecs/, the OpenAL backend is inopenal/, and reusable policy/sidecar headers used by tools and tests are inshared/. - OpenAL headers are provided through the Meson
openal-softsubproject 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 systemsoft_oal.dll, then the systemOpenAL32.dlllast. 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 usess_alDevice. - The OpenAL backend also exposes
s_alReverb,s_alOcclusion,s_alReverbGain, ands_alOcclusionStrengthfor the environmental spatial layer. Reverb enablement is latched because the EFX reverb slot is created at backend init. The backend prefersAL_EFFECT_EAXREVERB(LF decay, echo, and modulation parameters, including the underwater warble) and falls back toAL_EFFECT_REVERBwhen the runtime rejects it;s_inforeports 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 ands_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, ands_alDopplerSpeeddrivealDopplerFactor/alSpeedOfSoundlive (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 withAL_METERS_PER_UNIT(1 unit = 1 inch) and positional sources applyAL_AIR_ABSORPTION_FACTORfroms_alAirAbsorptionfor 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_EXTENSIBLEwrappers; 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, ands_alSpatializeStereo. Context creation first tries requested modern attributes, then standard source/frequency hints, then default attributes before the outer backend fallback can select legacy. Keeps_infoas the canonical place to compare requested values against active runtime/device values. WhenALC_SOFT_device_clockis available,s_infoshould 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_CONNECTEDwhenALC_EXT_disconnectis available, reports disconnect state ins_info, and usesALC_SOFT_reopen_devicefors_alRecoverDeviceands_alAutoRecoverlive recovery attempts. If live reopen/reset is unsupported or fails,snd_restartremains the deterministic full rebuild path.s_alConfigHintsis 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_alListDevicesands_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 distance1330, and rolloff1. 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-ins_alSpatializeStereocompatibility switch whenAL_SOFT_source_spatializeis available. Stereo and surround samples/streams should requestAL_DIRECT_CHANNELS_SOFTwhenAL_SOFT_direct_channelsis available, and preferAL_REMIX_UNMATCHED_SOFTwhenAL_SOFT_direct_channels_remixis available so unmatched authored channels are folded into narrower output layouts.AL_EXT_MCFORMATSgates 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.sndshdas a small idTech4-style sound shader subset. Supported tuning keys includeminDistance,maxDistance,volume,volumeDb,shakes,reverb,wetLevel,frequencyShift, and rawsound/...sample entries. Shader distance scaling clamps to reference scale 0.5-2.0 (minDistanceup to 160) and range scale 0.5-3.0 (maxDistanceup 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 shippedbaseq3andmissionpackfiles live underpkg/<game>/sound/, are packed intoFnQuake3-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, orambisonicuseAL_SOFT_UHJorAL_EXT_BFORMATbuffer formats when available. Encoded sound-field samples do not requestAL_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_updateswhen available. Keep streaming queue updates outside that batch so music/raw buffer progress remains straightforward to reason about. FNQ3_AUDIO_LOOPBACK_TESTSbuilds the deterministic audio test targets undertests/audio.fnq3_audio_zone_testsvalidates.azbruntime parsing, v1/v2 compatibility, zone priority selection, portal blend bounds, and invalid sidecar rejection; CTest registers it asfnq3_audio_zones.fnq3_audio_recovery_testsvalidates 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 asfnq3_audio_recovery.fnq3_audio_loopback_testsis a headless OpenAL Soft loopback harness that dynamically loads OpenAL, skips with exit code77whenALC_SOFT_loopbackis 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 asfnq3_audio_loopback.AL_SOFT_source_latencyis optional. When present,s_alDebugDumpshould useAL_SEC_OFFSET_LATENCY_SOFTfor the selected OpenAL source so voice-level offset/latency diagnostics line up with the device-level clock/latency values printed bys_info.s_alSourceClassDebugis a developer cvar for dump-only source-class aggregation. It should not affect source allocation, routing, filters, or playback state.fnq3-audiozonescbuilds the optional audio-zone sidecar compiler undercode/tools/audiozones. It compilesmaps/<mapname>.audiozonestext files into little-endianmaps/<mapname>.azbfiles 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 normalFS_ReadFilesearch 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 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.bspmaps to sidecarmaps/foo.azb. FnQuake3-pkg.fnznext to the executable has priority for packaged FnQ3 data-only sidecars. The loader tries game-dir-prefixed archive entries first, such asbaseq3/maps/foo.azb,baseq3/sound/fnq3-weapon-sounds.sndshd,missionpack/maps/foo.azb, ormissionpack/sound/fnq3-weapon-sounds.sndshd, then falls back to normal filesystem and pak search.s_alAudioZones 1enables sidecar loading;s_alAudioZones 0forces generic heuristics.- The sidecar is rechecked when the active map path or the cvar state changes.
- Higher
prioritywins 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
outdoorsorunderwaterpreset, 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, ands_alDebugDumpexpose whether zones are active, which material metadata they carried, and which values they contributed.- The runtime accepts version 1, version 2, and version 3
.azbsidecars. 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.bsptherefore win with their default priority0, while still allowing broader generated fallback coverage. fnq3-audiozonesc --audit --samples 32768 maps/foo.azbis 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;--strictcan 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/mapsis the bulk migration path for map estates. It preserves map-relative output names, merges matching.audiozonesoverrides, runs--auditon each generated sidecar, and emits JSON/CSV reports for review or CI artifacts. Use--dry-runfirst when validating a new map tree.- Standard Quake III Arena
baseq3arena-map sidecars are tracked underpkg/baseq3/maps/and packed intoFnQuake3-pkg.fnzasbaseq3/maps/entries for installs/releases. The standard and Team Arena weapon sound shaders are tracked underpkg/baseq3/sound/andpkg/missionpack/sound/, and packed asbaseq3/sound/fnq3-weapon-sounds.sndshdandmissionpack/sound/fnq3-weapon-sounds.sndshd. Regenerate audio zones from a local retailbaseq3pak directory withpython scripts/generate_standard_audio_zones.py --tool path/to/fnq3-audiozonesc path/to/Quake3/baseq3; the helper extracts BSPs into.tmp/and writes only.azbsidecars back to the package source tree. - Optional global-fog sidecars are tracked as text
pkg/baseq3/maps/*.fogandpkg/missionpack/maps/*.fogfiles and packaged at their matching virtualmaps/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.
- 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, ands_alConfigHintsare 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_infoshould 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_alSpatializeStereois an opt-in compatibility escape hatch for two-channel world samples only, and only on runtimes withAL_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.mdfor player-facing defaults and troubleshooting, and update the README templates rather than hand-editing generated README outputs. After template changes, runpython scripts/generate_docs.py. - Validate audio-facing migration changes with a normal client build. Run
fnq3_audio_zonesfor sidecar parser/runtime changes andfnq3_audio_recoveryfor device-loss policy changes; when OpenAL Soft loopback is available, also runfnq3_audio_loopbackso HRTF reporting, distance gain, direct stereo routing, idle silence, and EFX filters stay covered.
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.