v0.3.0: display abstraction, map-browser menu, SAGE terrain tiling/blends

- ra3.client::display: shared interactive loops; SDL and Vulkan backends
  implement only the primitives (init/present/poll_event/window_size/
  key_down/present_terrain). ra3.display picks the backend.
- Menu: maps by localized name (gamestrings.csf), red/gold theme, hover
  highlight, mouse + keyboard, wheel scroll, fullscreen and FPS/vsync
  options, loading progress bar.
- Terrain: continuous tile sampling via a texture array (REPEAT, uv =
  cell/(2*cellSize)) removes per-cell grid seams; SAGE blend ramp for
  material transitions; FPS label + top-right minimap overlays.
- Skip the skirmish sim for map views; reuse the Vulkan texture; no idle
  terrain redraw.
This commit is contained in:
EnderTheCoder
2026-09-20 02:20:39 +08:00
parent 702b4e29aa
commit b6d619254d
66 changed files with 37661 additions and 406 deletions
+56 -20
View File
@@ -15,19 +15,36 @@ with llvm-mingw for Windows.
└───────────────────┬───────────────────┘
┌──────────────┬───────────────┼───────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
ra3.ui ra3.render ra3.skirmish ra3.client ra3.game
SDL3 window framebuffer match rules display/loop RA3 sides
ra3.display ra3.render ra3.skirmish ra3.client ra3.game
backend pick framebuffer match rules display+loops RA3 sides
│ │ │ │ │
└──────┬───────┘ └───────┬───────┘ │
▼ ▼ │
ra3.core ra3.logic │
simulation │
┌───────────────────────────────────────┬──────────────────────┘
▼ ▼
ra3.map ra3.fs
map catalog BIG4 + RefPack
┌──┴───┐ │ └───────┬───────┘ │
▼ ▼ ▼ ▼ │
ra3.ui ra3.vulkan ra3.terrain ra3.logic │
SDL Vulkan heightmap/blends simulation │
┌──────────────────────┘
▼
ra3.map / ra3.fs
catalog / BIG4 + RefPack
```
## Presentation layer
`ra3.client::display` is the single seam between the engine and the windowing
stack. A backend implements only the low-level primitives (`init`, `present`,
`poll_event`, `window_size`, `key_down`, `shutdown`) and, optionally,
`present_terrain`; the interactive loops (menu, image viewer, camera viewer,
GPU terrain) and the native-to-`ui_event` mapping live once in the base class,
so the SDL and Vulkan paths cannot drift. SDL3 remains the platform layer (it
owns the window, input and the low-cost blit backend); Vulkan is the accelerated
backend. `ra3.display` selects the first backend that starts, and the app never
references a backend by name.
The Vulkan backend currently hand-rolls its pipelines. As materials, skybox and
HUD arrive, a thin RHI + render-graph belongs between `ra3.client` and the
Vulkan/SDL backends; `present_terrain` is the placeholder for that step.
## Module responsibilities
### `ra3.core`
@@ -44,9 +61,20 @@ spatial grid, and the 30 Hz `game_logic` driver (`prepare_new_game` /
`start_new_game` / `update`).
### `ra3.client`
The presentation boundary: an abstract `display` with a `headless_display`
implementation, and `game_client`, the seam a future W3D/D3D9 renderer plugs
into.
The presentation boundary: the abstract `display` (low-level primitives plus the
shared interactive loops and `ui_event` mapping), a `headless_display` for
CPU-only runs, and `game_client`, the facade that owns the simulation and drives
the frame loop.
### `ra3.display`
Backend selection: tries Vulkan, then SDL, then reports failure so the caller can
fall back to an offscreen image. The only module that names a concrete backend.
### `ra3.terrain`
The real map terrain: parses `CkMp`'s `HeightMapData` and `BlendTileData`
(tiles, `Blends`/`ThreeWayBlends` and their `BlendDescription`s), loads the
`Terrain.big` tile textures, and rasterises the map (software `render`/`render3d`
and the GPU `gpu_terrain` for `present_terrain`).
### `ra3.game`
Red Alert 3 data that SAGE keeps in `PlayerTemplate`: the three sides
@@ -62,7 +90,8 @@ on demand.
### `ra3.map`
Map discovery and loading: scans `MapsMultiplayer.big` for main map entries,
unwraps the two compression layers (`BIG4` RefPack → `EAR\0` wrapper → RefPack →
`CkMp`), and recovers `Player_N_Start` waypoint coordinates. Degenerate
`CkMp`), recovers `Player_N_Start` waypoint coordinates, and decodes the
localized display names from the install's `gamestrings.csf`. Degenerate
extractions are rejected so the caller can fall back.
### `ra3.skirmish`
@@ -72,14 +101,21 @@ passive + harvester income, a simple build AI, movement and combat on a fixed
### `ra3.render`
A dependency-free software renderer: an ARGB8888 `image` framebuffer with
blit/line/circle primitives, a TGA decoder for the game's map art, a 24-bit BMP
encoder for headless output, and `compose` which overlays a world grid and
markers (start positions, live units) onto the map art.
blit/line/circle/text primitives (an embedded 8x8 bitmap font), a TGA decoder for
the game's map art, a 24-bit BMP encoder for headless output, and `compose` which
overlays a world grid and markers (start positions, live units) onto the map art.
### `ra3.ui`
The windowed viewer. The SDL3 backend streams the rendered image to a texture
with pan/zoom and Esc-to-quit; when the build has no SDL3, a null backend
returns `false` so the caller writes an offscreen image instead.
The SDL3 backend: `sdl_display` implements the `ra3.client::display` primitives
using an `SDL_Renderer` streaming texture. Cheap and dependency-light, it is the
fallback when Vulkan is unavailable. A null backend returns `false` so the build
still runs without SDL3.
### `ra3.vulkan`
The accelerated backend: `vulkan_display` implements the same primitives and
adds `present_terrain`, a GPU heightfield raymarcher (mipmapped tile atlas with a
replicated gutter and the retail SAGE blend ramp). A null backend reports failure
when no Vulkan loader is present.
## Design rules
+99 -3
View File
@@ -78,6 +78,27 @@ Each holds an object pointer (0 when the subsystem is down).
cash `+0x64`, player slots `+0xfc` (stride `0x5c`, 6 slots), faction at
`slot + 0x18` (`Empire=2`, `Allied=4`, `Soviet=8`, `Random=7`).
### Tactical view (camera)
The in-game camera is the `TheTacticalView` object, held in the global at
`0x00cdb7b4`. Its vtable accessors return zoom, pitch (current/target), yaw,
world position and FOV; the debug overlay `FUN_005ef0a0` prints them through the
format string at `0x00c0b900`. Mouse state is the singleton at `0x00ce9284`
(cursor position vtable slot `+0x3c`, button down `+0x48`); the keyboard manager
is `0x00ce927c` (modifier mask `+0x38`).
Per-map camera tuning is a named-field table in `.rdata` (around
`0x00c11a54`): `cameraMinHeight`, `cameraMaxHeight`, `cameraPitchAngle`,
`cameraYawAngle`, `cameraScrollSpeedScalar`, `cameraGroundMinHeight`,
`cameraGroundMaxHeight`. The map-load chunk `CHUNK_TacticalView`
(`0x00beea14`, consumed near `0x00548a00`) seeds the view from the map; the
tutorial actions `LOCK_CAMERA_SCROLL` / `LOCK_CAMERA_ZOOM` / `LOCK_CAMERA_ROTATION`
gate the controls.
OpenRA3 has no 3D terrain yet, so `ra3::render::view_camera` reproduces the
*controls* - wheel zoom, screen-edge scroll, clamped pan and opening on the
player's start - over the 2D map overview, not the retail perspective camera.
### Match start
The BEGIN button calls `SkirmishGameOptionsMenu::start` (`0x00b28d60`), which
@@ -127,9 +148,84 @@ in `MPPositionList` instead yield no waypoints and fall back.
Verified example (`map_mp_2_feasel4`): `Player_1_Start` = `(1338.9, 1940.5, 0)`,
`Player_2_Start` = `(1290.8, 1404.9, 0)`.
### Terrain (`ra3.terrain`)
The `CkMp` tree is a flat chunk list: `"CkMp"`, `u32 assetCount`, the name
table (`{ u8 len, name, u32 index }`, index descending from `assetCount`), then
`{ u32 index, u16 version, u32 size, data }` per chunk. On
`map_mp_2_feasel4` the terrain is `HeightMapData` v6 (540 x 600, border 20,
`u16` elevations, scale `0.0390625`) and `BlendTileData` v27.
`BlendTileData` opens with `NumTiles`, the `u16` tile grid, then the
blend/three-way/cliff tables (`u16` for v27); the passability flag arrays that
follow are not needed for rendering, so the texture table is located by
scanning for its `{ cellStart, cellCount, cellSize, magic }` + `u16`-prefixed
name entries. A tile value is `(cellIndex << 2) | variant`, and `cellIndex`
indexes the global `TextureCellCount`-cell table (each texture owning
`cellSize^2` 64 px cells). The textures themselves are
`art\terrain\<stem>.tga` in `Terrain.big` / `Core11.big` (RefPack + 256x256
TGA). Rendered top-down, `map_mp_2_feasel4` correlates 0.94 with the official
`_art.tga` overview.
### Terrain blending (`BlendTileData` tail)
After the tile grid, `BlendTileData` stores three per-cell `u16` tables —
`Blends`, `ThreeWayBlends` and `CliffTextures` — then `TextureCellCount`,
`BlendsCount`, the texture table, two magic words and `BlendsCount - 1` blend
descriptions (18 bytes each: `u32 secondaryTile`, four direction bytes,
`u8 flags`, `u8 twoSided`, `u32 0xFFFFFFFF`, `u32 0x7ADA0000`). A non-zero
`Blends[cell]` is a 1-based index into the descriptions; `secondaryTile` is a
packed tile value (`secondaryTile >> 2` is its cell).
The retail `Terrain.fx` (compiled `terrain.fxo`, parameters `Terrain.BaseTexture`,
`Terrain.MacroTexture`, `MapCellSize`, `IsTerrainAtlasEnabled`; technique
`TerrainTile`) cross-fades a cell's base tile into `secondaryTile` with a linear
ramp selected by `BlendDirection`: `1` right, `2` top, `4` top-right, `8`
top-left, where `flags` bit 0 flips the axis and bit 1 marks a two-sided
diagonal. OpenSAGE's `Terrain.frag` reconstructs the exact
`CalculateBlendFactor`; `ra3::terrain::blend_factor` and `shaders/terrain.frag`
mirror it. The row axis is *not* inverted (73% of long-axis blends point at a
neighbour of the same texture, versus 25% inverted). On `map_mp_2_feasel4`,
33828 of 324000 cells carry a blend and there are 7094 descriptions.
The GPU atlas pads every 64 px tile with a 2-texel replicated gutter. Packed
edge-to-edge, bilinear/mipmap filtering averaged two unrelated tiles at every
cell border — that cross-tile bleed was the visible grid line the hardware path
drew. (`shaders/terrain.frag` samples `cell_stride = cell_texels + 2 * gutter`.)
### Open question: per-cell tile sampling
Measured on `map_mp_2_feasel4`, sampling each cell as its own 64 px block and
restarting the UV every cell leaves a 1.39x edge spike at cell boundaries
(43.4 vs 31.1 mean gradient at 8 px/cell). Two candidate mappings reduce it and
need a visual decision against the retail art:
| Mapping | Boundary/interior |
| --- | --- |
| per-cell 64 px block (current) | 1.39 |
| OpenSAGE `BlendTileTextureIndex` Morton layout, 32 px block | 1.18 |
| continuous `uv / (cellSize * 2)`, per OpenSAGE `Terrain.frag` | 1.11 |
The retail `Terrain.frag` (OpenSAGE) samples the tile texture *continuously*
(`uv / (CellSize * 2)`), so adjacent cells never restart the texture; our
per-cell restart is the remaining source of grid lines. Switching the atlas
from 64 px blocks to continuous 32 px regions (or the Morton 8x8 layout) is the
next step, pending an art correlation check.
### Map display names
The skirmish map list labels live in `Data\English.big`'s
`data\gamestrings.csf` (the newest `Lang-English*.big` wins) under
`MAP:<UPPERCASE_ID>`, e.g. `MAP:MAP_MP_2_FEASEL4` = "Battlebase Beta". CSF
values are UTF-16 code units whose low byte is XORed with `0xFF`
(`ra3::map::parse_map_names`). `openra3 extract` writes the decoded table to
`maps/map_names.tsv`; `openra3 menu` shows them instead of the raw map id.
### Still to recover
- `MPPositionList` layout (per-player starts for maps without waypoints).
- Map dimensions / `HeightMapData` / `BlendTileData`.
- Compiled asset blobs (`map.bin`, `global.bin`, `static.*.bin`) and the
`.manifest` schema used to deserialise them.
- Cliff textures and the `CliffTextureMapping` UV remap (`CliffTextures` is
parsed but not yet drawn).
- Compiled asset blobs (`global.bin`, `static.*.bin`) and the `.manifest`
schema used to deserialise them.
+27 -16
View File
@@ -18,25 +18,36 @@ that each one produces something that builds and runs.
`import std;` in every module; added an isolated Windows cross-build
(llvm-mingw + SDL3 MinGW) producing `openra3.exe` + `SDL3.dll`, alongside
the Linux build. Both images are separate to keep the toolchains apart.
- [x] **v0.3.2 — real terrain.** `ra3.terrain` parses the map's `HeightMapData`
and `BlendTileData` and rasterises the actual terrain from the install's
`Terrain.big` tile textures (top-down, elevation-shaded), replacing the
`<map>_art.tga` overview as what `render` draws.
- [x] **v0.3.0 — real balance + skirmish + Vulkan.** `ra3.data` pins the retail
numbers (damage types, `ArmorTemplate` percentages, weapons, units,
structures, ore economy) audited from EA's open RA3 XML; `ra3.skirmish` is
rebuilt on them (armour resolution, weapon target masks, pay-as-you-go
build queue, power, tech prerequisites, refinery ore cycle, team-wipe
victory); and `ra3.vulkan` replaces the presentation stack with Vulkan
(SDL3 surface, embedded SPIR-V), with a null fallback where no loader
exists.
## Next
- [ ] **v0.1.1 — correct world calibration.** Parse the map heightmap
dimensions (and `MPPositionList` starts) so the unit overlay lines up with
the map art instead of using an assumed world size.
- [ ] **v0.2.0 — data & file formats.** Parse compiled gameplay assets
(`GameObject`, `WeaponTemplate`, `ArmorTemplate`, `LocomotorTemplate`) so
units use the real balance numbers instead of OpenRA3's stand-ins.
- [ ] **v0.3.0 — deterministic simulation.** Real update modules, locomotor
movement, weapons/damage/armour resolution, build queues and the tech
tree, pathfinding and shroud.
- [ ] **v0.4.0 — AI.** Skirmish AI: build states, team composition, attack
waves (the reference tree's `AI*` modules).
- [ ] **v0.5.0 — full renderer.** Terrain heightmap rendering, W3D models and a
D3D9/Vulkan/OpenGL client backend implementing `ra3::client::display`,
plus full input.
- [ ] **v0.6.0 — content.** Load real maps, units, powers and strings; play a
skirmish end-to-end with a UI.
- [ ] **v0.3.1 — tactical map view.** The Vulkan/SDL viewer reproduces the
retail tactical view (`TheTacticalView`, `ra3_1.12.game` `0x00cdb7b4`):
opens centred on the player's start, wheel zoom, edge scroll and clamped
pan. A hand-rolled HUD was tried and removed — the real in-game interface
is `.apt` art that must be recovered first, so it is not on the near
roadmap.
- [ ] **v0.4.0 — real update modules.** Locomotor movement, projectiles and
warheads, build placement, shroud, and pathfinding, driven by the
`GameObject` module set recovered from the binary.
- [ ] **v0.5.0 — data-driven content.** Deserialise the compiled assets
(`global.bin`/`static.*.bin` + `.manifest`) so `ra3.data` reads the
install instead of pinned constants; real maps, models and strings.
- [ ] **v0.6.0 — full renderer.** Perspective terrain (the heightmap is decoded
but drawn top-down today), W3D models and the full in-game client on
Vulkan, plus input.
## Cross-cutting tracks