WIP: Advanced demo tools — seekable playback + cinematic cameras (#377)#849
Draft
Eclipse1982 wants to merge 18 commits into
Draft
WIP: Advanced demo tools — seekable playback + cinematic cameras (#377)#849Eclipse1982 wants to merge 18 commits into
Eclipse1982 wants to merge 18 commits into
Conversation
Server-hosted, client-controlled v2 demo playback; free/follow camera; seek/pause/speed/reverse; serverrecord omniscient multi-POV; director camera paths + ObjectivelyMVC timeline scrubber. OBS plugin deferred.
New src/common/demo.{h,c}: a versioned, timecoded, keyframed demo container
shared by the (forthcoming) client/server recorders and playback engine.
- QDM2 magic + header + epoch block; legacy v1 detection via Demo_IsV2.
- Self-framing timecoded records (FRAME_DELTA/FRAME_KEY/RELIABLE/CAMERA).
- Seek index footer (write/read) with a scan-based rebuild fallback for
footer-less (crash-truncated) files, and Demo_IndexFloor for seek lookup.
- Portable little-endian field I/O; depends only on common (no libnet).
Unit-tested in src/tests/check_demo.c (8 checks: header/record/index
round-trips, magic detection, floor lookup, scan fallback). No engine
behavior changed; module is not yet referenced.
Adds cg_demo.{c,h}: a detached free-fly camera (PM_SPECTATOR) controllable
during demo playback, toggled with the new 'demo_freecam' command (cvar
cg_demo_freecam). The recorded player stays frozen; only the view is
overridden, so this works on existing v1 demos as well as v2.
- Cg_UsePrediction grants prediction to the freecam (bypassing the demo
freeze) so the camera integrates from local input via Cg_PredictMovement.
- Cg_PrepareView routes to Cg_UpdateDemoCamera when active, skipping the
recorded-player origin/angles, third-person and bob.
- First-person weapon model suppressed while the freecam is active.
- Camera speed is real-time (driven by command msec), independent of demo
playback speed, so it works during slow-motion/fast-forward.
Follow/cycle POV is deferred to land with multi-POV server demos (P5).
Records demos in the v2 container (cvar cl_demo_v2, default on) by wrapping the exact v1 wire messages as timecoded, self-framing records: startup (server_data/config strings/baselines) as RELIABLE records at timecode 0, then per-frame FRAME_KEY/FRAME_DELTA records (datagram-only packets as RELIABLE), stamped with a demo-relative timecode. v1 recording is preserved when cl_demo_v2 is 0. Server-hosted playback detects the QDM2 magic (Demo_IsV2) and drives a demo clock (sv.demo_time, advanced per tick, scaled by time_scale). Sv_SendClientPackets transmits every record whose timecode has been reached, reframed exactly as the client expects, so playback parses identically to live. Legacy v1 demos fall through to the original one-message-per-tick path unchanged. This lays the timecoded substrate for seeking (P4) without touching the delta-compression or re-encoding frames. No protocol change.
Recorder now emits a re-encoded self-contained keyframe every DEMO_KEYFRAME_INTERVAL frames (full player state + every entity from its baseline, byte-identical to a natural uncompressed frame), giving seeking nearby entry points. The server scans these on load (Demo_ScanIndex) into a keyframe index. New server commands (listen-server console during v2 playback): - demo_seek <s|+s|-s> jump to nearest keyframe <= target, replay to target - demo_pause freeze/resume the demo clock - demo_speed [x] playback rate 0.1-8.0 (independent of the realtime cam) - demo_step [n] step n frames and pause Seeks need no client-side discontinuity handling: Cl_UpdateLerp already clamps cl.time into the current frame's range each frame, and a keyframe forces a no-lerp snap. Reverse = repeated backward seeks.
Adds a 'Demo' tab to Settings > Controls (DemoBindsViewController, mirroring MovementCombatViewController) with BindTextView rows for: toggle free camera, pause/resume, skip back/forward 5s, step back/forward, slower/faster. Adds demo_speed_up / demo_speed_down relative server commands so playback speed is bindable. Wired into both the autotools (ui/controls/Makefile.am) and MSBuild (cgame.vcxproj) builds.
Adds a 'Demos' main-menu entry (DemosViewController in ui/play) that lists
recorded demos/*.demo in a table (cgi.EnumerateFiles) and plays the selected
one via cgi.Cbuf("demo <name>") on the Play button or a double-click, with a
Refresh button. Wired into autotools (ui/play/Makefile.am) and MSBuild
(cgame.vcxproj).
The P1 demo module was wired into the autotools build but the matching Windows (MSBuild) project entry was added during local build setup and not committed. This ensures a fresh checkout compiles demo.c on Windows.
…lan#377) New src/server/sv_demo.c records a v2 demo entirely server-side: sv_record [name] / sv_stoprecord. Each tick (after the game frame) it captures an omniscient snapshot of every visible entity (no per-client PVS limiting) plus a default player state following the first in-game client, delta-encoded into v2 FRAME records with periodic keyframes, behind the same epoch (server data / config strings / baselines) a client demo uses. Plays back through the existing server-hosted v2 path; the free camera detaches as usual. Sv_WriteEntities / Sv_WritePlayerState are exposed (un-static) for reuse. Recording stops cleanly on map change (Sv_ClearState). No protocol change; default single POV. Per-player POV switching follows via client-side follow/cycle (chase of player entities, which are all present omnisciently).
Generalizes the demo camera into a mode enum (locked / free / follow) and adds demo_follow_next / demo_follow_prev: a third-person chase that cycles through the player entities present in the frame. Since every player's entity is recorded (omnisciently for serverrecord, and present in any demo), this gives per-player POV switching with no protocol change. Wired into the Demo binds tab. cg_view/cg_entity now gate on Cg_DemoOverridingView (free OR follow).
Adds a director camera: fly the free camera and demo_cam_add drops a keyframe (time + origin + angles); demo_cam_play flies a smooth Catmull-Rom dolly through them (shortest-arc angle blend), driven by the demo timeline so it tracks scrubbing. demo_cam_clear, and demo_cam_save/load to a sidecar (demos/cinematic.cam). Drop-keyframe and play are bindable in the Demo tab.
The demo-playback server pushes its status (time, duration, paused, speed) to the cgame each frame via the CS_DEMO_STATUS config string (transmitted explicitly, since the demo path does not flush the multicast). The cgame draws a non-modal timeline bar at the bottom of the screen during playback (Cg_DrawDemoBar): progress fill + playhead + MM:SS / MM:SS readout with the speed and a PAUSED indicator. Completes the P6 director tooling.
…lan#377) Seeking dropped the client with 'Delta frame too old' because recorded delta frames can reference a base older than the seek-target keyframe (listen-server ack timing means delta_frame_num isn't always frame-1). Once seeking skips those bases, the chain breaks. The recorder now re-encodes any frame whose delta_frame_num precedes the last keyframe as a clean delta from the immediately-preceding frame (mirroring Sv_WriteEntities), so every frame after a keyframe is reconstructable from that keyframe forward. Frames whose delta is already safe are still captured verbatim (datagrams/sounds preserved); only the unsafe few are rebased.
…ame (jdolan#377)" This reverts commit 5eac574.
Re-architect v2 demo playback so seeking can't break the delta chain:
- The recorder splits each server packet into [reliable prefix]
[isolated frame][datagram suffix] records, so the frame command is in
pure engine wire format the server can decode without the cgame.
- On load the server decodes the whole demo through a PACKET_BACKUP-deep
ring (mirroring the client, so each frame decodes against its true
delta base, not just the previous frame) and re-emits a clean stream:
a keyframe every DEMO_KEYFRAME_INTERVAL frames plus sequential deltas,
renumbered from 1 so no delta ever bases off frame 0 (which the client
reads as uncompressed). Older un-split demos are handled by skipping
the engine-command prefix.
- Throttle the seek catch-up to a few records per tick so the replayed
backlog cannot overrun the 64-slot loopback ring and silently drop
frames ("Delta frame too old").
…olan#377) Demo playback replays sounds and effects that may reference assets absent from the local install; several cgame paths dereferenced them blind: - Cg_ParseSound consumed only part of its message on a NULL sample, desyncing the rest of the packet ("Illegible server message"); it now reads the whole message before bailing. - Guard NULL precached assets at the choke points: Cg_AddSprite (NULL media, replacing a release-stripped assert), Cg_AnimationLifetime (NULL animation), Cg_AddDecal (NULL image), and Cg_RippleSplashEffect (NULL brush-side material plus an off-by-one bound). Cg_ClientEffectColor now range-checks the wire client index instead of asserting.
- Add an auto-orbit camera (demo_orbit), time-constant smoothing for the follow/orbit cameras, a cinematic FOV override, letterbox bars, free-cam roll, HUD and timeline toggles, and a one-shot demo_cinematic preset. - Reset all demo camera state between demos (Cg_ClearDemo via Cg_ClearState) and re-acquire a live follow target each frame, so the camera never locks onto a stale "ghost" entity after a seek or a player leaving the view. - Expand the Settings -> Controls -> Demo bindings tab with all the new commands, organized into Camera / Cinematic / Playback / Camera path.
8aa15af to
8e77c8e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This implements "Advanced demo tools" (#377): a seekable v2 demo container, server-hosted scrubbable playback, multiple spectator/cinematic cameras, omniscient
sv_record, and the supporting UI. Built on thefeature/demo-systembranch.Addresses #377.
Changes
src/common/demo.{c,h}): timecoded, self-framing records with a keyframe seek index; legacy v1 demos still play through the original forward-only path.PACKET_BACKUP-deep ring (so each frame decodes against its true delta base) and re-emits a clean keyframe + sequential-delta stream, so seeking cannot break the delta chain. Seek catch-up is throttled to stay within the loopback ring.demo_cinematicpreset.sv_record: omniscient server-side multi-POV recording that plays back through the same path.Testing
make/make checkon Linux not run this round (tree still has the known pre-existingcheck_masterlink failure)Notes
This is a draft and still has rough edges — please don't merge as-is. Sharing it now to get a read on the approach before further polish.
Known unfinished / unvalidated areas:
sv_record(omniscient server demos) shares the playback path but has not been exercised end-to-end yet (needs a live multiplayer game to record one).Open questions for the maintainer: is server-hosted scrubbing the right playback model, and does the v2 container/record interaction warrant a
PROTOCOL_MINORbump? Feedback welcome.