diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 39763cd..0bdd560 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -1,9 +1,8 @@ # Shipping a release -Players update themselves — but not entirely. What `make deploy` does, what -reaches the Windows folder with no human involved, and what does not. The -contents of that folder are [`WINDOWS.md`](WINDOWS.md). Your own master rather -than ours is [`MASTER.md`](MASTER.md). +What `make deploy` does, what reaches the Windows folder with no human involved, +and what does not. That folder is [`WINDOWS.md`](WINDOWS.md); your own master +rather than ours is [`MASTER.md`](MASTER.md). ```bash make setup-windows # rustup target + cargo-xwin, once @@ -16,52 +15,36 @@ make logs prod|dev SINCE=2h ``` Each recipe is one `cargo xtask` command — `windows`, `release`, `publish`, -`provision`, `logs`, `certs` — and `make` only forwards `PROFILE` and -`RELEASE`. `cargo xtask` on its own lists them. - -`prod`/`dev` is the publication channel, not the compilation profile. Address -and root are `IW4L_DEPLOY_HOST` / `IW4L_DEPLOY_ROOT` in `.env`. prod: udp/4433 -and `https://:8443/prod/`; dev: 4434 and `/dev/`. Git, protocol and SHA -are frozen into `deployment.json` at `release`, not at `publish`. +`provision`, `logs`, `certs` — and `make` forwards only `PROFILE` and `RELEASE`; +`cargo xtask` alone lists them. `prod`/`dev` is the publication channel, not the +compilation profile; address and root are `IW4L_DEPLOY_HOST` / +`IW4L_DEPLOY_ROOT` in `.env`. prod: udp/4433 and `https://:8443/prod/`; +dev: 4434 and `/dev/`. Git, protocol and SHA freeze into `deployment.json` at +`release`, not at `publish`. An ordinary publish compiles nothing, calls no zstd, rewrites nothing under -`/etc` and does not restart the master if its SHA matched. The uncompressed -`iw4l.exe` stays local. The archive is named `iw4l-.exe.zst`; the old -`iw4l.exe.zst` on the VPS is not deleted. The manifest is swapped in by rename. +`/etc` and leaves the master running if its SHA matched. The uncompressed +`iw4l.exe` stays local, the archive is `iw4l-.exe.zst`, the old one on the +VPS survives, and the manifest is swapped in by rename. -`iw4launcher.exe update` reads `IW4L_UPDATE_URL` from the sidecar `.env`, -fetches `manifest.json`, checks size/sha256 and downloads `download`. Without -that verb it starts the `iw4l.exe` already in the folder and makes no request at -all — the archive ships one, so updating is a thing the player asks for rather -than a thing that happens to them. The launcher never updates itself. The -player's `.env` is created once and never overwritten. - -Changing `master_protocol::PROTOCOL_VERSION` is also a change of `ALPN`: an old -client is refused at the handshake instead of silently decoding a drifted -format. The master and the clients of such a release ship together; `publish` -restarts the master, because its SHA changed. +Changing `master_protocol::PROTOCOL_VERSION` also changes `ALPN`: an old client +is refused at the handshake instead of silently decoding a drifted format. The +master and the clients of such a release ship together, and `publish` restarts +the master because its SHA changed. `make launcher windows` builds the portable ZIP in `dist/windows/`; it is not a -deploy. A fix to the updater arrives only by copying `iw4launcher.exe` by hand. +deploy, and a fix to the updater arrives only by copying `iw4launcher.exe` by +hand. What the player's launcher does with `IW4L_UPDATE_URL`: [`WINDOWS.md`](WINDOWS.md). -## Cutting a public release - -Tag `v0.1.0-demo.N`, titled `IW4L Technical Demo N`, and mark it a -**pre-release** on GitHub — the runtime is experimental and nothing about it is -production-ready. The release notes carry four things and are not worth writing -without them: - -* the commit it was built from; -* the platforms the archive was actually **run** on. A target that compiled is - not a verified platform, and a Linux release does not wait for a Windows - archive that has not been started yet — nor the other way round; -* the demo scenario: which game, which version, which map, what to do; -* the known limitations. +**Cutting a public release.** Tag `v0.1.0-demo.N`, titled `IW4L Technical +Demo N`, and mark it a **pre-release**. The notes carry four things and are not worth writing without +them: the commit it was built from; the platforms the archive was actually +**run** on; the demo scenario (which game, version and map, and what to do); the +known limitations. A target that merely compiled is not a verified platform, and +a Linux release waits for no unstarted Windows archive, nor the reverse. Every archive carries `LICENSE`, `NOTICE` and the two font licences (`release.rs::LEGAL_FILES`) and no game data. Check the archive, not just `git ls-files`: `make publish-check` reads the tracked tree and cannot see what -was staged into a ZIP. - -The published tags are not moved afterwards, and the history is not recreated -again once anything has been pushed. +was staged into a ZIP. Published tags are never moved, and history is never +recreated once anything has been pushed. diff --git a/docs/ENTITIES.md b/docs/ENTITIES.md index 2b6db72..1ef7baf 100644 --- a/docs/ENTITIES.md +++ b/docs/ENTITIES.md @@ -3,24 +3,23 @@ ## There is one funnel ```text -human ─┐ -bot ├─→ TickInput ─→ sim::step ─→ Snapshot ─→ client / theater / demo -replay ─┘ -``` +human ─┐ +bot ├─→ TickInput ─→ sim::step ─→ Snapshot ─→ client / theater / demo +replay ┘ -```rust -pub fn step(world: &mut SimWorld, tick: Tick, input: &TickInput, msec: i32) -> Snapshot; +pub fn step(world: &mut SimWorld, tick: Tick, input: &TickInput, + msec: i32, reason: StepReason) -> Snapshot; ``` Headless, prediction, replay and the determinism test call exactly this one. -`msec` is the length of the step; time enters **only** as an argument. Details -in `crates/sim/src/lib.rs`. One funnel in, one snapshot out. +`msec` is the step length; time enters **only** as an argument. The shape of +the funnel is [`SIM-STEP.md`](SIM-STEP.md), the code `crates/sim/src/lib.rs`. ## What can be an entity at all — fixed by the data (`entity_iw4`) The wiring is ours, the taxonomy comes with the data. The crate neither -serializes nor orders fields "for the network": it pins the layout it reads and -leaves transport to `net`. +serializes nor orders fields "for the network": it pins the layout it reads, +and leaves transport to `net`. | | | |---|---| @@ -28,27 +27,24 @@ leaves transport to `net`. | `Centity` | client-side slot, stride `0x204` (pose plus snapshot anchors) | | `ClientState` | per-client slot, stride `0x7c` (command, `name[16]`) | | `CorpseInfo` | server-side corpse slot, stride `0x53c`, 8 of them | -| `ET_PLAYER` / `ET_PLAYER_CORPSE` / `ET_ITEM` / `ET_MISSILE` | | -| `Trajectory` | `BG_EvaluateTrajectory`: gravity 400, delta 800 | -| events | the `CG_EntityEvent` ring, an 11-bit wrapper | +| `ET_PLAYER` / `ET_PLAYER_CORPSE` / `ET_ITEM` / `ET_MISSILE` | | +| `Trajectory` | `bg_evaluate_trajectory`: gravity 400, delta 800 | +| events | the entity-event ring, an 11-bit wrapper | | `ENTITYNUM_NONE` | `0x7FF` | - -The player and the input are `playerstate_iw4` (`playerState_t`, `usercmd_s`): -the two ends of the funnel. +| `playerstate_iw4` | player and input: the two ends of the funnel | ## Where everything else lives -* `sim/` — `gentity.rs`, `spawn.rs`, `world.rs`, `snapshot.rs`, `damage.rs`, - `bullet*.rs`, `missile.rs`, `item.rs`, `corpse.rs`, `match_state.rs`, - `score.rs`, `rules.rs`, `world_objects.rs`, `identities.rs` (`ScriptModelId` - is the one identity of a script model; a raw `u32` does not get to be a second); -* `frame` — frame and session markers: `AppScreen`, `HasWorld`, `RuntimeRole`, - `LaunchIdentity`. No plugins, no systems; +* `sim/` — `gentity spawn world snapshot damage bullet* missile item corpse + match_state score rules world_objects identities` (`ScriptModelId` is the one + identity of a script model; a raw `u32` is not a second one); +* `frame` — `AppScreen`, `HasWorld`, `RuntimeRole`, `LaunchIdentity`; no + plugins, no systems; * `session` — standing the match up and tearing it down (`MatchInstalled` / - `MatchTornDown`); `net` — the wire, deltas, prediction; `bots` — host-only - controllers that observe through a sensor adapter, walk a ClipMap-baked - graph, and enter the same `TickInput` funnel. Geometric `sim::step` scenes - in `bots` tests are the mapless check that a `UserCmd` actually moves. + `MatchTornDown`); `net` — wire, deltas, prediction; `bots` — host-only + controllers that observe through a sensor adapter, walk a ClipMap-baked graph + and enter the same `TickInput`. Geometric `sim::step` scenes in `bots` tests + check maplessly that a `UserCmd` moves. -The snapshot publishes the **semantics** of DObj composition, not runtime trees -and not posed vertices — see [`ANIM.md`](ANIM.md). +Snapshots publish the **semantics** of DObj composition, never runtime trees or +posed vertices ([`ANIM.md`](ANIM.md)). diff --git a/docs/MAP-LOAD.md b/docs/MAP-LOAD.md index 8c2888a..a7fee3b 100644 --- a/docs/MAP-LOAD.md +++ b/docs/MAP-LOAD.md @@ -2,68 +2,48 @@ Three floors: **disk** (`asset_transport`), **format** (`fastfile_*` → the `asset_iw4` IR), **products** (`asset_*` + `assets`). `assets` knows the disk -through the transport and knows nothing about the format; `fastfile_*` knows -the format and opens no files. Installing the match is not here — it is in -`session`. - -## The transaction: who calls whom +through the transport and nothing of the format; `fastfile_*` knows the format +and opens no files. Installing the match is `session`, not here. ```text -console/ui `map` → session::lifecycle SessionSwapRequest → tear down the previous - occupancy, then MapLoadApproved{request_id} - → assets::match_load approve_map_load find_zone_file / find_runtime_common_mp - start_match_load spawn onto load_pool() (MatchLoadBusy) - poll_match_load → PreparedMatchReady - → session::match_apply apply_prepared_match → scene + SimWorld + MatchInstalled +console/ui `map` → session::lifecycle SessionSwapRequest, tear the previous + occupancy down, then MapLoadApproved{request_id} + → assets::match_load approve_map_load / start_match_load onto load_pool() + (MatchLoadBusy) / poll_match_load → PreparedMatchReady + → session::match_apply apply_prepared_match → scene + SimWorld + MatchInstalled ``` Cancellation is `MatchLoadAbort(request_id)`: the walk returns -`MatchLoadOutcome::Canceled` at its next checkpoint and there is no prepared -match to throw away. Failure is `MapLoadFailed` — from discovery *and* from -install; `LoadingScreen` holds the failed install until teardown and cannot -turn it into a success. Every accepted request ends once: `MatchInstalled` or -`MapLoadFailed`, never both. +`MatchLoadOutcome::Canceled` at its next checkpoint, with no prepared match to +throw away. Failure is `MapLoadFailed`, from discovery *and* from install, and +every accepted request ends once — `MatchInstalled` or `MapLoadFailed`. -## The walk itself: `assets::session_load::load_prepared_match` +## The walk: `assets::session_load::load_prepared_match` -The single entry point; the composition root sees neither `fastfile_*` nor -`AssetSink`. Inside are several parallel walks on `load_pool()`: opening the -archive, startup materials, `common_mp`, the IW5/T5 weapon bundle, language -zones. `common_mp` goes **before** the map: its techset tables are needed -before `build_world_draw`. The output is `MatchLoadOutcome::Ready` with a -`PreparedMatch` (world, one `MatchMaterials` population, clip, weapons, -catalogs, `PreparedMap` — the single owner of spawns and map facts, `report`). - -Ownership rules the walk keeps: the zone arenas die with the walk that reads -them (only their size travels), a consumed donor catalog is moved row by row -rather than cloned, and the merged material population is finalized once — -whether or not the map has a `GfxWorld` — and never lives inside the geometry. +The single entry point; the composition root sees no `fastfile_*` and no +`AssetSink`. Inside are parallel walks on `load_pool()`: the archive, startup +materials, `common_mp`, the IW5/T5 weapon bundle, language zones. `common_mp` +goes **before** the map — its techset tables are needed before +`build_world_draw`. Output is `MatchLoadOutcome::Ready` with a `PreparedMatch` +(world, one `MatchMaterials` population, clip, weapons, catalogs and +`PreparedMap`, the single owner of spawns and map facts). The per-game adapter is `assets::lane` (`ZoneGame` → iw4/iw5/t5), the only -exhaustive `match ZoneGame` in the project; the runtime stays game-blind. A gap -in the lane is a typed `LaneGap`, not silence. +exhaustive `match ZoneGame` in the project; the runtime stays game-blind and a +gap in a lane is a typed `LaneGap`, not silence. -## Installing it: `session::match_apply` +Installing it is `session::match_apply`, in two halves. Preflight builds a +`MatchInstallPlan` — mode, doors, objectives, scene conversion, drawable world — +and publishes nothing until it hands one over, so a refusal leaves no +half-installed resources. Commit publishes it, boots the sim, writes +`MatchInstalled`. -Two halves. Preflight builds a `MatchInstallPlan`: mode, doors, objectives, -scene conversion, drawable world. Nothing of the match is published until it -hands one over, so an ordinary refusal leaves no half-installed resources -behind and completes the swap with `MapLoadFailed`. The commit half then -publishes the plan, boots the sim and writes `MatchInstalled`. +## Cache, and poking it: `iw4l-artifacts/cache//`, content-addressed -## Cache - -`iw4l-artifacts/cache//`, content-addressed, leaf in -`asset_transport::artifact_cache` (`cache_get` / `cache_put`, `fnv1a64`). A -miss is silent: the caller computes the value anyway, and a hit is obliged to -be the **same bytes** a miss would have written. The key names every -input; if the encoder changed, bump the format word at the caller. Live kinds: -`mips` (`asset_material/material_images.rs`), `wgsl` -(`render_frontend/.../wgsl_disk_cache.rs`). - -## How to poke it - -`IW4L_GAMES` is the root holding the game trees; no folder name is hardcoded -anywhere (T1); the container version picks the decoder, not the folder name -(T2). Live, it is `make map mp_boneyard` ([`RUN.md`](RUN.md)); the stages of the -walk are visible in `LoadProgress` and in the `.pftrace` ([`PERF.md`](PERF.md)). +Leaf in `asset_transport::artifact_cache` (`cache_get` / `cache_put`, `fnv1a64`). +A miss is silent — the caller computes the value anyway — and a hit must be the +**same bytes** a miss would have written. The key names every input; if the +encoder changed, bump the format word. Live kinds: `mips`, `wgsl`. `IW4L_GAMES` +is the root holding the game trees; no folder name is hardcoded and the container +version picks the decoder. Live it is `make map mp_boneyard` ([`RUN.md`](RUN.md)), +with stages in `LoadProgress` and the `.pftrace`. diff --git a/docs/MASTER.md b/docs/MASTER.md index bf096f6..8fd4b40 100644 --- a/docs/MASTER.md +++ b/docs/MASTER.md @@ -1,80 +1,50 @@ # Your own master -`iw4l-master` is a relay and a server browser, not a game server: the host's -client simulates the match. No dedicated server ships in this repository. The -crate pulls in no engine dependency, so a VPS needs neither assets nor a GPU. -Publishing *our* releases is [`DEPLOY.md`](DEPLOY.md). +`iw4l-master` is a relay and server browser, not a game server: the host's client +simulates the match. It pulls in no engine dependency, so a VPS needs no assets +and no GPU. Publishing our own releases: [`DEPLOY.md`](DEPLOY.md). -## Install - -Everything runs from the machine holding the clone; the VPS needs only ssh. +## Install — from the machine with the clone; the VPS needs only ssh ```bash cargo xtask master install root@1.2.3.4 cargo xtask master logs root@1.2.3.4 --since 10min ``` -`install` mints a CA and a server certificate under `~/.iw4l/ca`, builds the -static binary (`crt-static`, so the VPS glibc version does not matter), places -it in `/usr/local/lib/iw4l/` and the certificates in `/etc/iw4l/` (key -`0640 root:iw4l`), writes the unit through `iw4l-master print-unit`, opens the -channel port and runs `enable --now`. The CA private key never leaves the -local machine. +`install` mints a CA and server certificate under `~/.iw4l/ca`, builds a static +binary, installs it with the certificates under `/usr/local/lib/iw4l/` and +`/etc/iw4l/`, writes the unit through `iw4l-master print-unit` and runs +`enable --now`. The CA key never leaves the local machine. `master update`, +`status` and `uninstall` follow, each taking `--channel dev` and `--ca DIR`; +`uninstall` keeps the certificates and CA, so a reinstall stays trusted. -The binary prints its **own** unit, so `ExecStart` cannot drift from what -`serve` parses; port and unit name come from `master_protocol::Channel`. Then -`master update` (rebuild, upload, restart), `status`, `uninstall`. Every verb -takes `--channel dev` for the second port and `--ca DIR` for another CA; -`uninstall` keeps both certificates and CA, so a reinstall stays trusted by -everyone who already has your `iw4l-ca.pem`. +## No domain required — the certificate is not tied to one -## No domain required +The client connects by address but checks the certificate against a **separate** +name — `IW4L_MASTER_SERVER_NAME`, a fixed label — so nothing in the SAN depends on +the host; with `IW4L_MASTER_CA_CERT` set it loads only that PEM into an empty +`RootCertStore` (`net/src/transport/master.rs:3107`), bypassing the platform +verifier. `install` signs `San::Labels` alone: the certificate is minted **once +per user, not per server**, and a new IP or VPS keeps it. -The client connects by address but verifies the certificate against a -**separate** name, and with `IW4L_MASTER_CA_CERT` set it puts only that PEM -into an empty `RootCertStore` (`net/src/transport/master.rs:3107`, `:3150`) — -the platform verifier is bypassed entirely. +## What it records — nothing on disk -| path | certificate checked against | host in SAN | -|---|---|---| -| client → master, QUIC | `IW4L_MASTER_SERVER_NAME`, a fixed label | not needed | -| updater → Caddy, HTTPS | host from `IW4L_UPDATE_URL` (`updater:267`) | needed | +Rooms, the advertised match name, peers and room membership live in `ServiceState` +in memory, gone when the room closes or the process restarts. No account, history, +analytics or telemetry. What survives is the systemd journal — +startup and failures, not matches or players — under the VPS's own `journald` +retention. Peer IPs are visible to the kernel and to any packet capture on that +host while a connection is open, as with any server. Run it for others and that is +the honest description: it forwards packets and vouches for nobody. -`San::WithHost` / `cert_covers_host` in `xtask/src/certs.rs` serve the second -row, our CDN. Your own master runs no Caddy, so `install` signs `San::Labels` -alone and nothing in the SAN depends on the host: the certificate is minted -**once per user, not per server** — a new IP or a new VPS keeps the same one. +## Hand out to players — this block plus `iw4l-ca.pem` -## What it records - -Nothing on disk. `iw4l-master` opens no file, no database and no log of its own: -the state it holds — rooms, the advertised match name the host typed, peers and -who is in which room — lives in `ServiceState` in memory and is gone when the -room closes or the process restarts. There is no account, no history and no -analytics; the runtime sends no telemetry to it. - -What survives is the systemd journal, and only what the process writes to -stderr — startup and failures, not matches or players. Its retention is the -VPS's `journald` configuration, not ours; `cargo xtask master logs --since` just -reads it. The transport is QUIC, so peer IP addresses are visible to the kernel -and to any packet capture on that host for as long as a connection is open, the -same as for any server you connect to. - -If you run the relay for other people, that is the honest description to give -them: it forwards packets between clients who are already talking to each other, -and it vouches for nobody. - -## Hand out to players - -`install` prints the block below; give it and `iw4l-ca.pem` to your friends: +The host opens a match with `IW4L_MASTER_HOST_NAME='name' make map mp_boneyard`, +the rest join from `make menu`, 2..=18 (`IW4L_MASTER_MAX_PLAYERS`). Trust rests +entirely on pinning `ca.pem`, so hand it over a trusted channel. ```dotenv IW4L_MASTER_ADDR=1.2.3.4:4433 IW4L_MASTER_SERVER_NAME=iw4l-prod IW4L_MASTER_CA_CERT=/path/to/iw4l-ca.pem ``` - -The host opens a match with `IW4L_MASTER_HOST_NAME='name' make map mp_boneyard`; -everyone else joins from `make menu`, 2..=18 (`IW4L_MASTER_MAX_PLAYERS`). The -label `iw4l-prod` is identical for everyone, so trust rests entirely on pinning -`ca.pem` — hand it over a channel the players trust. diff --git a/docs/RENDER.md b/docs/RENDER.md index 36fdd0b..4759e83 100644 --- a/docs/RENDER.md +++ b/docs/RENDER.md @@ -1,87 +1,50 @@ # Render: what lives where -An island of nine crates: `render` (composition and diagnostics only), -`render_frontend`, `render_gpu`, `render_frame`, -`render_backend`, `render_material`, `render_anim`, `render_fx`, -`render_scene`. `render_frontend` reads the presented snapshot, `render_gpu` -writes the pixels. The island decides no game logic and never writes -`AuthorityWorld`. No asset decoding happens here. +Nine crates. `render_frontend` reads the presented snapshot, `render_gpu` writes +the pixels and solely owns `wgpu`. The island decides no game logic, never +writes `AuthorityWorld`, decodes no assets. -## What lives where: `render_frontend` (three seams) + neighbours - -* `prepare/` — scene/view/cull (`scene/world.rs`, `scene/spawn.rs`, - `scene/cull.rs`, `scene/camera.rs`, `scene/gfx_scene.rs` anim-submission - bridge, `worker_cmds.rs`); occupancy Resources, lighting request/result, frustum planes, and camera - pose — `render_scene`; -* `adapters/` — occupancy out of animation and FX (`adapters/anim/*`, - `adapters/fx/*`); -* `assemble/` — the immutable products of a frame (`drawsurf/retained_list.rs`, - `drawsurf/frame_products.rs`, `drawsurf/material_runtime.rs`, - `drawsurf/tess/`); -* `render_anim` / `render_fx` — producers of poses and FX; `render_scene` — - occupancy storage; `render_frame` — the immutable snapshot of frame products - (`SourceRevisions`, `PackedSegments`, pure packing) at the frontend ↔ GPU border; - `render_material` — administrator of the material tables; `render_backend` — - the list views; `render_gpu` — the sole owner of `wgpu`; - `render::diag/` — observation. +| crate | owns | +|---|---| +| `render_frontend` | `prepare/` scene, view, cull (`scene/{world,spawn,cull,camera}.rs`, `scene/gfx_scene.rs` anim-submission bridge, `worker_cmds.rs`); `adapters/` occupancy out of anim and FX; `assemble/` frame products (`drawsurf/{retained_list,frame_products,material_runtime}.rs`, `drawsurf/tess/`) | +| `render_scene` | occupancy storage: lighting request/result, frustum planes, camera pose. `render_anim` / `render_fx` produce the poses and FX | +| `render_frame` | the immutable frame-product snapshot at the frontend ↔ GPU border (`SourceRevisions`, `PackedSegments`, pure packing) | +| `render_material`, `render_backend` | the material tables; the list views | +| `render` | composition, `extract.rs`, `diag/` | ## The frame path ```text -zone → asset_iw4 IR → prepare/scene/world.rs the world in our own types - → prepare/scene/spawn.rs GPU spawn - prepare/scene/camera.rs (look/WASD), frustum.rs + cull.rs + dpvs_iw4 - → assemble/drawsurf/list.rs ONE sorted GfxDrawSurf[] - → setup.rs R_SetupMaterial — techType → technique - → state.rs R_ChangeState — GfxPassState / blend +zone → asset_iw4 IR → prepare/scene/world.rs (our own types) → spawn.rs (GPU) + camera.rs (look/WASD), frustum.rs + cull.rs + dpvs_iw4 + → assemble/drawsurf/list.rs ONE sorted GfxDrawSurf[] + → setup.rs techType → technique → state.rs GfxPassState / blend → tess/{world,smodel,xmodel,fx,mark,glass,particle_cloud}.rs - → material_runtime.rs N-pass execution or a typed refusal - → sm3*.rs SM3 tokens (`d3d9_sm3`) → WGSL → gpu_contract.rs → wgpu + → material_runtime.rs N-pass execution or a typed refusal + → sm3*.rs SM3 tokens (`d3d9_sm3`) → WGSL → gpu_contract.rs → wgpu ``` -The key point: **one** drawsurf machine, with only the tess emitters forking -per surface type. World, static models and the viewmodel are not three -renderers; they are three emitters into one list. Lighting: `lighting_iw4` (light grid) plus -`prepare/scene/smodel_lighting.rs` and the `model_lighting_atlas.rs` atlas; -shadows are `assemble/drawsurf/sun_shadow.rs`. +**One** drawsurf machine; only the tess emitters fork per surface type. World, +static models and the viewmodel are three emitters into one list, not three +renderers. Lighting is `lighting_iw4` (light grid) with +`prepare/scene/smodel_lighting.rs` and `model_lighting_atlas.rs`, shadows +`assemble/drawsurf/sun_shadow.rs`. -## GPU-side ownership +## GPU-side ownership: `render_gpu/src/drawsurf/colour_submit/`, three owners -`render_gpu/src/drawsurf/colour_submit/` — one list, three owners: - -* `geometry.rs` + `residency.rs` — stream residency. Allocation capacity, live - length, allocation generation and the uploaded producer revision are four - different quantities: a frame that shrank does not recreate the buffer, and a - producer segment that changed (`render_frame::PackedSegments`) is uploaded - alone rather than the whole stream; +* `geometry.rs` + `residency.rs` — residency. Capacity, live length, allocation + generation and uploaded revision are four quantities: a shrunk frame keeps its + buffer and a changed `PackedSegments` segment uploads alone; * `prepare_camera.rs` — shared installation resets generations and publishes - shadow views, then sun, spot and camera prepare in order. Executors and arenas - are pass-local; binding slots and the shadow texture table still share mutable - ownership. The camera's pretess cache is `CameraWorldPretess`; shadows do not - hold it. There is exactly one camera: a second `Camera3d` is the typed - refusal `MultipleCameraViews`; -* `record.rs` + `indirect.rs` — recording. The direct path is `draw_indexed`; - with `IW4L_MULTI_DRAW` plus `INDIRECT_FIRST_INSTANCE` and - `DownlevelFlags::INDIRECT_EXECUTION`, adjacent compatible runs of the same - plan are written as a single `multi_draw_indexed_indirect` (the `multi_draw` / - `multi_draw_cmds` counters). Order and per-command `first_instance` do not - change. + shadow views; sun, spot and camera then prepare in order, on pass-local + executors. A second `Camera3d` is the typed refusal `MultipleCameraViews`; +* `record.rs` + `indirect.rs` — recording. Direct path `draw_indexed`; with + `IW4L_MULTI_DRAW` and the indirect downlevel flags, adjacent compatible runs + merge into one `multi_draw_indexed_indirect`, order preserved. -The `render/src/extract.rs` bridge publishes `InstalledRenderWorld` when static -geometry, ports or SMC maps change, and `PublishedRenderFrame` every frame, -checking frame/world/material identity. -CPU image handles share the `assets::image_handles` Arc with the upload registry. -The loader's decisions are made in -`render_frontend/src/prepare/scene/world_gpu.rs::consume_gpu_load_progress`. - -## Neighbours - -`d3d9_decl` / `d3d9_sm3` / `d3d9_state` carry D3D9 semantics without a single -word about IW4; the only D3D9 → `wgpu` crossing is the adapter inside `render`. -`session` stands the match up and tears it down, `assets` owns the bytes. - -## How to poke it - -There are no live `r_*` dvars. Live, it is env -vars (`IW4L_SINGLE_CELL`, `IW4L_SUN_SHADOW_*`, …) and native Perfetto -(`IW4L_PERF=1`, [`PERF.md`](PERF.md)). +`extract.rs` publishes `InstalledRenderWorld` when static geometry, ports or SMC +maps change, and `PublishedRenderFrame` every frame. `d3d9_decl` / `d3d9_sm3` / +`d3d9_state` carry D3D9 semantics with no word about IW4; the only D3D9 → `wgpu` +crossing is the adapter inside `render`, while `session` stands the match up and +`assets` owns the bytes. No live `r_*` dvars: env vars (`IW4L_SINGLE_CELL`, +`IW4L_SUN_SHADOW_*`, …) and Perfetto (`IW4L_PERF=1`, [`PERF.md`](PERF.md)). diff --git a/docs/RUN.md b/docs/RUN.md index 284a6c1..d88cba7 100644 --- a/docs/RUN.md +++ b/docs/RUN.md @@ -2,66 +2,49 @@ How to touch the live process. Observation is [`PERF.md`](PERF.md). -## Launch - ```bash set -a; . ./.env; set +a # IW4L_GAMES; DISPLAY=:0 if the session has none make map mp_boneyard CMDS='spawn assault; wait 2s; quit' cargo run --profile play -p launcher -- map iw5:mp_overwatch --cmds '…' ``` -`make` does not pass foreign flags through: in the `Makefile` it is the `CMDS` -variable, on the binary it is `--cmds`. The colon is a GNU make pattern: -`make map iw5:…` fails; write `make map ZONE=iw5:mp_overwatch` or use -`cargo run`. Recipes: `make scenario` / `chaos` / `bench-demo` / `bench-live` / -`lifecycle-*` (`*_CMDS` in the `Makefile`). Live recipes use `[profile.play]`; -for LTO, `PROFILE=release`. +`make` passes no foreign flags through: in the `Makefile` it is `CMDS`, on the +binary `--cmds`. The colon is a GNU make pattern, so `make map iw5:…` fails — +write `make map ZONE=iw5:mp_overwatch` or use `cargo run`. Recipes: `make +scenario`, `chaos`, `bench-demo`, `bench-live`, `lifecycle-*` (`*_CMDS` in the +`Makefile`). Live recipes use `[profile.play]`; for LTO, `PROFILE=release`. -## The match hasn't started — controls are frozen +**Controls are frozen until the match starts.** `sim::step` allows movement only +in `MatchPhase::Playing`; `hold +attack`, `+forward` and `move` do nothing before +then. You wait out `PLAYER_WAIT_MS` 15 s + `MATCH_START_MS` 5 s +(`crates/gamemode_iw4/src/prematch.rs`) — bots (≥ 2 live players) cut the first +timer short, a single-player script waits the full 20 s. The verb is shorter: +`wait world; spawn assault; force_match_start; wait 1s; …`, and a script +involving firing, hits or decals **lies with a zero without it**. -`sim::step` allows movement only in `MatchPhase::Playing`. Before that, -`hold +attack`, `+forward` and `move` do nothing. You have to wait out -`PLAYER_WAIT_MS` 15 s + `MATCH_START_MS` 5 s -(`crates/gamemode_iw4/src/prematch.rs`); the first timer is cut short by bots -(≥ 2 live players), a single-player script waits the full 20 s. The verb is -shorter: `wait world; spawn assault; force_match_start; wait 1s; …` — a script -involving firing / hits / decals **lies with a zero without -`force_match_start`**. - -## The script waits on its own (sync-by-default) - -Ritual `wait`s are unnecessary: a command holds the FIFO until its own fact — -`map`/`demo`/`play` until the swap plus GPU-ready `WorldScene::spawned`, -`spawn` until `AppScreen::InGame`, `move`/`look`/`tp` until a matching -presented pose, `force_match_start` until `Playing`, `disconnect` until the -hold is torn down. `wait Ns` remains, for pacing and observation windows. -The `&` suffix is a loud refusal to wait (`map mp_rust &`), `!` jumps the queue -(in bash write `disconnect !`, otherwise history expansion bites). The 120 s -gate timeout aborts the rest of the script with an echo. Queue parsing is in -`crates/console/src/`. -Hand-typed `dump`/`clip`/`screenshot` pass through any wait while preserving -the queue; inside `--cmds` that takes an explicit `!`. +**Sync by default.** Ritual `wait`s are unnecessary: a command holds the FIFO +until its own fact — `map`/`demo`/`play` until the swap plus GPU-ready +`WorldScene::spawned`, `spawn` until `AppScreen::InGame`, `move`/`look`/`tp` +until a matching presented pose, `force_match_start` until `Playing`, +`disconnect` until the hold is torn down. `wait Ns` remains, for pacing and +observation windows. The `&` suffix is a loud refusal to wait (`map mp_rust &`), +`!` jumps the queue (in bash write `disconnect !`, or history expansion bites). +The 120 s gate timeout aborts the rest with an echo; parsing is +`crates/console/src/`. Hand-typed `dump`/`clip`/`screenshot` pass through any +wait while preserving the queue, which inside `--cmds` takes an explicit `!`. ## Verbs and traps `map spawn class give attach name kill damage move tp look nudge press hold release bind bot wait mark record stoprecord clip demo dump screenshot ui disconnect quit` -plus the debug `force_match_start` / `showpos`. -`bot` is `add | hold | tp | give | fire`; `dump [name]` writes the current -snapshot into `dumps/`. -`clip` writes the last available 45 s into `clips//` (demo + dump) on the -host and on the clients; the host records authority, a client records the -snapshots it received plus its own presented state. -The source is named in the manifest; `demo LATEST` plays the clip back. -* ADS is `hold +speed_throw`, not `+speed`; -* bolt action (`fire_type=1`): `hold +attack` is **one** shot; a full magazine - will not reload itself (`press +attack` ×N, then `press +reload`); -* `look` without `LookState` only writes `ps.viewangles` — the script is not - aiming; -* `give` takes a namespace: `give t5:weapon/psg1_acog`, `give iw5:weapon/msr`. +plus the debug `force_match_start` / `showpos`. `bot` is +`add | hold | tp | give | fire`; `dump [name]` writes the current snapshot into +`dumps/`. `clip` writes the last available 45 s into `clips//` (demo + dump) +on host and clients — the host records authority, a client the snapshots it +received plus its own presented state; `demo LATEST` plays it back. -`make bench-load-session` runs `make menu` with the script -`mark in_menu; map mp_hanoi; spawn; mark in_hanoi; map mp_underpass; spawn; mark in_underpass; quit`. -`python3 scripts/bench-marks.py ` computes the intervals between engine -marks; `--json` gives machine-readable output. Build and menu startup are not -part of the intervals. +* ADS is `hold +speed_throw`, not `+speed`; +* bolt action (`fire_type=1`): `hold +attack` is **one** shot, and a full + magazine will not reload itself (`press +attack` ×N, then `press +reload`); +* `look` without `LookState` only writes `ps.viewangles` — no aiming; +* `give` takes a namespace: `give t5:weapon/psg1_acog`, `give iw5:weapon/msr`. diff --git a/docs/WINDOWS.md b/docs/WINDOWS.md index 1afa34a..c8dd6c5 100644 --- a/docs/WINDOWS.md +++ b/docs/WINDOWS.md @@ -1,51 +1,15 @@ # Portable Windows folder -`make launcher windows` creates `dist/windows/iw4l-windows-dev.zip` and -`dist/windows/iw4l-windows-prod.zip`. Both archives use the password -`contextrot` and are self-contained — the game binary is in the archive, so the -folder runs with no network at all: - -```text -.env -iw4l-ca.pem -iw4launcher.exe -iw4l.exe -LICENSE -NOTICE -OFL-Oxanium.txt -COPYING-FreeFont.txt -``` - -The four licence files are not decoration: `iw4l.exe` has both fonts compiled -into it with `include_bytes!`, so an archive carrying the binary carries their -licences too. `release.rs::LEGAL_FILES` is the list, and a build that ships -without them is the bug. - -Extract the selected archive into any dedicated folder and add ordinary Windows -`.lnk` shortcuts to installed Call of Duty title folders or title executables. -MW2 **Multiplayer** files are required for the menu (`zone/**/common_mp.ff`); -a folder containing only `zone/dlc/*.ff` is insufficient. BO1 and MW3 are optional. -Run the launcher; it starts the game without modifying those installations. - -**Updating is something you ask for.** Plain `iw4launcher.exe` starts the -`iw4l.exe` that is already in the folder and touches nothing else. Fetching a -newer build is `iw4launcher.exe update` (or `IW4L_UPDATE=1` for a shortcut that -cannot pass an argument), which needs `IW4L_UPDATE_URL` in the portable `.env`. -No build replaces its own binary on its own. How a release gets published is -[`DEPLOY.md`](DEPLOY.md). - -Steam shipped MW2 and MW3 re-releases whose FastFiles are serialized with -64-bit pointers. MW2 is read in both serializations; **MW3 x64 zones are -detected and refused**, so an MW3 shortcut to such an install contributes no -weapons and no maps, and says so in the log -(`IW5 zone is serialized x64 (Steam re-release)`). +`make launcher windows` creates `dist/windows/iw4l-windows-{dev,prod}.zip`. Both +archives use the password `contextrot` and are self-contained — the game binary +is inside, so the folder runs with no network at all. ```text iw4l-portable/ ├── iw4launcher.exe from the archive ├── iw4l.exe from the archive; replaced only by `iw4launcher update` ├── iw4l-ca.pem public trust anchor -├── LICENSE NOTICE … the four licence files, kept with the binary +├── LICENSE NOTICE OFL-Oxanium.txt COPYING-FreeFont.txt ├── .env from the archive; player edits win forever ├── Modern Warfare 2.lnk target: game folder or title .exe ├── Black Ops.lnk optional @@ -53,28 +17,34 @@ iw4l-portable/ └── iw4l-artifacts/ saves, caches, demos, logs and captures ``` -The folder containing the two executables is the process working directory even -when Explorer supplies another one. `IW4L_GAMES` defaults to this folder on -Windows; `.lnk` targets directly inside it are additional read-only search -roots. An explicit `IW4L_GAMES` still works and may itself contain shortcuts. +The four licence files are not decoration: `iw4l.exe` has both fonts compiled in +with `include_bytes!`, so an archive carrying the binary carries their licences +too. `release.rs::LEGAL_FILES` is the list; a build shipping without them is a +bug. -`.env` is loaded before release-manifest defaults. The launcher creates it only -when missing and never overwrites it. `IW4L_UPDATE_URL` is read only by -`iw4launcher.exe update`, never by a plain start; the same `iw4launcher.exe` -serves both channels. +Extract into any dedicated folder and add ordinary Windows `.lnk` shortcuts to +installed Call of Duty title folders or executables. MW2 **Multiplayer** files +are required for the menu (`zone/**/common_mp.ff`); `zone/dlc/*.ff` alone is +insufficient. BO1 and MW3 are optional. The launcher starts the game without +modifying those installations. -```dotenv -IW4L_UPDATE_URL=https://host:8443/prod -IW4L_MASTER_ADDR=master.example.net:4433 -IW4L_MASTER_SERVER_NAME=iw4l-prod -IW4L_MASTER_CA_CERT=iw4l-ca.pem -``` +**Updating is something you ask for.** Plain `iw4launcher.exe` starts the +`iw4l.exe` already in the folder and touches nothing else. Fetching a newer +build is `iw4launcher.exe update` (or `IW4L_UPDATE=1` for a shortcut that cannot +pass an argument), which needs `IW4L_UPDATE_URL` in the portable `.env`. No +build replaces its own binary on its own. Publishing is [`DEPLOY.md`](DEPLOY.md). -An unreachable master is not a launch error: the browser keeps retrying and -shows the connection error. All writable game state stays below -`iw4l-artifacts/` (including Windows `settings.cfg`, unless +Steam shipped MW2 and MW3 re-releases whose FastFiles are serialized with 64-bit +pointers. MW2 is read in both serializations; **MW3 x64 zones are detected and +refused**, so an MW3 shortcut to such an install contributes no weapons and no +maps, and says so in the log (`IW5 zone is serialized x64 (Steam re-release)`). + +The folder holding the two executables is the process working directory even +when Explorer supplies another; `IW4L_GAMES` defaults to it, and `.lnk` targets +inside it are additional read-only search roots. `.env` is loaded before +release-manifest defaults, created only when missing and never overwritten. + +`.env` holds `IW4L_UPDATE_URL=https://host:8443/prod` plus the master keys +from [`MASTER.md`](MASTER.md). An unreachable master is not a launch error: the browser keeps retrying. All +writable state stays below `iw4l-artifacts/` (including `settings.cfg`, unless `IW4L_SETTINGS_PATH` overrides it); shortcut targets are never output paths. - -Startup errors print the detected FastFile counts, missing base assets and log -path in the console. On Windows, an interactive console waits for Enter before -closing; redirected input exits immediately with code 2. No extra dialog opens.