Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/commit-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ jobs:
tools/tests/linux_gui_presentation_defaults.py \
tools/tests/linux_highdpi_mouse.py \
tools/tests/linux_appimage_packaging.py \
tools/tests/lang_table_encoding.py \
tools/tests/linux_metadata_fuzz.py \
tools/tests/linux_packaging_guidance.py \
tools/tests/linux_sanitizer_ci.py \
Expand Down Expand Up @@ -141,6 +142,7 @@ jobs:
tools/tests/renderer_vulkan_shadow_compatibility.py \
tools/tests/renderer_vulkan_world_interaction_compatibility.py \
tools/tests/savegame_corruption_contract.py \
tools/tests/savegame_pointer_width_safety.py \
tools/tests/sdl3_input_parity.py \
tools/tests/sdl3_multidisplay_windowing.py \
tools/tests/settings_menu_coverage.py \
Expand Down Expand Up @@ -172,6 +174,7 @@ jobs:
python tools/tests/linux_gui_presentation_defaults.py
python tools/tests/linux_highdpi_mouse.py
python tools/tests/linux_appimage_packaging.py
python tools/tests/lang_table_encoding.py
python tools/tests/linux_metadata_fuzz.py
python tools/tests/linux_packaging_guidance.py
python tools/tests/linux_pk4_legacy_tools.py
Expand Down Expand Up @@ -227,6 +230,7 @@ jobs:
python tools/tests/renderer_vulkan_shadow_compatibility.py
python tools/tests/renderer_vulkan_world_interaction_compatibility.py
python tools/tests/savegame_corruption_contract.py
python tools/tests/savegame_pointer_width_safety.py
python tools/tests/sdl3_input_parity.py
python tools/tests/sdl3_multidisplay_windowing.py
python tools/tests/settings_menu_coverage.py
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/push-verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ jobs:
tools/tests/linux_gui_presentation_defaults.py \
tools/tests/linux_highdpi_mouse.py \
tools/tests/linux_appimage_packaging.py \
tools/tests/lang_table_encoding.py \
tools/tests/linux_metadata_fuzz.py \
tools/tests/linux_packaging_guidance.py \
tools/tests/linux_sanitizer_ci.py \
Expand Down Expand Up @@ -141,6 +142,7 @@ jobs:
tools/tests/renderer_vulkan_shadow_compatibility.py \
tools/tests/renderer_vulkan_world_interaction_compatibility.py \
tools/tests/savegame_corruption_contract.py \
tools/tests/savegame_pointer_width_safety.py \
tools/tests/sdl3_input_parity.py \
tools/tests/sdl3_multidisplay_windowing.py \
tools/tests/settings_menu_coverage.py \
Expand Down Expand Up @@ -172,6 +174,7 @@ jobs:
python tools/tests/linux_gui_presentation_defaults.py
python tools/tests/linux_highdpi_mouse.py
python tools/tests/linux_appimage_packaging.py
python tools/tests/lang_table_encoding.py
python tools/tests/linux_metadata_fuzz.py
python tools/tests/linux_packaging_guidance.py
python tools/tests/linux_pk4_legacy_tools.py
Expand Down Expand Up @@ -227,6 +230,7 @@ jobs:
python tools/tests/renderer_vulkan_shadow_compatibility.py
python tools/tests/renderer_vulkan_world_interaction_compatibility.py
python tools/tests/savegame_corruption_contract.py
python tools/tests/savegame_pointer_width_safety.py
python tools/tests/sdl3_input_parity.py
python tools/tests/sdl3_multidisplay_windowing.py
python tools/tests/settings_menu_coverage.py
Expand Down
7 changes: 7 additions & 0 deletions docs/dev/release-completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,13 @@ is `docs/dev/macos-moltenvk-decision.md`.
- [x] SDL3 desktop focus transitions now release fullscreen relative-mouse capture, clear keyboard/controller/touch state exactly once, reject queued user input while the game window is inactive, and apply `s_muteUnfocused` consistently on Linux and experimental macOS as well as Windows.
- [x] SDL3 Linux and experimental macOS system-console input now decodes committed and pasted UTF-8 before narrowing to the stock single-byte font range, and asynchronous OpenAL device hotplug notifications use an atomic handoff instead of racing the audio update thread.
- [x] Vulkan now renders both authored MD5 meshes and converted or prebuilt MD5R assets consistently in single-player and multiplayer. MD5R models materialize the classic geometry contract used by every Vulkan consumer—including ambient and interaction lighting, stencil and mapped-shadow submission, fog/blend lights, sky, decals, overlays, and collision—while retaining animated tangents and legacy MD5 skin-scale behavior. Native ASCII `.md5r` / `.MD5RProc` and compiled `.md5rc` / `.MD5RProcc` export-and-reload paths are covered, including retail packed vertex formats and rebuild-on-load payloads; no replacement game content is required.
- [x] Accented characters now display correctly in Spanish, French, and Italian menus and subtitles. The shipped string tables are UTF-8 while the stock Quake 4 fonts are a fixed 256-glyph set, so text such as `MENÚS` previously rendered as `MENA S`; openQ4 now converts a UTF-8 string table to the font's character set when it loads one, and substitutes readable ASCII for typographic characters the stock fonts do not draw. English apostrophes in subtitles are also fixed, community translations authored in UTF-8 now work, and retail localized packs are untouched.
- [x] Startup failures no longer exit silently. A second error raised while the engine was already shutting down used to end the process with a success code and no message, which left users with a log that simply stopped mid-shutdown and no way to find out what went wrong. Both messages are now reported and the process exits with a failure code. Content checks that abort startup additionally list the resolved `fs_basepath`/`fs_savepath`/`fs_cdpath` and every PK4 actually found in `q4base` and `baseoq4`, which identifies the most common Linux cause: a case-mismatched folder or PK4 name copied from a Windows install.
- [x] Custom DDS texture packs, including BC7, are now selected reliably. Texture-compression capabilities were only ever published to one of the binaries that need them, so depending on the active renderer every DDS replacement could be rejected without explanation; user replacements are now also probed ahead of the stock precompressed data instead of behind it, and the highest performance preset no longer switches replacements off and discards a player's setting. `image_showPrecompressedTextures 1` now lists every path tried and why each was rejected, so a pack author can see exactly what a file must be named. BC7 normal maps use the ordinary XYZ-in-RGB layout — packs previously encoded with `bc7enc -r2a` for openQ4 need re-encoding without it.
- [x] Missing sound samples are no longer silent failures: the engine reports the sample it could not find along with every path it tried. Voice-over now falls back to English when a language ships localized text but no localized voice track, matching the original game, and the `s_showVoices` diagnostic prints its voice list again instead of producing no output at all.
- [x] Navigation data problems are now reported instead of being developer-only, and a scripted character that cannot reach its destination can no longer stall a map script indefinitely — the move is abandoned after `ai_scriptedMoveTimeout` seconds without progress (30 by default, 0 to disable) with a log line naming the character, its target, and why it was stuck. Flying-enemy movement and tether goals are also corrected: area ceiling heights were never computed, so every navigation area reported a ceiling of zero.
- [x] Loading a savegame without first quitting to the menu now starts from the same clean state as loading after a restart; looping sounds and render objects from the previous session are released before the new one is built.
- [x] Release builds are more robust on 64-bit ARM: the binary declaration reader now fails cleanly on an unexpected value instead of silently losing sync with the rest of the stream when assertions are compiled out, an unexpected BC7 texture degrades to uncompressed instead of ending the session mid-load, and floating-point contraction is pinned off so ARM64 and x86-64 evaluate physics and navigation math identically.

## macOS Evidence Gate

Expand Down
85 changes: 63 additions & 22 deletions docs/user/texture-replacements.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,14 @@

openQ4 can use DDS texture-pack replacements without changing the original Quake 4 materials. This includes the stock game's `progimg/` image-program results, dhewm3-style `dds/` packs, and same-folder replacements used by some Quake 4 packs.

## Enable replacements

Set `image_usePrecompressedTextures` before loading a map:

- `0` disables automatic DDS replacements.
- `1` uses any supported DDS replacement. DXT1, DXT5, RXGB, and BC7 data can upload directly when the renderer supports the format. This mode also reuses the stock `progimg/` files shipped in the Quake 4 PK4s.
- `2` uses only BC7/BPTC replacements and falls back to the original image when a BC7 replacement is unavailable or invalid.

BC7 requires a renderer with BPTC support. Run `gfxInfo` and look for the BC7/BPTC capability line if a pack is not being selected.
DXT/RXGB files fall back to CPU decoding when direct texture-compression upload is unavailable. BC7 files require BPTC and otherwise fall back to the original source image.

## Where files belong

The preferred layout is the dhewm3-compatible `dds/` shadow tree inside the active game directory or a PK4:
Put your `.dds` files under a `dds/` folder inside `baseoq4` (loose, or at the root of a PK4 you drop in `baseoq4`). Mirror the original asset path underneath it and change the extension:

```text
Original asset: textures/stone/wall01.tga
Replacement: baseoq4/dds/textures/stone/wall01.dds
```

```text
baseoq4/
Expand All @@ -25,18 +19,53 @@ baseoq4/
wall01.dds
```

For an image named `textures/stone/wall01`, openQ4 first checks `dds/textures/stone/wall01.dds`. A loose file therefore belongs at `baseoq4/dds/textures/stone/wall01.dds`; a PK4 should contain the same `dds/textures/...` path at its root.
A `.dds` placed next to the original asset path instead — `baseoq4/textures/stone/wall01.dds`, the layout some Quake 4 packs use — also works. The `dds/` tree is preferred and wins if both exist.

`q4base/dds/...` is on the search path too, but keep your files in `baseoq4` so a Steam file verification does not delete them.

### Bump maps and other image programs

openQ4 uses dhewm3's complete image-program conversion too. For example:
Quake 4 materials rarely name a bump texture directly. They usually write an image *program*:

```text
heightmap(models/monsters/foo_local, 4)
-> dds/heightmap/models/monsters/foo_local 4.dds
bumpmap addnormals( textures/stone/wall01_local, heightmap( textures/stone/wall01_h, 4 ) )
```

Texture-pack tools should preserve that mapping for composed programs such as `heightmap`, `addnormals`, `add`, and `scale`. For simple image references, openQ4 also checks the extension-normalized shadow path and then a `.dds` beside the original `.tga` or `.jpg`. The canonical `dds/` path wins when more than one replacement exists.
You can replace either end of that:

- **Replace an input.** Shipping `dds/textures/stone/wall01_local.dds` replaces the source texture and openQ4 recomputes the program result on load. This is the simplest option and is what a Quake-4-shaped pack normally does.
- **Replace the finished result.** Name the file after the whole program using dhewm3's mangling:

```text
heightmap(models/monsters/foo_local, 4)
-> dds/heightmap/models/monsters/foo_local 4.dds
```

Texture-pack tools should preserve that mapping for composed programs such as `heightmap`, `addnormals`, `add`, and `scale`.

If you are unsure what a given material image is called, turn on `image_showPrecompressedTextures` (below) — openQ4 prints every path it looked for.

The original Quake 4 archives use a different `progimg/` naming scheme. openQ4 recognizes those paths automatically, including nested image programs and plain `gfx/env/` cube sides; users do not need to extract, rename, or regenerate the retail files. Custom texture packs should continue to prefer the portable `dds/` layout above.
### Search order

For each image, openQ4 probes in this order and takes the first usable file:

1. `dds/<image program name>.dds` — the dhewm3 naming scheme
2. `dds/<asset path>.dds` — for plain image references
3. `<asset path>.dds` — beside the original `.tga`/`.jpg`, the Quake 4 style
4. `progimg/...` — the stock precomputed results shipped in the retail PK4s

The retail `progimg/` tree is probed **last**, so a user replacement always wins. openQ4 recognizes those retail paths automatically, including nested image programs and plain `gfx/env/` cube sides; you do not need to extract, rename, or regenerate the retail files.

## Enable replacements

Set `image_usePrecompressedTextures` before loading a map:

- `0` disables automatic DDS replacements entirely, including your own packs.
- `1` (default) uses any supported DDS replacement — DXT1, DXT5, RXGB and BC7 — and also reuses the stock `progimg/` files shipped in the Quake 4 PK4s. **This is the right setting for a BC7 pack.**
- `2` uses only BC7/BPTC replacements and ignores DXT/RXGB data, including the stock `progimg/` tree. Use this only if you specifically want everything else to come from the uncompressed sources.

BC7 requires a renderer with BPTC support. Run `gfxInfo` and look for the BC7/BPTC capability line if a pack is not being selected.
DXT/RXGB files fall back to CPU decoding when direct texture-compression upload is unavailable. A BC7 file on a renderer without BPTC support is uploaded uncompressed rather than failing the load.

## BC7 file requirements

Expand All @@ -47,18 +76,30 @@ Supported BC7 files are single 2D textures using either:

Texture arrays, cube maps, volume textures, invalid dimensions, and truncated declared mip chains are rejected safely. Authored mip levels are uploaded directly. Texture downsizing skips leading authored levels, and images using an unmipped sampler upload only their base level.

Doom 3-style bump maps store the red normal channel in alpha (RXGB/XGBR layout). openQ4 recognizes the retail `RXGB` FourCC and preserves that normal-map swizzle for direct uploads and decoded fallbacks. BC7 normal maps intended for this path must use the same layout; Daniel Gibson's customized `bc7enc` supports it with `-r2a`.
### Normal maps

Write BC7 normal maps the ordinary way: X, Y, Z in RGB, alpha unused. That is what `texconv`, NVTT, Compressonator and `bc7enc` all produce by default, and it is what openQ4 expects.

> **Changed:** earlier openQ4 builds incorrectly forced BC7 bump maps into the DXT5nm/RXGB channel layout, which required encoding with `bc7enc -r2a`. A pack authored that way will now render with flat lighting and must be re-encoded without `-r2a`. Only DXT5 and true `RXGB` files use the alpha-swizzled layout; that convention exists because DXT5 gives alpha its own higher-precision interpolator, and BC7 has no such limitation.

## Troubleshooting

Use these commands when checking a pack:

```text
image_usePrecompressedTextures 2
image_usePrecompressedTextures 1
image_showPrecompressedTextures 1
reloadImages all
```

The log then prints the exact DDS path selected for each replacement. `listImages` identifies loaded BC7 images, while `imageDDSSelfTest` validates the built-in retail/dhewm3 naming conversions plus RXGB and BC7 header checks.
The log then prints the DDS path selected for each replacement, and — for every image where nothing was selected — the full list of candidate paths it tried with the reason each one was rejected. That list is the fastest way to find out what a file should be named:

```text
no DDS replacement for 'textures/stone/wall01':
dds/textures/stone/wall01.dds [dds/ (dhewm3 naming)] no such file
textures/stone/wall01.dds [alongside the original asset (Quake 4 style)] no such file
```

`listImages` identifies loaded BC7 images, while `imageDDSSelfTest` validates the built-in retail/dhewm3 naming conversions plus RXGB and BC7 header checks.

During development, an automatic DDS replacement older than its original source is ignored so stale generated data cannot hide a newer texture edit. The stock `progimg/` archives receive a four-second timestamp-granularity allowance; a genuinely newer loose source still wins. Rebuild or retimestamp a custom DDS after changing its source image.
During development, an automatic DDS replacement older than its original source is ignored so stale generated data cannot hide a newer texture edit. The stock `progimg/` archives receive a four-second timestamp-granularity allowance; a genuinely newer loose source still wins. Rebuild or retimestamp a custom DDS after changing its source image. Note that files inside a PK4 report no timestamp at all, so a packed replacement is never treated as stale — if you are authoring loose files alongside extracted `.tga` sources, the extracted sources will look newer and your `.dds` will be skipped.
12 changes: 12 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,18 @@ else
shared_c_args += aliasing_arg
endif
endforeach
# GCC/Clang default to -ffp-contract=fast, which fuses a*b+c into an FMA
# wherever the target has one. AArch64 always does; the x86-64 SSE2 baseline
# openQ4 is validated against does not. That makes identical source produce
# different float results per architecture in collision, AAS and physics
# code, which is exactly the class of divergence behind arm64-only map and
# savegame failures. MSVC already defaults to precise contraction.
foreach fp_arg : ['-ffp-contract=off']
if cpp.has_argument(fp_arg) and cc.has_argument(fp_arg)
shared_cpp_args += fp_arg
shared_c_args += fp_arg
endif
endforeach
endif

if host_system == 'darwin'
Expand Down
53 changes: 53 additions & 0 deletions src/aas/AASFile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1139,13 +1139,66 @@ bool idAASFileLocal::ParseClusters( idLexer &src ) {
idAASFileLocal::FinishAreas
================
*/
// upward probe length used to find the ceiling above a floor area
static const float AAS_FLOOR_CEILING_TRACE_HEIGHT = 500.0f;
// far-above probe that PushPointIntoAreaNum() clamps down to a volume's top
static const float AAS_NONFLOOR_CEILING_PROBE_Z = 131072.0f;
static const int AAS_CEILING_TRACE_MAX_AREAS = 10;

void idAASFileLocal::FinishAreas( void ) {
int i;

// A file with fewer than two areas carries no usable navigation volume;
// deriving from it would trace against a degenerate tree. Zero the derived
// fields and stop, matching the original engine.
if ( areas.Num() < 2 ) {
for ( i = 0; i < areas.Num(); i++ ) {
areas[i].center.Zero();
areas[i].bounds.Clear();
areas[i].ceiling = 0.0f;
}
return;
}

for ( i = 0; i < areas.Num(); i++ ) {
areas[i].center = AreaReachableGoal( i );
areas[i].bounds = AreaBounds( i );
}

// aasArea_t::ceiling is not an editor-only leftover: the game surfaces it
// through idAASLocal::AreaCeiling(), which clamps flying-AI movement and
// bounds tether goal searches. It is not stored in the file, so failing to
// derive it here left every area reporting a ceiling of 0 and broke both.
for ( i = 0; i < areas.Num(); i++ ) {
if ( areas[i].flags & AREA_FLOOR ) {
aasTrace_t trace;
int traceAreas[AAS_CEILING_TRACE_MAX_AREAS];
idVec3 tracePoints[AAS_CEILING_TRACE_MAX_AREAS];
idVec3 end;

trace.areas = traceAreas;
trace.points = tracePoints;
trace.maxAreas = AAS_CEILING_TRACE_MAX_AREAS;

end = areas[i].center;
end.z += AAS_FLOOR_CEILING_TRACE_HEIGHT;

Trace( trace, areas[i].center, end );
areas[i].ceiling = trace.endpos.z;

// stop at the first point where the probe left this area
for ( int j = 0; j < trace.numAreas; j++ ) {
if ( trace.areas[j] != i ) {
areas[i].ceiling = trace.points[j].z;
break;
}
}
} else {
idVec3 point( 0.0f, 0.0f, AAS_NONFLOOR_CEILING_PROBE_Z );
PushPointIntoAreaNum( i, point );
areas[i].ceiling = point.z;
}
}
}

/*
Expand Down
Loading
Loading