Repository navigation
Releases: PinKushin/Tf2DemoSalvage
Release list
Tf2DemoSalvage 0.1.0-beta.55
Tf2DemoSalvage 0.1.0-beta.55
Changes in 0.1.0-beta.55
Gate for this release (every test executed, 0 failed): core 2713, cli 75, logging 37, fonts 5, animation 5352, scene 3379,
audio 383, presentation 1323, content 1859, corpus 236, rendering 1270, viewer 159, UI suite 84 of 84 with 0 skipped; CI on
the merge commit cb7dbb2 green with 0 annotations (Test run 40663194687, CodeQL run 40663194672). Figures are on the
cp_process_f12 recording, one machine, first person unless it says otherwise; the file beside each figure holds it.
- New setting:
demo_retimeview, default 1. Point-of-view demos recorded over a jittery connection get a smoother
first-person camera (D238). TF2 stamps each packet of a point-of-view demo with the time the recording client received it, and
the camera positions are spread along those stamps, so a connection that delivers two packets in one tick and none in the
next shows as uneven camera speed. The server's own tick is in every packet. With the setting on, the camera's position
is spread along the server's tick; the view angles stay on the stamps, as TF2 plays them.demo_retimeview 0gives TF2's
exact camera. To turn it off, putdemo_retimeview 0insettings.cfg, or for one run usetf2demoview <demo> +demo_retimeview 0.
On a stuttering demo a user reported (a pl_pier match, 23 players): camera-position pacing unevenness (CV of speed) 0.877 to 0.246, and
frames more than 50% off their neighbors 20.7% to 3.2% (docs/measurements/2026-10-27-retime-view-by-server-tick.md).
The same packet rate without any jitter reads 0.111, so what is left is partly the player's own motion (docs/measurements/2026-10-27-retime-view-by-server-tick.md).
A calm 2013 recording: 3.3% to 1.2% of frames. A modern loaded match (rgl-pug-2026-08-10-pov): 30.7% to 21.3%
(docs/measurements/2026-10-27-retime-view-by-server-tick.md).
Where the server itself lost time, the camera holds for a frame. A teleport (a respawn) that falls across a stamp gap is held, as
TF2's own teleport rule holds it, instead of being smeared over several frames (docs/measurements/2026-10-27-retime-view-by-server-tick.md).
A demo whose packets carry no server tick, or whose stamps and ticks disagree by more than 8 ticks, plays as TF2 plays it.
A SourceTV demo is unchanged by the setting: it records no view, and the one specimen checked answered nothing at all 23,899 of its ticks, on or off (docs/measurements/2026-10-27-retime-view-by-server-tick.md).
No frame-time figure is claimed; nothing there is a time (docs/measurements/2026-10-27-retime-view-by-server-tick.md). The costs are under Known gaps. - Opening a map takes about half a second longer, and no static prop's lighting is built while the demo plays (B607).
The viewer now builds every static prop's lighting when the map loads, as TF2 does, instead of when each prop first comes
into view. Before play on f12: +0.5-0.6 s (docs/measurements/2026-10-27-static-prop-handle-at-load.md).
In a 20 s run of play: 233 colour-mesh builds (10.8-17.3 ms) to 0, and static-state builds to 0 (docs/measurements/2026-10-27-static-prop-handle-at-load.md).
Those builds were a stall as each prop came into view, not a per-frame cost (docs/measurements/2026-10-27-static-prop-handle-at-load.md).
Frame time is unchanged: 8.37-8.38 ms against 8.35-8.39 ms (docs/measurements/2026-10-27-static-prop-handle-at-load.md).
The screenshot matches outside the frame-rate readout (docs/measurements/2026-10-27-static-prop-handle-at-load.md).
The load-time figures predate two counter fixes, so only the total, +0.5-0.6 s, is supported (docs/measurements/2026-10-27-static-prop-handle-at-load.md).
These figures were timed on the first implementation, before four review rounds changed it (a cached ranking for
bump-mapped props, read-only views of the cached lists, a dropped rebuild); the timing was not repeated afterwards, and f12 draws
no bump-mapped prop that would exercise the cached ranking (docs/measurements/2026-10-27-static-prop-handle-at-load.md). - Static props no longer check every dynamic-light slot (parity). The viewer read all 32 dynamic-light slots for every
static prop every frame; TF2 reads only the lights that are live.
Static-prop lighting (baked): 0.450-0.488 ms to 0.050-0.075 ms a frame, -0.40 to -0.44 ms (docs/measurements/2026-10-27-static-prop-lighting.md).
Frame: -0.37 to -0.47 ms, 4.4-5.5% (docs/measurements/2026-10-27-static-prop-lighting.md).
The screenshot matches outside the frame-rate readout (docs/measurements/2026-10-27-static-prop-lighting.md).
Not exercised by these runs: f12 had no live dynamic light and no styled light at any drawn static prop in the window, so
the dynamic-light path is checked by tests, not by this timing (docs/measurements/2026-10-27-static-prop-lighting.md).
Empty view: no change, because no prop is visited there (docs/measurements/2026-10-27-static-prop-lighting.md). - The HUD's font check takes constant time (B608, parity). Each character of HUD text used to search a list of font
handles; TF2 checks a bound.
On an empty view,hud0.61-0.81 ms to 0.25-0.30 ms, -0.41 ms; the chat panel 0.29-0.39 ms to 0.011 ms (docs/measurements/2026-10-27-empty-view-breakdown.md).
The frame time is not claimed to have fallen: the ranges overlap, and other passes read higher in the after build (docs/measurements/2026-10-27-empty-view-breakdown.md).
Steady window 3.81 to 3.35 ms; 3.45 to 3.35 ms with one outlying run left out (docs/measurements/2026-10-27-empty-view-breakdown.md).
First person is one run per build:hudfell 0.31 ms, with no spread, supporting only (docs/measurements/2026-10-27-empty-view-breakdown.md).
The screenshots match outside the frame-time overlay, but the chat had faded in both, so they cannot speak for the chat's
own text, and the world was not compared (docs/measurements/2026-10-27-empty-view-breakdown.md). - The world is walked only through the parts the visibility set reaches, as TF2 does (B259). The listed leaves and
surfaces are unchanged oncp_processandcp_badlands(docs/measurements/2026-10-28-pvs-node-prune.md).
Camera setup in first person: 0.761-0.770 ms to 0.404-0.415 ms, -0.36 ms, 5.3% of the frame (docs/measurements/2026-10-28-pvs-node-prune.md).
The sky view's share: 0.198 ms to 0.050-0.052 ms (docs/measurements/2026-10-28-pvs-node-prune.md).
Most of that saving became waiting. The main thread now finishes sooner and waits longer for the render thread to finish
the previous frame (camera wait0.253-0.263 ms to 0.554-0.568 ms); the render thread's own time did not change (docs/measurements/2026-10-28-pvs-node-prune.md).
Frame in the steady window: -0.12 ms, ranges apart, and only with one disturbed run left out (docs/measurements/2026-10-28-pvs-node-prune.md).
Over the whole 20 s the ranges overlap and there is no difference (docs/measurements/2026-10-28-pvs-node-prune.md).
Empty view: no change claimed (docs/measurements/2026-10-28-pvs-node-prune.md).
The screenshots match in the world at one paused tick with two cameras; nothing on a moving path was compared (docs/measurements/2026-10-28-pvs-node-prune.md).
Changes in 0.1.0-beta.54
Gate for this release (every test executed, 0 failed): core 2680, cli 75, logging 37, fonts 5, animation 5352, scene 3360,
audio 383, presentation 1321, content 1859, corpus 235, rendering 1252, viewer 159, UI suite 84 of 84 with 0 skipped; CI on
the merge commit green with 0 annotations. Figures are on the cp_process_f12 recording, one machine, first person unless it says
otherwise; the file beside each figure holds it.
- Attachment queries no longer rebuild a skeleton the same frame already built (parity). The viewer advanced its bone
clock once per drawing pass; TF2 advances it once a frame, so every query made after a pass saw a stale clock and rebuilt.
Attachment builds: 7.4-7.5 to 3.5 a frame (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md).
The burst search that makes the queries: 0.55-0.56 to 0.32-0.33 ms a frame (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md).
Frame: -0.43 to -0.53 ms, 4.7-5.8% (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md).
From tick 30031 on, the bones drawn equal the old build's on every label compared; the first ~30 ticks are outside what
that comparison can judge (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md). - Detail sprites (grass) are sorted per leaf, as TF2 sorts them. Gummo, first person, in detail range:
detail cost 0.729-0.753 ms to 0.454-0.459 ms a frame, -0.27 to -0.30 ms (docs/measurements/2026-10-27-detail-transport-b577.md).
Frame 8.93-9.27 ms to 8.71-8.83 ms, ranges apart (docs/measurements/2026-10-27-detail-transport-b577.md).
The sort is Valve's own heap sort, which is not stable, so sprites at exactly equal distance can swap places with
the previous version (docs/measurements/2026-10-27-detail-transport-b577.md). - New setting:
cl_threaded_bone_setup, default 0, as in TF2. At 1 the skeletons the previous frame used are posed
on the worker threads before the frame. With it on the pose equals the off pose on every label compared (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md).
setupfalls 0.13-0.17 ms (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md).
The frame time did not separate: 8.64-8.72 ms off, 8.53-8.68 ms on (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md).
It stays off. - No measured change: the playback bar's tick label is skipped when it has not changed, and a one-shot particle effect
that finished is served from a cache. Neither moved the frame (docs/measurements/2026-10-27-detail-transport-b577.md).
They stay as parity and allocation tidying. The cost in particle dispatch is in the live effects' offer...
Tf2DemoSalvage 0.1.0-beta.54
Tf2DemoSalvage 0.1.0-beta.54
Changes in 0.1.0-beta.54
Gate for this release (every test executed, 0 failed): core 2680, cli 75, logging 37, fonts 5, animation 5352, scene 3360,
audio 383, presentation 1321, content 1859, corpus 235, rendering 1252, viewer 159, UI suite 84 of 84 with 0 skipped; CI on
the merge commit green with 0 annotations. Figures are on the cp_process_f12 recording, one machine, first person unless it says
otherwise; the file beside each figure holds it.
- Attachment queries no longer rebuild a skeleton the same frame already built (parity). The viewer advanced its bone
clock once per drawing pass; TF2 advances it once a frame, so every query made after a pass saw a stale clock and rebuilt.
Attachment builds: 7.4-7.5 to 3.5 a frame (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md).
The burst search that makes the queries: 0.55-0.56 to 0.32-0.33 ms a frame (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md).
Frame: -0.43 to -0.53 ms, 4.7-5.8% (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md).
From tick 30031 on, the bones drawn equal the old build's on every label compared; the first ~30 ticks are outside what
that comparison can judge (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md). - Detail sprites (grass) are sorted per leaf, as TF2 sorts them. Gummo, first person, in detail range:
detail cost 0.729-0.753 ms to 0.454-0.459 ms a frame, -0.27 to -0.30 ms (docs/measurements/2026-10-27-detail-transport-b577.md).
Frame 8.93-9.27 ms to 8.71-8.83 ms, ranges apart (docs/measurements/2026-10-27-detail-transport-b577.md).
The sort is Valve's own heap sort, which is not stable, so sprites at exactly equal distance can swap places with
the previous version (docs/measurements/2026-10-27-detail-transport-b577.md). - New setting:
cl_threaded_bone_setup, default 0, as in TF2. At 1 the skeletons the previous frame used are posed
on the worker threads before the frame. With it on the pose equals the off pose on every label compared (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md).
setupfalls 0.13-0.17 ms (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md).
The frame time did not separate: 8.64-8.72 ms off, 8.53-8.68 ms on (docs/measurements/2026-10-27-bone-clock-and-threaded-setup.md).
It stays off. - No measured change: the playback bar's tick label is skipped when it has not changed, and a one-shot particle effect
that finished is served from a cache. Neither moved the frame (docs/measurements/2026-10-27-detail-transport-b577.md).
They stay as parity and allocation tidying. The cost in particle dispatch is in the live effects' offers, not the finished
ones, and is not yet split out (docs/measurements/2026-10-27-detail-transport-b577.md).
Changes in 0.1.0-beta.53
Released 2026-10-27.
Gate for this release (every test executed, 0 failed): core 2680, cli 75, logging 37, fonts 5, animation 5346, scene 3346,
audio 383, presentation 1306, content 1859, corpus 235, rendering 1252, viewer 157, UI suite 84 of 84 with 0 skipped; CI on
the release commit green with 0 annotations. Figures are on the cp_process_f12 recording, one machine, first person unless it says
otherwise; the file beside each figure holds it.
- The sun costs almost nothing now (B594). A sky trace used to test every piece of terrain along its whole 57,016-unit
length, though the walls had already stopped it. It now stops where the walls stop it, with the same answers.
Sun: 0.457-0.465 ms to 0.012-0.013 ms a frame, with the same 1.52-1.55 calls a frame (docs/measurements/2026-10-26-threaded-particles-and-sun.md).
Frame: 9.37-9.55 ms to 8.74-8.94 ms, -0.6 ms, -6.5% (docs/measurements/2026-10-26-threaded-particles-and-sun.md).
The sun is 0.45 ms of that; the rest is estimated, not timed, to be other traces that take the same limit (docs/measurements/2026-10-26-threaded-particles-and-sun.md).
Empty view: frame 3.60-3.63 ms to 3.53-3.55 ms, -0.06 ms (docs/measurements/2026-10-26-threaded-particles-and-sun.md).
On the real map, 0 of 50,000 traces differ from the old answer (docs/measurements/2026-10-26-threaded-particles-and-sun.md). - Particle effects are stepped on several threads, as TF2 does (
r_threaded_particles, default 1). The picture is the
same. The step splits across your physical cores minus one, and effects with 4 steps or fewer stay on one thread.
Particle step (bursts/step): 0.275-0.282 ms to 0.196-0.204 ms a frame, -0.07 to -0.09 ms (docs/measurements/2026-10-26-threaded-particles-and-sun.md).
The whole-frame gain is not established: frame 8.93-9.03 ms to 8.74-8.94 ms, and the two ranges touch (docs/measurements/2026-10-26-threaded-particles-and-sun.md).
To go back to one thread, putr_threaded_particles 0insettings.cfg, or use+r_threaded_particles 0for one run
(tf2demoview --help). Thresholds of 0 and 16 gave the same step as 4, so 4 is kept without a measured advantage (docs/measurements/2026-10-26-threaded-particles-and-sun.md). - A face uploads only the part of its stream that can move (B597). Each model sends its first-to-last animated
slots instead of the whole stream. The picture is the same (tests compare every pixel).
Upload: 3.155-3.159 MB to 1.261-1.265 MB a frame (docs/measurements/2026-10-26-b597-flex-range.md).
Flex span: 0.296-0.305 ms to 0.149-0.156 ms a frame (docs/measurements/2026-10-26-b597-flex-range.md).
Frame time did not separate: 10.04-10.17 ms before, 9.99-10.31 ms after (docs/measurements/2026-10-26-b597-flex-range.md).
A cache that skipped a face whose animation had not changed was tried and removed: under 1% of faces held still (docs/measurements/2026-10-26-b597-face-cache.md). - Occlusion tests are uploaded once a frame. Occlusion pass: 0.372-0.377 ms to 0.174-0.192 ms, -0.18 to -0.20 ms (docs/measurements/2026-10-26-b597-constant-ring-and-occlusion.md).
That file attributes no frame-time change to it. A second change tried beside it, constant rings, measured slightly
slower and was removed (D235). - Less garbage per frame. Three allocation sites removed. Gummo first person: 1.15-1.19 MB to 0.84-0.90 MB a frame,
-0.30 MB, -26%; GC pause 0.20-0.23 ms to 0.15-0.18 ms a frame (docs/measurements/2026-10-25-b599-three-allocation-sites.md).
Frame time did not separate (docs/measurements/2026-10-25-b599-three-allocation-sites.md). - Weapon scripts and face-sound lengths are read before play, not during it. Main-thread file reads in play: 36 to 0
in each of five runs; loading takes 51.5-65.9 ms for the face lengths (docs/measurements/2026-10-25-b595-part6-main-thread-reads-in-play.md).
The mean frame time did not move beyond run-to-run spread, and the weapon scripts' load cost was not measured (docs/measurements/2026-10-25-b595-part6-main-thread-reads-in-play.md). - Player animation variants are picked the way TF2 picks them. Where a class model has several sequences for one
activity, the viewer now chooses at random by their weights when the player enters the activity and holds that choice
until the activity changes, instead of always showing the heaviest. Gestures (flinches, reloads, voice commands) pick
their variant once when they start, the same way. Picks are reproducible: seeking to a moment shows the variant that
playing through to it showed, with three exceptions — a player who had been in that activity for more than 2048 ticks
(31 seconds on a 66-tick server), a change too brief for playback at your frame rate to see, and a point-of-view
demo's recorder, whose movement is read differently while seeking. - The cost. About 140 KB of memory, no per-frame allocation while playing (docs/measurements/2026-10-25-d233-activity-tuples-cost.md).
- A seek spends about 9 ms (up to 57 ms) recovering 10 players' variants on the f12 demo, an estimated 22 ms for 24 (docs/measurements/2026-10-25-d233-activity-tuples-cost.md).
- Four more things now follow the game, each held by tests and none seen on screen yet (finding 97):
- A zoomed recorder and his gear are no longer drawn in any view, including a monitor (B598). A decal can still
land on him. A player you follow stays visible in a monitor. Not checked: a point-of-view demo where the
recorder scopes, seen in third person. - Detail props (grass) reach out to the plain
cl_detaildist, and the zoom's field-of-view factor now reaches the
detail fade (B580). Not re-run on f12. - A camera monitor lights the followed player through the light cache, as the game does (B594). No screenshot taken.
- A sequence of zero or negative weight is treated as the engine treats it when choosing (B584).
- A zoomed recorder and his gear are no longer drawn in any view, including a monitor (B598). A decal can still
- A running point-of-view recorder is no longer predicted to stand still so often (B604). Collision with walls and
slopes now uses Source's published clip, so a player resting against a wall is not stopped dead by it.
Gaps where prediction says still while the server says over 100 u/s: 7,723 to 2,369 on etf2l-12025 (docs/measurements/2026-10-26-pov-recorder-seek-scan.md).
The same onrgl-pug-2026-08-10-pov, a match whose map matches: 3,194 to 1,000 of 94,321 gaps (docs/measurements/2026-10-26-pov-recorder-seek-scan.md).
The footstep ground probe also treats a player clip as ground (tests only). The remainder is open (see Known gaps). - Rewinding a point-of-view demo re-reads far less. On etf2l-12025, mean 6.978 ms to 0.026 ms (docs/measurements/2026-10-26-pov-recorder-seek-scan.md).
Commands re-read a rewind: 57,714.9 to 40.9 (docs/measurements/2026-10-26-pov-recorder-seek-scan.md).
This holds only for a file the viewer can show has a simple shape (D234). Any other file is re-read from its start, up to 44.778 ms on that demo (docs/measurements/2026-10-26-pov-recorder-seek-scan.md).
Of 75 demos swept, all 75 were simple; the othe...
Tf2DemoSalvage 0.1.0-beta.53
Tf2DemoSalvage 0.1.0-beta.53
Changes in 0.1.0-beta.53
Gate for this release (every test executed, 0 failed): core 2680, cli 75, logging 37, fonts 5, animation 5346, scene 3346,
audio 383, presentation 1306, content 1859, corpus 235, rendering 1252, viewer 157, UI suite 84 of 84 with 0 skipped; CI on
the release commit green with 0 annotations. Figures are on the cp_process_f12 recording, one machine, first person unless it says
otherwise; the file beside each figure holds it.
- The sun costs almost nothing now (B594). A sky trace used to test every piece of terrain along its whole 57,016-unit
length, though the walls had already stopped it. It now stops where the walls stop it, with the same answers.
Sun: 0.457-0.465 ms to 0.012-0.013 ms a frame, with the same 1.52-1.55 calls a frame (docs/measurements/2026-10-26-threaded-particles-and-sun.md).
Frame: 9.37-9.55 ms to 8.74-8.94 ms, -0.6 ms, -6.5% (docs/measurements/2026-10-26-threaded-particles-and-sun.md).
The sun is 0.45 ms of that; the rest is estimated, not timed, to be other traces that take the same limit (docs/measurements/2026-10-26-threaded-particles-and-sun.md).
Empty view: frame 3.60-3.63 ms to 3.53-3.55 ms, -0.06 ms (docs/measurements/2026-10-26-threaded-particles-and-sun.md).
On the real map, 0 of 50,000 traces differ from the old answer (docs/measurements/2026-10-26-threaded-particles-and-sun.md). - Particle effects are stepped on several threads, as TF2 does (
r_threaded_particles, default 1). The picture is the
same. The step splits across your physical cores minus one, and effects with 4 steps or fewer stay on one thread.
Particle step (bursts/step): 0.275-0.282 ms to 0.196-0.204 ms a frame, -0.07 to -0.09 ms (docs/measurements/2026-10-26-threaded-particles-and-sun.md).
The whole-frame gain is not established: frame 8.93-9.03 ms to 8.74-8.94 ms, and the two ranges touch (docs/measurements/2026-10-26-threaded-particles-and-sun.md).
To go back to one thread, putr_threaded_particles 0insettings.cfg, or use+r_threaded_particles 0for one run
(tf2demoview --help). Thresholds of 0 and 16 gave the same step as 4, so 4 is kept without a measured advantage (docs/measurements/2026-10-26-threaded-particles-and-sun.md). - A face uploads only the part of its stream that can move (B597). Each model sends its first-to-last animated
slots instead of the whole stream. The picture is the same (tests compare every pixel).
Upload: 3.155-3.159 MB to 1.261-1.265 MB a frame (docs/measurements/2026-10-26-b597-flex-range.md).
Flex span: 0.296-0.305 ms to 0.149-0.156 ms a frame (docs/measurements/2026-10-26-b597-flex-range.md).
Frame time did not separate: 10.04-10.17 ms before, 9.99-10.31 ms after (docs/measurements/2026-10-26-b597-flex-range.md).
A cache that skipped a face whose animation had not changed was tried and removed: under 1% of faces held still (docs/measurements/2026-10-26-b597-face-cache.md). - Occlusion tests are uploaded once a frame. Occlusion pass: 0.372-0.377 ms to 0.174-0.192 ms, -0.18 to -0.20 ms (docs/measurements/2026-10-26-b597-constant-ring-and-occlusion.md).
That file attributes no frame-time change to it. A second change tried beside it, constant rings, measured slightly
slower and was removed (D235). - Less garbage per frame. Three allocation sites removed. Gummo first person: 1.15-1.19 MB to 0.84-0.90 MB a frame,
-0.30 MB, -26%; GC pause 0.20-0.23 ms to 0.15-0.18 ms a frame (docs/measurements/2026-10-25-b599-three-allocation-sites.md).
Frame time did not separate (docs/measurements/2026-10-25-b599-three-allocation-sites.md). - Weapon scripts and face-sound lengths are read before play, not during it. Main-thread file reads in play: 36 to 0
in each of five runs; loading takes 51.5-65.9 ms for the face lengths (docs/measurements/2026-10-25-b595-part6-main-thread-reads-in-play.md).
The mean frame time did not move beyond run-to-run spread, and the weapon scripts' load cost was not measured (docs/measurements/2026-10-25-b595-part6-main-thread-reads-in-play.md). - Player animation variants are picked the way TF2 picks them. Where a class model has several sequences for one
activity, the viewer now chooses at random by their weights when the player enters the activity and holds that choice
until the activity changes, instead of always showing the heaviest. Gestures (flinches, reloads, voice commands) pick
their variant once when they start, the same way. Picks are reproducible: seeking to a moment shows the variant that
playing through to it showed, with three exceptions — a player who had been in that activity for more than 2048 ticks
(31 seconds on a 66-tick server), a change too brief for playback at your frame rate to see, and a point-of-view
demo's recorder, whose movement is read differently while seeking. - The cost. About 140 KB of memory, no per-frame allocation while playing (docs/measurements/2026-10-25-d233-activity-tuples-cost.md).
- A seek spends about 9 ms (up to 57 ms) recovering 10 players' variants on the f12 demo, an estimated 22 ms for 24 (docs/measurements/2026-10-25-d233-activity-tuples-cost.md).
- Four more things now follow the game, each held by tests and none seen on screen yet (finding 97):
- A zoomed recorder and his gear are no longer drawn in any view, including a monitor (B598). A decal can still
land on him. A player you follow stays visible in a monitor. Not checked: a point-of-view demo where the
recorder scopes, seen in third person. - Detail props (grass) reach out to the plain
cl_detaildist, and the zoom's field-of-view factor now reaches the
detail fade (B580). Not re-run on f12. - A camera monitor lights the followed player through the light cache, as the game does (B594). No screenshot taken.
- A sequence of zero or negative weight is treated as the engine treats it when choosing (B584).
- A zoomed recorder and his gear are no longer drawn in any view, including a monitor (B598). A decal can still
- A running point-of-view recorder is no longer predicted to stand still so often (B604). Collision with walls and
slopes now uses Source's published clip, so a player resting against a wall is not stopped dead by it.
Gaps where prediction says still while the server says over 100 u/s: 7,723 to 2,369 on etf2l-12025 (docs/measurements/2026-10-26-pov-recorder-seek-scan.md).
The same onrgl-pug-2026-08-10-pov, a match whose map matches: 3,194 to 1,000 of 94,321 gaps (docs/measurements/2026-10-26-pov-recorder-seek-scan.md).
The footstep ground probe also treats a player clip as ground (tests only). The remainder is open (see Known gaps). - Rewinding a point-of-view demo re-reads far less. On etf2l-12025, mean 6.978 ms to 0.026 ms (docs/measurements/2026-10-26-pov-recorder-seek-scan.md).
Commands re-read a rewind: 57,714.9 to 40.9 (docs/measurements/2026-10-26-pov-recorder-seek-scan.md).
This holds only for a file the viewer can show has a simple shape (D234). Any other file is re-read from its start, up to 44.778 ms on that demo (docs/measurements/2026-10-26-pov-recorder-seek-scan.md).
Of 75 demos swept, all 75 were simple; the other 708 of 783 had not been swept when this was written (docs/measurements/2026-10-26-pov-recorder-seek-scan.md).
The 0.026 ms run did not check that the desktop was quiet (docs/measurements/2026-10-26-pov-recorder-seek-scan.md).
Changes in 0.1.0-beta.52
Released 2026-10-25.
- The HUD's texture sheets are now loaded when the HUD starts, as in TF2, not at the first kill drawn with each one.
Before, the main thread read each sheet from disk in the middle of play. In one cold-disk run that read cost 361.1 ms and 223.9 ms inside two stalls (docs/measurements/2026-10-25-b595-sync-io-in-hud-and-pose.md).
Warm disk,cp_process_f12: main-thread file reads in play went from 45 to 33 over 20 s, none of them a HUD sheet (docs/measurements/2026-10-25-b595-sync-io-in-hud-and-pose.md).
Not shown yet: a cold-disk run after the change. The reads moved out of play; a stall disappearing was not observed, so treat the cold-disk benefit as expected, not measured. - The first-person view builds its draw list faster. A set of props was compared member by member; it now compares by reference. What is drawn does not change.
cp_process_f12, first person,drawlistper frame: 0.56 and 0.46 ms before, 0.22 and 0.21 ms after (docs/measurements/2026-10-24-drawlist-parts.md).
The filter part: 0.30-0.35 ms before, 0.06 ms after (docs/measurements/2026-10-24-drawlist-parts.md). This was in beta.51 and was left out of its notes.
Changes in 0.1.0-beta.51
Released 2026-10-25.
- The viewer's log file is now written in the background (D230). Writing it used to pause playback for up to about a
tenth of a second now and then (docs/measurements/2026-10-24-d230-async-file-log.md). A log on disk lags by a fraction of a second while the viewer runs, and it is complete
after a normal close or a crash the viewer can report. Ending a frozen viewer with Task Manager still leaves its log current up to the freeze
(see Known gaps).
Main-thread time spent writing the log, per 60 s run oncp_process_f12: 5.6-691.1 ms before, 0.0 ms after (docs/measurements/2026-10-24-d230-async-file-log.md).
This removes the log's share of the hitches, not all of them. Frames over 100 ms across five 60 s runs: 7 before, 4 after, all four in one run, ashudandposestalls that show no log time (docs/measurements/2026-10-24-d230-async-file-log.md).
Their cause is not yet explained; one candidate,hudtexture reads, is addressed in beta.52 but not shown to be the cause. - Two repeated log lines are quieter. The viewmodel skin line is written once per skin change, and the muzzle-flash
line is written only at Debug level. Each used to be flushed to disk from the frame (docs/measurements/2026-10-24-b595-first-use-builds.md,
`docs/measurements/2026-10-24-muzzle-flash-log-lin...
Tf2DemoSalvage 0.1.0-beta.52
Tf2DemoSalvage 0.1.0-beta.52
Unreleased
Changes in 0.1.0-beta.52
Released 2026-10-25.
- The HUD's texture sheets are now loaded when the HUD starts, as in TF2, not at the first kill drawn with each one.
Before, the main thread read each sheet from disk in the middle of play. In one cold-disk run that read cost 361.1 ms and 223.9 ms inside two stalls (docs/measurements/2026-10-25-b595-sync-io-in-hud-and-pose.md).
Warm disk,cp_process_f12: main-thread file reads in play went from 45 to 33 over 20 s, none of them a HUD sheet (docs/measurements/2026-10-25-b595-sync-io-in-hud-and-pose.md).
Not shown yet: a cold-disk run after the change. The reads moved out of play; a stall disappearing was not observed, so treat the cold-disk benefit as expected, not measured. - The first-person view builds its draw list faster. A set of props was compared member by member; it now compares by reference. What is drawn does not change.
cp_process_f12, first person,drawlistper frame: 0.56 and 0.46 ms before, 0.22 and 0.21 ms after (docs/measurements/2026-10-24-drawlist-parts.md).
The filter part: 0.30-0.35 ms before, 0.06 ms after (docs/measurements/2026-10-24-drawlist-parts.md). This was in beta.51 and was left out of its notes.
Changes in 0.1.0-beta.51
Released 2026-10-25.
- The viewer's log file is now written in the background (D230). Writing it used to pause playback for up to about a
tenth of a second now and then (docs/measurements/2026-10-24-d230-async-file-log.md). A log on disk lags by a fraction of a second while the viewer runs, and it is complete
after a normal close or a crash the viewer can report. Ending a frozen viewer with Task Manager still leaves its log current up to the freeze
(see Known gaps).
Main-thread time spent writing the log, per 60 s run oncp_process_f12: 5.6-691.1 ms before, 0.0 ms after (docs/measurements/2026-10-24-d230-async-file-log.md).
This removes the log's share of the hitches, not all of them. Frames over 100 ms across five 60 s runs: 7 before, 4 after, all four in one run, ashudandposestalls that show no log time (docs/measurements/2026-10-24-d230-async-file-log.md).
Their cause is not yet explained; one candidate,hudtexture reads, is addressed in beta.52 but not shown to be the cause. - Two repeated log lines are quieter. The viewmodel skin line is written once per skin change, and the muzzle-flash
line is written only at Debug level. Each used to be flushed to disk from the frame (docs/measurements/2026-10-24-b595-first-use-builds.md,
docs/measurements/2026-10-24-muzzle-flash-log-line.md). What is drawn does not change.
Changes in 0.1.0-beta.50
Released 2026-10-24.
- Detail props (grass and small sprites) cost almost nothing to place. Like TF2, the viewer now reaches only the
detail props in the leaves near the camera instead of every prop in the map. The drawn detail is unchanged.
Oncp_process_f12, first person: detail step 0.55 ms to 0.002 ms a frame (measurements/2026-10-23-b580-detail-reach.md).
Whole frame about 13.1 ms to about 12.3 ms (measurements/2026-10-23-b580-detail-reach.md). - The HUD event feed no longer walks the whole match every frame. What the HUD shows is unchanged.
Empty view: its stage 0.13-0.20 ms to 0-0.02 ms a frame (measurements/2026-10-23-hud-event-feed-window.md). - The viewer now draws on a second thread by default, as TF2 does.
mat_queue_modedefaults to TF2's own-1:
on a machine with two or more physical CPU cores each frame is drawn on a second thread, and on one core it is drawn
on the main thread as before. On the author'scp_process_f12recording this was measured at about 14% faster.
Frame time: 13.04 ms to 11.25 ms (finding 98, measurements/2026-10-23-queued-render-thread-rung-2.md).
To get the old behaviour back, setmat_queue_mode 0insettings.cfg, or use+mat_queue_mode 0for one run.
Asettings.cfgsaved by an earlier version keeps following the default, because the viewer writes defaults as
comments.
Changes in 0.1.0-beta.49
Released 2026-10-23.
- New setting:
mat_queue_mode. Set it to 2 to draw each frame on a second thread, as TF2 does with its own setting
of that name. It is off by default, so the viewer behaves as before. It was measured on the author'scp_process_f12
recording, in first person at 1280x720, over repeated alternating runs on a quiet machine.
Frame time: 13.04 ms to 11.25 ms, 76.7 to 88.9 fps, about 14% faster (finding 98, measurements/2026-10-23-queued-render-thread-rung-2.md).
TF2 turns this on by itself when the machine has two or more physical CPU cores; the viewer does not yet, and a later
release may make it the default after more testing. - Screenshots,
--shotand seeking wait for the second thread to finish its frame before they read anything. - Mirrors and monitors show their own camera's view. On maps such as Boardwalk they used to draw the main view's
geometry, so the room behind the player showed as black (finding 98). - Alt+Enter no longer starts DXGI's own full screen switch, which could hang the viewer in the threaded mode. Full
screen still works through the viewer's own control. - Developer note.
--measureprints two new parts,camera waitandqueue render thread.
Changes in 0.1.0-beta.48
Released 2026-10-22.
- A sound no longer freezes a frame the first time it plays. A sound that was not loaded yet used to be read from
the game files and decoded in the middle of a frame. It now loads in the background and starts as soon as it is
ready, which is how TF2 treats a sound whose data has not arrived. On the author'scp_process_f12recording, over
60 seconds of playback, sounds read or decoded on the render thread went from 5 to 0 (finding 90).
The one hitch the game's own log flagged, a 171 ms stall on a voice line, is gone (finding 90). The trade: such a
sound starts 1 to 28 simulation ticks late the first time it is heard (finding 90), and a stop that arrives while it
is still loading now cancels it. A stop for a sound that was never loaded no longer loads it. - Less memory churn per frame. Seven more per-frame allocations are gone (corpse physics, corpse animation
copies, layer lists, placement matrices, the healing beam and the HUD's vertex array). On the same recording the
render thread went from 1.21-1.26 MB to 0.94-1.01 MB allocated per frame (finding 91), and all threads together
from 2.50-2.98 MB to 1.54-1.83 MB per frame (finding 91), with fewer garbage-collection passes in 60 seconds
(1,631-1,903 to 945-1,124, finding 91). Frame times did not change measurably, so this release makes no frame-rate
claim for it. - Developer notes. Findings 90 and 91 and risks B568, B570 and B574 in the repository's
docsfolder record how
these were measured.
Changes in 0.1.0-beta.47
Released 2026-10-21.
- The viewer draws faster. In first person on the author's
cp_process_f12recording, a frame went from about
16.5-17.1 ms to about 14.5-15.3 ms, and with nothing in view from about 7.3 ms to about 5.6 ms. The largest single
part was a one-millisecond wait after every frame that served only accessibility and test tools. It now runs only
when such a tool is attached to the window. - Less memory churn per frame. Memory allocated per frame fell from about 7.7 MB to about 5.1 MB over all threads
(the render thread from about 4.0 MB to 2.2 MB), because skeleton poses and inverse-kinematics lists are reused.
This also shortens the collector's pauses. - Fewer playback hitches from sounds. Weapon draw and reload sounds are loaded while the demo loads instead of
the first time they play. On that recording, misses during play fell from 12 a minute to 4. - A saved screenshot appears only when the whole image is on disk, so a tool or a file watcher can no longer pick
up a half-written PNG. - A particle effect whose owner is out of view for a while and then returns is no longer lost. Before, it stayed
gone until you scrubbed. - Developer options.
--measurealso prints the memory allocated per frame by the render thread, and lists
stalls beside whether the whole process paused.
Changes in 0.1.0-beta.46
Released 2026-10-21.
- Large demos open faster. Building the timeline for a 58 MB match (the
cp_process_f12recording) went from about
170 seconds to about 100 seconds on the author's machine, with the same memory use. - Fewer hitches during playback. Surface sounds (footsteps, impacts, scrapes) are decoded while the demo loads
instead of the first time they play. Rocket-jump smoke effects that had already finished are no longer rebuilt all at
once after the camera moves. - Particle effects follow the game's rules for what is drawn and what sleeps. An effect that is out of view is not
drawn and goes to sleep after a few seconds, as in TF2, and one frame of particle simulation is limited to 0.1
seconds, as in the game. On the author's f12 test the picture is unchanged: the comparison images differ only by the
frame-rate counter and nine pixels that are off by two shades out of 255. - Developer option.
--measureprints new lines for JIT compilation and for particle work per frame.
Changes in 0.1.0-beta.45
Released 2026-10-20. Betas 0.1.0-beta.16 to 0.1.0-beta.44 were never published, so this is the first public build since beta.15. The
sections below cover every change since, one per version.
--shotwrites a companion file beside the picture.--shot out.pngalso writesout.png.json: the demo's
file name and its sha256, the map, the tick, the camera, the view size, the settings that differ from their
defaults, the build and the command line (file names only). You can rebuild a capture from it later (D226).
Deleting a screenshot from the viewer's tidy-up deletes its com...
Tf2DemoSalvage 0.1.0-beta.51
Tf2DemoSalvage 0.1.0-beta.51
Unreleased
Changes in 0.1.0-beta.51
Released 2026-10-25.
- The viewer's log file is now written in the background (D230). Writing it used to pause playback for up to about a
tenth of a second now and then (docs/measurements/2026-10-24-d230-async-file-log.md). A log on disk lags by a fraction of a second while the viewer runs, and it is complete
after a normal close or a crash the viewer can report. Ending a frozen viewer with Task Manager still leaves its log current up to the freeze
(see Known gaps).
Main-thread time spent writing the log, per 60 s run oncp_process_f12: 5.6-691.1 ms before, 0.0 ms after (docs/measurements/2026-10-24-d230-async-file-log.md).
This removes the log's share of the hitches, not all of them. Frames over 100 ms across five 60 s runs: 7 before, 4 after, all four in one run, ashudandposestalls that show no log time (docs/measurements/2026-10-24-d230-async-file-log.md).
Their cause is not yet explained; one candidate,hudtexture reads, is addressed in beta.52 but not shown to be the cause. - Two repeated log lines are quieter. The viewmodel skin line is written once per skin change, and the muzzle-flash
line is written only at Debug level. Each used to be flushed to disk from the frame (docs/measurements/2026-10-24-b595-first-use-builds.md,
docs/measurements/2026-10-24-muzzle-flash-log-line.md). What is drawn does not change.
Changes in 0.1.0-beta.50
Released 2026-10-24.
- Detail props (grass and small sprites) cost almost nothing to place. Like TF2, the viewer now reaches only the
detail props in the leaves near the camera instead of every prop in the map. The drawn detail is unchanged.
Oncp_process_f12, first person: detail step 0.55 ms to 0.002 ms a frame (measurements/2026-10-23-b580-detail-reach.md).
Whole frame about 13.1 ms to about 12.3 ms (measurements/2026-10-23-b580-detail-reach.md). - The HUD event feed no longer walks the whole match every frame. What the HUD shows is unchanged.
Empty view: its stage 0.13-0.20 ms to 0-0.02 ms a frame (measurements/2026-10-23-hud-event-feed-window.md). - The viewer now draws on a second thread by default, as TF2 does.
mat_queue_modedefaults to TF2's own-1:
on a machine with two or more physical CPU cores each frame is drawn on a second thread, and on one core it is drawn
on the main thread as before. On the author'scp_process_f12recording this was measured at about 14% faster.
Frame time: 13.04 ms to 11.25 ms (finding 98, measurements/2026-10-23-queued-render-thread-rung-2.md).
To get the old behaviour back, setmat_queue_mode 0insettings.cfg, or use+mat_queue_mode 0for one run.
Asettings.cfgsaved by an earlier version keeps following the default, because the viewer writes defaults as
comments.
Changes in 0.1.0-beta.49
Released 2026-10-23.
- New setting:
mat_queue_mode. Set it to 2 to draw each frame on a second thread, as TF2 does with its own setting
of that name. It is off by default, so the viewer behaves as before. It was measured on the author'scp_process_f12
recording, in first person at 1280x720, over repeated alternating runs on a quiet machine.
Frame time: 13.04 ms to 11.25 ms, 76.7 to 88.9 fps, about 14% faster (finding 98, measurements/2026-10-23-queued-render-thread-rung-2.md).
TF2 turns this on by itself when the machine has two or more physical CPU cores; the viewer does not yet, and a later
release may make it the default after more testing. - Screenshots,
--shotand seeking wait for the second thread to finish its frame before they read anything. - Mirrors and monitors show their own camera's view. On maps such as Boardwalk they used to draw the main view's
geometry, so the room behind the player showed as black (finding 98). - Alt+Enter no longer starts DXGI's own full screen switch, which could hang the viewer in the threaded mode. Full
screen still works through the viewer's own control. - Developer note.
--measureprints two new parts,camera waitandqueue render thread.
Changes in 0.1.0-beta.48
Released 2026-10-22.
- A sound no longer freezes a frame the first time it plays. A sound that was not loaded yet used to be read from
the game files and decoded in the middle of a frame. It now loads in the background and starts as soon as it is
ready, which is how TF2 treats a sound whose data has not arrived. On the author'scp_process_f12recording, over
60 seconds of playback, sounds read or decoded on the render thread went from 5 to 0 (finding 90).
The one hitch the game's own log flagged, a 171 ms stall on a voice line, is gone (finding 90). The trade: such a
sound starts 1 to 28 simulation ticks late the first time it is heard (finding 90), and a stop that arrives while it
is still loading now cancels it. A stop for a sound that was never loaded no longer loads it. - Less memory churn per frame. Seven more per-frame allocations are gone (corpse physics, corpse animation
copies, layer lists, placement matrices, the healing beam and the HUD's vertex array). On the same recording the
render thread went from 1.21-1.26 MB to 0.94-1.01 MB allocated per frame (finding 91), and all threads together
from 2.50-2.98 MB to 1.54-1.83 MB per frame (finding 91), with fewer garbage-collection passes in 60 seconds
(1,631-1,903 to 945-1,124, finding 91). Frame times did not change measurably, so this release makes no frame-rate
claim for it. - Developer notes. Findings 90 and 91 and risks B568, B570 and B574 in the repository's
docsfolder record how
these were measured.
Changes in 0.1.0-beta.47
Released 2026-10-21.
- The viewer draws faster. In first person on the author's
cp_process_f12recording, a frame went from about
16.5-17.1 ms to about 14.5-15.3 ms, and with nothing in view from about 7.3 ms to about 5.6 ms. The largest single
part was a one-millisecond wait after every frame that served only accessibility and test tools. It now runs only
when such a tool is attached to the window. - Less memory churn per frame. Memory allocated per frame fell from about 7.7 MB to about 5.1 MB over all threads
(the render thread from about 4.0 MB to 2.2 MB), because skeleton poses and inverse-kinematics lists are reused.
This also shortens the collector's pauses. - Fewer playback hitches from sounds. Weapon draw and reload sounds are loaded while the demo loads instead of
the first time they play. On that recording, misses during play fell from 12 a minute to 4. - A saved screenshot appears only when the whole image is on disk, so a tool or a file watcher can no longer pick
up a half-written PNG. - A particle effect whose owner is out of view for a while and then returns is no longer lost. Before, it stayed
gone until you scrubbed. - Developer options.
--measurealso prints the memory allocated per frame by the render thread, and lists
stalls beside whether the whole process paused.
Changes in 0.1.0-beta.46
Released 2026-10-21.
- Large demos open faster. Building the timeline for a 58 MB match (the
cp_process_f12recording) went from about
170 seconds to about 100 seconds on the author's machine, with the same memory use. - Fewer hitches during playback. Surface sounds (footsteps, impacts, scrapes) are decoded while the demo loads
instead of the first time they play. Rocket-jump smoke effects that had already finished are no longer rebuilt all at
once after the camera moves. - Particle effects follow the game's rules for what is drawn and what sleeps. An effect that is out of view is not
drawn and goes to sleep after a few seconds, as in TF2, and one frame of particle simulation is limited to 0.1
seconds, as in the game. On the author's f12 test the picture is unchanged: the comparison images differ only by the
frame-rate counter and nine pixels that are off by two shades out of 255. - Developer option.
--measureprints new lines for JIT compilation and for particle work per frame.
Changes in 0.1.0-beta.45
Released 2026-10-20. Betas 0.1.0-beta.16 to 0.1.0-beta.44 were never published, so this is the first public build since beta.15. The
sections below cover every change since, one per version.
--shotwrites a companion file beside the picture.--shot out.pngalso writesout.png.json: the demo's
file name and its sha256, the map, the tick, the camera, the view size, the settings that differ from their
defaults, the build and the command line (file names only). You can rebuild a capture from it later (D226).
Deleting a screenshot from the viewer's tidy-up deletes its companion too. The F12 key writes one only in a
Debug build.--like <shot.png.json>reproduces a TF2 capture. Give it the companion file of a TF2-side capture and the
viewer takes its tick, camera, view size and console variables, and its demo too, from the file's folder, when
you name none. A--tickor+cvaryou pass still wins; the camera and view-size environment variables do not.
If the demo you open is not the one the file names (different sha256), the viewer warns and still shoots. A
file with a bad camera, or one whose demo is a path rather than a file name, is refused with the reason.- Static props are no longer walked every frame. A map's static props (1,353 on
cp_process_f12) are filed
once and only the ones in view are posed; the picture is identical pixel for pixel in the two golden pairs
taken (finding 83, item 1). Measured on one machine, on f12 at tick 30000 in a Debug viewer, looking at nothing
(TF2VIEW_CAMERA="1613 -2821 154 89 0"): the simulate phase fell from 1.2-1.4 ms to 0.5-0.7 ms a frame and
the draw-list phase from 0.4-0.6 ms to 0.1-0.2 ms. The whole frame ranged 7.7-9.0 ms before and 6.0-9.4 ms
after, with other work on the machine, so quote the phas...
Tf2DemoSalvage 0.1.0-beta.50
Tf2DemoSalvage 0.1.0-beta.50
Unreleased
Changes in 0.1.0-beta.50
Released 2026-10-24.
- Detail props (grass and small sprites) cost almost nothing to place. Like TF2, the viewer now reaches only the
detail props in the leaves near the camera instead of every prop in the map. The drawn detail is unchanged.
Oncp_process_f12, first person: detail step 0.55 ms to 0.002 ms a frame (measurements/2026-10-23-b580-detail-reach.md).
Whole frame about 13.1 ms to about 12.3 ms (measurements/2026-10-23-b580-detail-reach.md). - The HUD event feed no longer walks the whole match every frame. What the HUD shows is unchanged.
Empty view: its stage 0.13-0.20 ms to 0-0.02 ms a frame (measurements/2026-10-23-hud-event-feed-window.md). - The viewer now draws on a second thread by default, as TF2 does.
mat_queue_modedefaults to TF2's own-1:
on a machine with two or more physical CPU cores each frame is drawn on a second thread, and on one core it is drawn
on the main thread as before. On the author'scp_process_f12recording this was measured at about 14% faster.
Frame time: 13.04 ms to 11.25 ms (finding 98, measurements/2026-10-23-queued-render-thread-rung-2.md).
To get the old behaviour back, setmat_queue_mode 0insettings.cfg, or use+mat_queue_mode 0for one run.
Asettings.cfgsaved by an earlier version keeps following the default, because the viewer writes defaults as
comments.
Changes in 0.1.0-beta.49
Released 2026-10-23.
- New setting:
mat_queue_mode. Set it to 2 to draw each frame on a second thread, as TF2 does with its own setting
of that name. It is off by default, so the viewer behaves as before. It was measured on the author'scp_process_f12
recording, in first person at 1280x720, over repeated alternating runs on a quiet machine.
Frame time: 13.04 ms to 11.25 ms, 76.7 to 88.9 fps, about 14% faster (finding 98, measurements/2026-10-23-queued-render-thread-rung-2.md).
TF2 turns this on by itself when the machine has two or more physical CPU cores; the viewer does not yet, and a later
release may make it the default after more testing. - Screenshots,
--shotand seeking wait for the second thread to finish its frame before they read anything. - Mirrors and monitors show their own camera's view. On maps such as Boardwalk they used to draw the main view's
geometry, so the room behind the player showed as black (finding 98). - Alt+Enter no longer starts DXGI's own full screen switch, which could hang the viewer in the threaded mode. Full
screen still works through the viewer's own control. - Developer note.
--measureprints two new parts,camera waitandqueue render thread.
Changes in 0.1.0-beta.48
Released 2026-10-22.
- A sound no longer freezes a frame the first time it plays. A sound that was not loaded yet used to be read from
the game files and decoded in the middle of a frame. It now loads in the background and starts as soon as it is
ready, which is how TF2 treats a sound whose data has not arrived. On the author'scp_process_f12recording, over
60 seconds of playback, sounds read or decoded on the render thread went from 5 to 0 (finding 90).
The one hitch the game's own log flagged, a 171 ms stall on a voice line, is gone (finding 90). The trade: such a
sound starts 1 to 28 simulation ticks late the first time it is heard (finding 90), and a stop that arrives while it
is still loading now cancels it. A stop for a sound that was never loaded no longer loads it. - Less memory churn per frame. Seven more per-frame allocations are gone (corpse physics, corpse animation
copies, layer lists, placement matrices, the healing beam and the HUD's vertex array). On the same recording the
render thread went from 1.21-1.26 MB to 0.94-1.01 MB allocated per frame (finding 91), and all threads together
from 2.50-2.98 MB to 1.54-1.83 MB per frame (finding 91), with fewer garbage-collection passes in 60 seconds
(1,631-1,903 to 945-1,124, finding 91). Frame times did not change measurably, so this release makes no frame-rate
claim for it. - Developer notes. Findings 90 and 91 and risks B568, B570 and B574 in the repository's
docsfolder record how
these were measured.
Changes in 0.1.0-beta.47
Released 2026-10-21.
- The viewer draws faster. In first person on the author's
cp_process_f12recording, a frame went from about
16.5-17.1 ms to about 14.5-15.3 ms, and with nothing in view from about 7.3 ms to about 5.6 ms. The largest single
part was a one-millisecond wait after every frame that served only accessibility and test tools. It now runs only
when such a tool is attached to the window. - Less memory churn per frame. Memory allocated per frame fell from about 7.7 MB to about 5.1 MB over all threads
(the render thread from about 4.0 MB to 2.2 MB), because skeleton poses and inverse-kinematics lists are reused.
This also shortens the collector's pauses. - Fewer playback hitches from sounds. Weapon draw and reload sounds are loaded while the demo loads instead of
the first time they play. On that recording, misses during play fell from 12 a minute to 4. - A saved screenshot appears only when the whole image is on disk, so a tool or a file watcher can no longer pick
up a half-written PNG. - A particle effect whose owner is out of view for a while and then returns is no longer lost. Before, it stayed
gone until you scrubbed. - Developer options.
--measurealso prints the memory allocated per frame by the render thread, and lists
stalls beside whether the whole process paused.
Changes in 0.1.0-beta.46
Released 2026-10-21.
- Large demos open faster. Building the timeline for a 58 MB match (the
cp_process_f12recording) went from about
170 seconds to about 100 seconds on the author's machine, with the same memory use. - Fewer hitches during playback. Surface sounds (footsteps, impacts, scrapes) are decoded while the demo loads
instead of the first time they play. Rocket-jump smoke effects that had already finished are no longer rebuilt all at
once after the camera moves. - Particle effects follow the game's rules for what is drawn and what sleeps. An effect that is out of view is not
drawn and goes to sleep after a few seconds, as in TF2, and one frame of particle simulation is limited to 0.1
seconds, as in the game. On the author's f12 test the picture is unchanged: the comparison images differ only by the
frame-rate counter and nine pixels that are off by two shades out of 255. - Developer option.
--measureprints new lines for JIT compilation and for particle work per frame.
Changes in 0.1.0-beta.45
Released 2026-10-20. Betas 0.1.0-beta.16 to 0.1.0-beta.44 were never published, so this is the first public build since beta.15. The
sections below cover every change since, one per version.
-
--shotwrites a companion file beside the picture.--shot out.pngalso writesout.png.json: the demo's
file name and its sha256, the map, the tick, the camera, the view size, the settings that differ from their
defaults, the build and the command line (file names only). You can rebuild a capture from it later (D226).
Deleting a screenshot from the viewer's tidy-up deletes its companion too. The F12 key writes one only in a
Debug build. -
--like <shot.png.json>reproduces a TF2 capture. Give it the companion file of a TF2-side capture and the
viewer takes its tick, camera, view size and console variables, and its demo too, from the file's folder, when
you name none. A--tickor+cvaryou pass still wins; the camera and view-size environment variables do not.
If the demo you open is not the one the file names (different sha256), the viewer warns and still shoots. A
file with a bad camera, or one whose demo is a path rather than a file name, is refused with the reason. -
Static props are no longer walked every frame. A map's static props (1,353 on
cp_process_f12) are filed
once and only the ones in view are posed; the picture is identical pixel for pixel in the two golden pairs
taken (finding 83, item 1). Measured on one machine, on f12 at tick 30000 in a Debug viewer, looking at nothing
(TF2VIEW_CAMERA="1613 -2821 154 89 0"): the simulate phase fell from 1.2-1.4 ms to 0.5-0.7 ms a frame and
the draw-list phase from 0.4-0.6 ms to 0.1-0.2 ms. The whole frame ranged 7.7-9.0 ms before and 6.0-9.4 ms
after, with other work on the machine, so quote the phases, not the frame. A static prop you look at is still
animated when it has an animation, which the game does not do (B560). -
A demo whose map is missing still opens, and the status bar says why (D224). The viewer fetches a map it
lacks, so you should rarely see this. TF2 itself refuses such a demo. Unverified: that the message reaches
the status bar on every path (see Known gaps). -
TF2VIEW_VIEWPORT_SIZE("1280x720", WIDTHxHEIGHT) sizes the 3D view itself; the window grows around it,
so the view is that size at any display scaling. A value that does not parse is logged and ignored. For scripted
captures;--helplists it. -
Demo voice plays. Voice chat recorded in a demo is heard through five voice channels, as TF2 plays it; a sixth
speaker at once is dropped, as in the game.voice_scaleandvoice_enableare read. Not yet ported: the
lowering of other sounds while someone speaks and the speaking indicator (B549, see Known gaps).
Not checked by ear in a played demo. -
snd_musicvolumelowers music, as TF2 does: the sounds with a#in their name and no distance falloff,
including map soundscapes, and a change reaches music already playing (B545). -
Animated props draw their real pose. A door or a resupply locker that animates is now posed by its bones
instead of from one stored copy per animation step. Before, the last step of a one-shot a...
Tf2DemoSalvage 0.1.0-beta.49
Tf2DemoSalvage 0.1.0-beta.49
Changes in 0.1.0-beta.49
Released 2026-10-23.
- New setting:
mat_queue_mode. Set it to 2 to draw each frame on a second thread, as TF2 does with its own setting
of that name. It is off by default, so the viewer behaves as before. It was measured on the author'scp_process_f12
recording, in first person at 1280x720, over repeated alternating runs on a quiet machine.
Frame time: 13.04 ms to 11.25 ms, 76.7 to 88.9 fps, about 14% faster (finding 98, measurements/2026-10-23-queued-render-thread-rung-2.md).
TF2 turns this on by itself when the machine has two or more physical CPU cores; the viewer does not yet, and a later
release may make it the default after more testing. - Screenshots,
--shotand seeking wait for the second thread to finish its frame before they read anything. - Mirrors and monitors show their own camera's view. On maps such as Boardwalk they used to draw the main view's
geometry, so the room behind the player showed as black (finding 98). - Alt+Enter no longer starts DXGI's own full screen switch, which could hang the viewer in the threaded mode. Full
screen still works through the viewer's own control. - Developer note.
--measureprints two new parts,camera waitandqueue render thread.
Changes in 0.1.0-beta.48
Released 2026-10-22.
- A sound no longer freezes a frame the first time it plays. A sound that was not loaded yet used to be read from
the game files and decoded in the middle of a frame. It now loads in the background and starts as soon as it is
ready, which is how TF2 treats a sound whose data has not arrived. On the author'scp_process_f12recording, over
60 seconds of playback, sounds read or decoded on the render thread went from 5 to 0 (finding 90).
The one hitch the game's own log flagged, a 171 ms stall on a voice line, is gone (finding 90). The trade: such a
sound starts 1 to 28 simulation ticks late the first time it is heard (finding 90), and a stop that arrives while it
is still loading now cancels it. A stop for a sound that was never loaded no longer loads it. - Less memory churn per frame. Seven more per-frame allocations are gone (corpse physics, corpse animation
copies, layer lists, placement matrices, the healing beam and the HUD's vertex array). On the same recording the
render thread went from 1.21-1.26 MB to 0.94-1.01 MB allocated per frame (finding 91), and all threads together
from 2.50-2.98 MB to 1.54-1.83 MB per frame (finding 91), with fewer garbage-collection passes in 60 seconds
(1,631-1,903 to 945-1,124, finding 91). Frame times did not change measurably, so this release makes no frame-rate
claim for it. - Developer notes. Findings 90 and 91 and risks B568, B570 and B574 in the repository's
docsfolder record how
these were measured.
Changes in 0.1.0-beta.47
Released 2026-10-21.
- The viewer draws faster. In first person on the author's
cp_process_f12recording, a frame went from about
16.5-17.1 ms to about 14.5-15.3 ms, and with nothing in view from about 7.3 ms to about 5.6 ms. The largest single
part was a one-millisecond wait after every frame that served only accessibility and test tools. It now runs only
when such a tool is attached to the window. - Less memory churn per frame. Memory allocated per frame fell from about 7.7 MB to about 5.1 MB over all threads
(the render thread from about 4.0 MB to 2.2 MB), because skeleton poses and inverse-kinematics lists are reused.
This also shortens the collector's pauses. - Fewer playback hitches from sounds. Weapon draw and reload sounds are loaded while the demo loads instead of
the first time they play. On that recording, misses during play fell from 12 a minute to 4. - A saved screenshot appears only when the whole image is on disk, so a tool or a file watcher can no longer pick
up a half-written PNG. - A particle effect whose owner is out of view for a while and then returns is no longer lost. Before, it stayed
gone until you scrubbed. - Developer options.
--measurealso prints the memory allocated per frame by the render thread, and lists
stalls beside whether the whole process paused.
Changes in 0.1.0-beta.46
Released 2026-10-21.
- Large demos open faster. Building the timeline for a 58 MB match (the
cp_process_f12recording) went from about
170 seconds to about 100 seconds on the author's machine, with the same memory use. - Fewer hitches during playback. Surface sounds (footsteps, impacts, scrapes) are decoded while the demo loads
instead of the first time they play. Rocket-jump smoke effects that had already finished are no longer rebuilt all at
once after the camera moves. - Particle effects follow the game's rules for what is drawn and what sleeps. An effect that is out of view is not
drawn and goes to sleep after a few seconds, as in TF2, and one frame of particle simulation is limited to 0.1
seconds, as in the game. On the author's f12 test the picture is unchanged: the comparison images differ only by the
frame-rate counter and nine pixels that are off by two shades out of 255. - Developer option.
--measureprints new lines for JIT compilation and for particle work per frame.
Changes in 0.1.0-beta.45
Released 2026-10-20. Betas 0.1.0-beta.16 to 0.1.0-beta.44 were never published, so this is the first public build since beta.15. The
sections below cover every change since, one per version.
-
--shotwrites a companion file beside the picture.--shot out.pngalso writesout.png.json: the demo's
file name and its sha256, the map, the tick, the camera, the view size, the settings that differ from their
defaults, the build and the command line (file names only). You can rebuild a capture from it later (D226).
Deleting a screenshot from the viewer's tidy-up deletes its companion too. The F12 key writes one only in a
Debug build. -
--like <shot.png.json>reproduces a TF2 capture. Give it the companion file of a TF2-side capture and the
viewer takes its tick, camera, view size and console variables, and its demo too, from the file's folder, when
you name none. A--tickor+cvaryou pass still wins; the camera and view-size environment variables do not.
If the demo you open is not the one the file names (different sha256), the viewer warns and still shoots. A
file with a bad camera, or one whose demo is a path rather than a file name, is refused with the reason. -
Static props are no longer walked every frame. A map's static props (1,353 on
cp_process_f12) are filed
once and only the ones in view are posed; the picture is identical pixel for pixel in the two golden pairs
taken (finding 83, item 1). Measured on one machine, on f12 at tick 30000 in a Debug viewer, looking at nothing
(TF2VIEW_CAMERA="1613 -2821 154 89 0"): the simulate phase fell from 1.2-1.4 ms to 0.5-0.7 ms a frame and
the draw-list phase from 0.4-0.6 ms to 0.1-0.2 ms. The whole frame ranged 7.7-9.0 ms before and 6.0-9.4 ms
after, with other work on the machine, so quote the phases, not the frame. A static prop you look at is still
animated when it has an animation, which the game does not do (B560). -
A demo whose map is missing still opens, and the status bar says why (D224). The viewer fetches a map it
lacks, so you should rarely see this. TF2 itself refuses such a demo. Unverified: that the message reaches
the status bar on every path (see Known gaps). -
TF2VIEW_VIEWPORT_SIZE("1280x720", WIDTHxHEIGHT) sizes the 3D view itself; the window grows around it,
so the view is that size at any display scaling. A value that does not parse is logged and ignored. For scripted
captures;--helplists it. -
Demo voice plays. Voice chat recorded in a demo is heard through five voice channels, as TF2 plays it; a sixth
speaker at once is dropped, as in the game.voice_scaleandvoice_enableare read. Not yet ported: the
lowering of other sounds while someone speaks and the speaking indicator (B549, see Known gaps).
Not checked by ear in a played demo. -
snd_musicvolumelowers music, as TF2 does: the sounds with a#in their name and no distance falloff,
including map soundscapes, and a change reaches music already playing (B545). -
Animated props draw their real pose. A door or a resupply locker that animates is now posed by its bones
instead of from one stored copy per animation step. Before, the last step of a one-shot animation blended toward
the closed pose, so on f12 a door at tick 6520 drew shut where TF2 draws it open (finding 80). The change is
not measurable in frame time: eight 20-second runs on f12 at tick 30000 averaged 40.1 ms (before) and 40.3 ms
(after). A few props still differ slightly; see Known gaps. -
A building or prop shows its state at the moment you are watching, not the demo's last one. Its model, item,
class, team, parent, owner and attachments are read for the current tick. On f12 a sentry (entity 457) drew its
end-of-demo level 3 at tick 66600, where it was level 1 (B548). -
Parts that move with a door are placed and lit where the door puts them. Every child of a
func_dooron
badwater, upward and vigil was lit from its own offset, and a grandchild was drawn only one level deep (B551). -
Effects end where TF2 ends them. An effect whose owner went dormant now stops emitting and fades, and one
nothing is drawing sleeps, as TF2's particle code does, where they used to hang in the air and pile up. On f12,
60 seconds from tick 30000, live effects stayed between 8 and 41 where they had climbed from 65 to 199
(merge3315abde; finding 79). A seek replays the effects as playing does (D221). -
Particles step once per frame on that frame's own points and time, as TF2 does (D221, B539).
-
Smoother playback on a busy scene. A dead player's physics was rebuil...
Tf2DemoSalvage 0.1.0-beta.48
Tf2DemoSalvage 0.1.0-beta.48
Changes in 0.1.0-beta.48
Released 2026-10-22.
- A sound no longer freezes a frame the first time it plays. A sound that was not loaded yet used to be read from
the game files and decoded in the middle of a frame. It now loads in the background and starts as soon as it is
ready, which is how TF2 treats a sound whose data has not arrived. On the author'scp_process_f12recording, over
60 seconds of playback, sounds read or decoded on the render thread went from 5 to 0 (finding 90).
The one hitch the game's own log flagged, a 171 ms stall on a voice line, is gone (finding 90). The trade: such a
sound starts 1 to 28 simulation ticks late the first time it is heard (finding 90), and a stop that arrives while it
is still loading now cancels it. A stop for a sound that was never loaded no longer loads it. - Less memory churn per frame. Seven more per-frame allocations are gone (corpse physics, corpse animation
copies, layer lists, placement matrices, the healing beam and the HUD's vertex array). On the same recording the
render thread went from 1.21-1.26 MB to 0.94-1.01 MB allocated per frame (finding 91), and all threads together
from 2.50-2.98 MB to 1.54-1.83 MB per frame (finding 91), with fewer garbage-collection passes in 60 seconds
(1,631-1,903 to 945-1,124, finding 91). Frame times did not change measurably, so this release makes no frame-rate
claim for it. - Developer notes. Findings 90 and 91 and risks B568, B570 and B574 in the repository's
docsfolder record how
these were measured.
Changes in 0.1.0-beta.47
Released 2026-10-21.
- The viewer draws faster. In first person on the author's
cp_process_f12recording, a frame went from about
16.5-17.1 ms to about 14.5-15.3 ms, and with nothing in view from about 7.3 ms to about 5.6 ms. The largest single
part was a one-millisecond wait after every frame that served only accessibility and test tools. It now runs only
when such a tool is attached to the window. - Less memory churn per frame. Memory allocated per frame fell from about 7.7 MB to about 5.1 MB over all threads
(the render thread from about 4.0 MB to 2.2 MB), because skeleton poses and inverse-kinematics lists are reused.
This also shortens the collector's pauses. - Fewer playback hitches from sounds. Weapon draw and reload sounds are loaded while the demo loads instead of
the first time they play. On that recording, misses during play fell from 12 a minute to 4. - A saved screenshot appears only when the whole image is on disk, so a tool or a file watcher can no longer pick
up a half-written PNG. - A particle effect whose owner is out of view for a while and then returns is no longer lost. Before, it stayed
gone until you scrubbed. - Developer options.
--measurealso prints the memory allocated per frame by the render thread, and lists
stalls beside whether the whole process paused.
Changes in 0.1.0-beta.46
Released 2026-10-21.
- Large demos open faster. Building the timeline for a 58 MB match (the
cp_process_f12recording) went from about
170 seconds to about 100 seconds on the author's machine, with the same memory use. - Fewer hitches during playback. Surface sounds (footsteps, impacts, scrapes) are decoded while the demo loads
instead of the first time they play. Rocket-jump smoke effects that had already finished are no longer rebuilt all at
once after the camera moves. - Particle effects follow the game's rules for what is drawn and what sleeps. An effect that is out of view is not
drawn and goes to sleep after a few seconds, as in TF2, and one frame of particle simulation is limited to 0.1
seconds, as in the game. On the author's f12 test the picture is unchanged: the comparison images differ only by the
frame-rate counter and nine pixels that are off by two shades out of 255. - Developer option.
--measureprints new lines for JIT compilation and for particle work per frame.
Changes in 0.1.0-beta.45
Released 2026-10-20. Betas 0.1.0-beta.16 to 0.1.0-beta.44 were never published, so this is the first public build since beta.15. The
sections below cover every change since, one per version.
-
--shotwrites a companion file beside the picture.--shot out.pngalso writesout.png.json: the demo's
file name and its sha256, the map, the tick, the camera, the view size, the settings that differ from their
defaults, the build and the command line (file names only). You can rebuild a capture from it later (D226).
Deleting a screenshot from the viewer's tidy-up deletes its companion too. The F12 key writes one only in a
Debug build. -
--like <shot.png.json>reproduces a TF2 capture. Give it the companion file of a TF2-side capture and the
viewer takes its tick, camera, view size and console variables, and its demo too, from the file's folder, when
you name none. A--tickor+cvaryou pass still wins; the camera and view-size environment variables do not.
If the demo you open is not the one the file names (different sha256), the viewer warns and still shoots. A
file with a bad camera, or one whose demo is a path rather than a file name, is refused with the reason. -
Static props are no longer walked every frame. A map's static props (1,353 on
cp_process_f12) are filed
once and only the ones in view are posed; the picture is identical pixel for pixel in the two golden pairs
taken (finding 83, item 1). Measured on one machine, on f12 at tick 30000 in a Debug viewer, looking at nothing
(TF2VIEW_CAMERA="1613 -2821 154 89 0"): the simulate phase fell from 1.2-1.4 ms to 0.5-0.7 ms a frame and
the draw-list phase from 0.4-0.6 ms to 0.1-0.2 ms. The whole frame ranged 7.7-9.0 ms before and 6.0-9.4 ms
after, with other work on the machine, so quote the phases, not the frame. A static prop you look at is still
animated when it has an animation, which the game does not do (B560). -
A demo whose map is missing still opens, and the status bar says why (D224). The viewer fetches a map it
lacks, so you should rarely see this. TF2 itself refuses such a demo. Unverified: that the message reaches
the status bar on every path (see Known gaps). -
TF2VIEW_VIEWPORT_SIZE("1280x720", WIDTHxHEIGHT) sizes the 3D view itself; the window grows around it,
so the view is that size at any display scaling. A value that does not parse is logged and ignored. For scripted
captures;--helplists it. -
Demo voice plays. Voice chat recorded in a demo is heard through five voice channels, as TF2 plays it; a sixth
speaker at once is dropped, as in the game.voice_scaleandvoice_enableare read. Not yet ported: the
lowering of other sounds while someone speaks and the speaking indicator (B549, see Known gaps).
Not checked by ear in a played demo. -
snd_musicvolumelowers music, as TF2 does: the sounds with a#in their name and no distance falloff,
including map soundscapes, and a change reaches music already playing (B545). -
Animated props draw their real pose. A door or a resupply locker that animates is now posed by its bones
instead of from one stored copy per animation step. Before, the last step of a one-shot animation blended toward
the closed pose, so on f12 a door at tick 6520 drew shut where TF2 draws it open (finding 80). The change is
not measurable in frame time: eight 20-second runs on f12 at tick 30000 averaged 40.1 ms (before) and 40.3 ms
(after). A few props still differ slightly; see Known gaps. -
A building or prop shows its state at the moment you are watching, not the demo's last one. Its model, item,
class, team, parent, owner and attachments are read for the current tick. On f12 a sentry (entity 457) drew its
end-of-demo level 3 at tick 66600, where it was level 1 (B548). -
Parts that move with a door are placed and lit where the door puts them. Every child of a
func_dooron
badwater, upward and vigil was lit from its own offset, and a grandchild was drawn only one level deep (B551). -
Effects end where TF2 ends them. An effect whose owner went dormant now stops emitting and fades, and one
nothing is drawing sleeps, as TF2's particle code does, where they used to hang in the air and pile up. On f12,
60 seconds from tick 30000, live effects stayed between 8 and 41 where they had climbed from 65 to 199
(merge3315abde; finding 79). A seek replays the effects as playing does (D221). -
Particles step once per frame on that frame's own points and time, as TF2 does (D221, B539).
-
Smoother playback on a busy scene. A dead player's physics was rebuilt every frame: 48.7 ms a rebuild, about 1 ms
now. Below that, TF2's HUD texture sizes are read once, and a first-person arm nobody can see is not posed. -
Less memory on a map. Measured on f12, a Debug viewer,
--measure 20(finding 80); run-to-run spread is
about 400 MB, and the figures are one change each, not a sum:- A map's screen overlays decode their animated normal map once, not once per frame: the load's transient peak
fell from 2,245 to 940 MB, the viewer's peak working set from 9.3 to 7.6 GB and peak private bytes from 11.1 to
9.5 GB. - The allocation fixes behind finding 80's "after the four fixes" re-census (particle lookups, face flex
arrays, decals, sound conversion; the finding lists each) took the large allocations of 20 seconds of playback from 1,867-2,241 MB to 67-87 MB; private bytes at the end of
playback went from 10,689 to 8,494-8,733 MB and the working set from 8,170 to 6,015-6,249 MB. - Sounds are held as 16-bit, and the sound cache is capped at 512 MB, least recently used out first (the cap is
our figure; TF2's is not in the SDK). WAV memory 387 to 194 MB; private bytes at the end 8,493-8,514 to
8,298-8,305 MB. - Animated props from bones (above): private bytes at the end 7,378 and 7,370 M...
- A map's screen overlays decode their animated normal map once, not once per frame: the load's transient peak
Tf2DemoSalvage 0.1.0-beta.47
Tf2DemoSalvage 0.1.0-beta.47
Unreleased
Changes in 0.1.0-beta.47
Released 2026-10-21.
- The viewer draws faster. In first person on the author's
cp_process_f12recording, a frame went from about
16.5-17.1 ms to about 14.5-15.3 ms, and with nothing in view from about 7.3 ms to about 5.6 ms. The largest single
part was a one-millisecond wait after every frame that served only accessibility and test tools. It now runs only
when such a tool is attached to the window. - Less memory churn per frame. Memory allocated per frame fell from about 7.7 MB to about 5.1 MB over all threads
(the render thread from about 4.0 MB to 2.2 MB), because skeleton poses and inverse-kinematics lists are reused.
This also shortens the collector's pauses. - Fewer playback hitches from sounds. Weapon draw and reload sounds are loaded while the demo loads instead of
the first time they play. On that recording, misses during play fell from 12 a minute to 4. - A saved screenshot appears only when the whole image is on disk, so a tool or a file watcher can no longer pick
up a half-written PNG. - A particle effect whose owner is out of view for a while and then returns is no longer lost. Before, it stayed
gone until you scrubbed. - Developer options.
--measurealso prints the memory allocated per frame by the render thread, and lists
stalls beside whether the whole process paused.
Changes in 0.1.0-beta.46
Released 2026-10-21.
- Large demos open faster. Building the timeline for a 58 MB match (the
cp_process_f12recording) went from about
170 seconds to about 100 seconds on the author's machine, with the same memory use. - Fewer hitches during playback. Surface sounds (footsteps, impacts, scrapes) are decoded while the demo loads
instead of the first time they play. Rocket-jump smoke effects that had already finished are no longer rebuilt all at
once after the camera moves. - Particle effects follow the game's rules for what is drawn and what sleeps. An effect that is out of view is not
drawn and goes to sleep after a few seconds, as in TF2, and one frame of particle simulation is limited to 0.1
seconds, as in the game. On the author's f12 test the picture is unchanged: the comparison images differ only by the
frame-rate counter and nine pixels that are off by two shades out of 255. - Developer option.
--measureprints new lines for JIT compilation and for particle work per frame.
Changes in 0.1.0-beta.45
Released 2026-10-20. Betas 0.1.0-beta.16 to 0.1.0-beta.44 were never published, so this is the first public build since beta.15. The
sections below cover every change since, one per version.
-
--shotwrites a companion file beside the picture.--shot out.pngalso writesout.png.json: the demo's
file name and its sha256, the map, the tick, the camera, the view size, the settings that differ from their
defaults, the build and the command line (file names only). You can rebuild a capture from it later (D226).
Deleting a screenshot from the viewer's tidy-up deletes its companion too. The F12 key writes one only in a
Debug build. -
--like <shot.png.json>reproduces a TF2 capture. Give it the companion file of a TF2-side capture and the
viewer takes its tick, camera, view size and console variables, and its demo too, from the file's folder, when
you name none. A--tickor+cvaryou pass still wins; the camera and view-size environment variables do not.
If the demo you open is not the one the file names (different sha256), the viewer warns and still shoots. A
file with a bad camera, or one whose demo is a path rather than a file name, is refused with the reason. -
Static props are no longer walked every frame. A map's static props (1,353 on
cp_process_f12) are filed
once and only the ones in view are posed; the picture is identical pixel for pixel in the two golden pairs
taken (finding 83, item 1). Measured on one machine, on f12 at tick 30000 in a Debug viewer, looking at nothing
(TF2VIEW_CAMERA="1613 -2821 154 89 0"): the simulate phase fell from 1.2-1.4 ms to 0.5-0.7 ms a frame and
the draw-list phase from 0.4-0.6 ms to 0.1-0.2 ms. The whole frame ranged 7.7-9.0 ms before and 6.0-9.4 ms
after, with other work on the machine, so quote the phases, not the frame. A static prop you look at is still
animated when it has an animation, which the game does not do (B560). -
A demo whose map is missing still opens, and the status bar says why (D224). The viewer fetches a map it
lacks, so you should rarely see this. TF2 itself refuses such a demo. Unverified: that the message reaches
the status bar on every path (see Known gaps). -
TF2VIEW_VIEWPORT_SIZE("1280x720", WIDTHxHEIGHT) sizes the 3D view itself; the window grows around it,
so the view is that size at any display scaling. A value that does not parse is logged and ignored. For scripted
captures;--helplists it. -
Demo voice plays. Voice chat recorded in a demo is heard through five voice channels, as TF2 plays it; a sixth
speaker at once is dropped, as in the game.voice_scaleandvoice_enableare read. Not yet ported: the
lowering of other sounds while someone speaks and the speaking indicator (B549, see Known gaps).
Not checked by ear in a played demo. -
snd_musicvolumelowers music, as TF2 does: the sounds with a#in their name and no distance falloff,
including map soundscapes, and a change reaches music already playing (B545). -
Animated props draw their real pose. A door or a resupply locker that animates is now posed by its bones
instead of from one stored copy per animation step. Before, the last step of a one-shot animation blended toward
the closed pose, so on f12 a door at tick 6520 drew shut where TF2 draws it open (finding 80). The change is
not measurable in frame time: eight 20-second runs on f12 at tick 30000 averaged 40.1 ms (before) and 40.3 ms
(after). A few props still differ slightly; see Known gaps. -
A building or prop shows its state at the moment you are watching, not the demo's last one. Its model, item,
class, team, parent, owner and attachments are read for the current tick. On f12 a sentry (entity 457) drew its
end-of-demo level 3 at tick 66600, where it was level 1 (B548). -
Parts that move with a door are placed and lit where the door puts them. Every child of a
func_dooron
badwater, upward and vigil was lit from its own offset, and a grandchild was drawn only one level deep (B551). -
Effects end where TF2 ends them. An effect whose owner went dormant now stops emitting and fades, and one
nothing is drawing sleeps, as TF2's particle code does, where they used to hang in the air and pile up. On f12,
60 seconds from tick 30000, live effects stayed between 8 and 41 where they had climbed from 65 to 199
(merge3315abde; finding 79). A seek replays the effects as playing does (D221). -
Particles step once per frame on that frame's own points and time, as TF2 does (D221, B539).
-
Smoother playback on a busy scene. A dead player's physics was rebuilt every frame: 48.7 ms a rebuild, about 1 ms
now. Below that, TF2's HUD texture sizes are read once, and a first-person arm nobody can see is not posed. -
Less memory on a map. Measured on f12, a Debug viewer,
--measure 20(finding 80); run-to-run spread is
about 400 MB, and the figures are one change each, not a sum:- A map's screen overlays decode their animated normal map once, not once per frame: the load's transient peak
fell from 2,245 to 940 MB, the viewer's peak working set from 9.3 to 7.6 GB and peak private bytes from 11.1 to
9.5 GB. - The allocation fixes behind finding 80's "after the four fixes" re-census (particle lookups, face flex
arrays, decals, sound conversion; the finding lists each) took the large allocations of 20 seconds of playback from 1,867-2,241 MB to 67-87 MB; private bytes at the end of
playback went from 10,689 to 8,494-8,733 MB and the working set from 8,170 to 6,015-6,249 MB. - Sounds are held as 16-bit, and the sound cache is capped at 512 MB, least recently used out first (the cap is
our figure; TF2's is not in the SDK). WAV memory 387 to 194 MB; private bytes at the end 8,493-8,514 to
8,298-8,305 MB. - Animated props from bones (above): private bytes at the end 7,378 and 7,370 MB before, 6,045 and 6,087 after.
- A map load reads each texture file once, particle definitions are one compact copy per TF2 install
(managed heap after a load, 1,026.0 to 934.2 MB), and a block texture's upload no longer copies its mip levels. - The load's peak did not move: the upload still peaks at 9.3-11.1 GB private bytes in those runs.
The six-versus-six figures under Requirements have not been re-measured.
- A map's screen overlays decode their animated normal map once, not once per frame: the load's transient peak
-
A map's own particle file is read (
maps/<name>_particles.txt, orparticles.txtin the map), so a community
map's custom effects appear, as in TF2 (B554, finding 81). A particle declared twice keeps the later
definition, which TF2 does and the viewer did not, so 30 stock effects now come from a different file than
before:ExplosionCore_MidAirand fourExplosions_MA_*(nowbigboom.pcf), the eleven_unusual_parent_*
(nowweapon_unusual_energyorb.pcf),blood_decap,taunt_gasblast_steam,mini_firework*, the three
speech_medic*and others (B555). Not compared on screen in a demo. -
The stock HUD is closer to TF2's (finding 78; it applies when you do not pass
--hud): the ammo and
class backgrounds take their shape, soft edge and outline from the game's masks instead of flat blocks; the
class background follows your team (it was red for a BLU player); the class model stands in its activity pose;
HUD colors are gamma-corrected, which TF2's tan text needs; the ammo number paints over its shadow (B552); and
a medic gets the ÜberCharge meter. The ...
Tf2DemoSalvage 0.1.0-beta.46
Tf2DemoSalvage 0.1.0-beta.46
Unreleased
Changes in 0.1.0-beta.46
Released 2026-10-21.
- Large demos open faster. Building the timeline for a 58 MB match (the
cp_process_f12recording) went from about
170 seconds to about 100 seconds on the author's machine, with the same memory use. - Fewer hitches during playback. Surface sounds (footsteps, impacts, scrapes) are decoded while the demo loads
instead of the first time they play. Rocket-jump smoke effects that had already finished are no longer rebuilt all at
once after the camera moves. - Particle effects follow the game's rules for what is drawn and what sleeps. An effect that is out of view is not
drawn and goes to sleep after a few seconds, as in TF2, and one frame of particle simulation is limited to 0.1
seconds, as in the game. On the author's f12 test the picture is unchanged: the comparison images differ only by the
frame-rate counter and nine pixels that are off by two shades out of 255. - Developer option.
--measureprints new lines for JIT compilation and for particle work per frame.
Changes in 0.1.0-beta.45
Released 2026-10-20. Betas 0.1.0-beta.16 to 0.1.0-beta.44 were never published, so this is the first public build since beta.15. The
sections below cover every change since, one per version.
-
--shotwrites a companion file beside the picture.--shot out.pngalso writesout.png.json: the demo's
file name and its sha256, the map, the tick, the camera, the view size, the settings that differ from their
defaults, the build and the command line (file names only). You can rebuild a capture from it later (D226).
Deleting a screenshot from the viewer's tidy-up deletes its companion too. The F12 key writes one only in a
Debug build. -
--like <shot.png.json>reproduces a TF2 capture. Give it the companion file of a TF2-side capture and the
viewer takes its tick, camera, view size and console variables, and its demo too, from the file's folder, when
you name none. A--tickor+cvaryou pass still wins; the camera and view-size environment variables do not.
If the demo you open is not the one the file names (different sha256), the viewer warns and still shoots. A
file with a bad camera, or one whose demo is a path rather than a file name, is refused with the reason. -
Static props are no longer walked every frame. A map's static props (1,353 on
cp_process_f12) are filed
once and only the ones in view are posed; the picture is identical pixel for pixel in the two golden pairs
taken (finding 83, item 1). Measured on one machine, on f12 at tick 30000 in a Debug viewer, looking at nothing
(TF2VIEW_CAMERA="1613 -2821 154 89 0"): the simulate phase fell from 1.2-1.4 ms to 0.5-0.7 ms a frame and
the draw-list phase from 0.4-0.6 ms to 0.1-0.2 ms. The whole frame ranged 7.7-9.0 ms before and 6.0-9.4 ms
after, with other work on the machine, so quote the phases, not the frame. A static prop you look at is still
animated when it has an animation, which the game does not do (B560). -
A demo whose map is missing still opens, and the status bar says why (D224). The viewer fetches a map it
lacks, so you should rarely see this. TF2 itself refuses such a demo. Unverified: that the message reaches
the status bar on every path (see Known gaps). -
TF2VIEW_VIEWPORT_SIZE("1280x720", WIDTHxHEIGHT) sizes the 3D view itself; the window grows around it,
so the view is that size at any display scaling. A value that does not parse is logged and ignored. For scripted
captures;--helplists it. -
Demo voice plays. Voice chat recorded in a demo is heard through five voice channels, as TF2 plays it; a sixth
speaker at once is dropped, as in the game.voice_scaleandvoice_enableare read. Not yet ported: the
lowering of other sounds while someone speaks and the speaking indicator (B549, see Known gaps).
Not checked by ear in a played demo. -
snd_musicvolumelowers music, as TF2 does: the sounds with a#in their name and no distance falloff,
including map soundscapes, and a change reaches music already playing (B545). -
Animated props draw their real pose. A door or a resupply locker that animates is now posed by its bones
instead of from one stored copy per animation step. Before, the last step of a one-shot animation blended toward
the closed pose, so on f12 a door at tick 6520 drew shut where TF2 draws it open (finding 80). The change is
not measurable in frame time: eight 20-second runs on f12 at tick 30000 averaged 40.1 ms (before) and 40.3 ms
(after). A few props still differ slightly; see Known gaps. -
A building or prop shows its state at the moment you are watching, not the demo's last one. Its model, item,
class, team, parent, owner and attachments are read for the current tick. On f12 a sentry (entity 457) drew its
end-of-demo level 3 at tick 66600, where it was level 1 (B548). -
Parts that move with a door are placed and lit where the door puts them. Every child of a
func_dooron
badwater, upward and vigil was lit from its own offset, and a grandchild was drawn only one level deep (B551). -
Effects end where TF2 ends them. An effect whose owner went dormant now stops emitting and fades, and one
nothing is drawing sleeps, as TF2's particle code does, where they used to hang in the air and pile up. On f12,
60 seconds from tick 30000, live effects stayed between 8 and 41 where they had climbed from 65 to 199
(merge3315abde; finding 79). A seek replays the effects as playing does (D221). -
Particles step once per frame on that frame's own points and time, as TF2 does (D221, B539).
-
Smoother playback on a busy scene. A dead player's physics was rebuilt every frame: 48.7 ms a rebuild, about 1 ms
now. Below that, TF2's HUD texture sizes are read once, and a first-person arm nobody can see is not posed. -
Less memory on a map. Measured on f12, a Debug viewer,
--measure 20(finding 80); run-to-run spread is
about 400 MB, and the figures are one change each, not a sum:- A map's screen overlays decode their animated normal map once, not once per frame: the load's transient peak
fell from 2,245 to 940 MB, the viewer's peak working set from 9.3 to 7.6 GB and peak private bytes from 11.1 to
9.5 GB. - The allocation fixes behind finding 80's "after the four fixes" re-census (particle lookups, face flex
arrays, decals, sound conversion; the finding lists each) took the large allocations of 20 seconds of playback from 1,867-2,241 MB to 67-87 MB; private bytes at the end of
playback went from 10,689 to 8,494-8,733 MB and the working set from 8,170 to 6,015-6,249 MB. - Sounds are held as 16-bit, and the sound cache is capped at 512 MB, least recently used out first (the cap is
our figure; TF2's is not in the SDK). WAV memory 387 to 194 MB; private bytes at the end 8,493-8,514 to
8,298-8,305 MB. - Animated props from bones (above): private bytes at the end 7,378 and 7,370 MB before, 6,045 and 6,087 after.
- A map load reads each texture file once, particle definitions are one compact copy per TF2 install
(managed heap after a load, 1,026.0 to 934.2 MB), and a block texture's upload no longer copies its mip levels. - The load's peak did not move: the upload still peaks at 9.3-11.1 GB private bytes in those runs.
The six-versus-six figures under Requirements have not been re-measured.
- A map's screen overlays decode their animated normal map once, not once per frame: the load's transient peak
-
A map's own particle file is read (
maps/<name>_particles.txt, orparticles.txtin the map), so a community
map's custom effects appear, as in TF2 (B554, finding 81). A particle declared twice keeps the later
definition, which TF2 does and the viewer did not, so 30 stock effects now come from a different file than
before:ExplosionCore_MidAirand fourExplosions_MA_*(nowbigboom.pcf), the eleven_unusual_parent_*
(nowweapon_unusual_energyorb.pcf),blood_decap,taunt_gasblast_steam,mini_firework*, the three
speech_medic*and others (B555). Not compared on screen in a demo. -
The stock HUD is closer to TF2's (finding 78; it applies when you do not pass
--hud): the ammo and
class backgrounds take their shape, soft edge and outline from the game's masks instead of flat blocks; the
class background follows your team (it was red for a BLU player); the class model stands in its activity pose;
HUD colors are gamma-corrected, which TF2's tan text needs; the ammo number paints over its shadow (B552); and
a medic gets the ÜberCharge meter. The class portrait is drawn at tone-map scale 1, which accounts for its being
about 20 luma darker than TF2's. The ammo golden is live (B552); the class and ÜberCharge goldens stay
Explicit (B559): whether the portrait now matches TF2's is for those goldens to show. -
The program has its own icon, a film reel with a play hub in brown and gold, replacing the Windows default.
-
Developer options.
--measurealso prints the GC's own memory and the large allocations by type.
--windows(in--help) walks the demo into 2-second checkpoints and logs, per seek, what a window cost and
whether it matched; nothing drawn changes (D217).
Changes in 0.1.0-beta.44
- Volume control. View > Volume sets the sound volume in tenths, and
volumeworks as in TF2: in a config,
as+volume 0.5at launch, and in binds such asbind m "toggle volume 0 1"orincrementvar volume 0 1 0.1.
The menu remembers its choice. The viewer now goes silent while another window has focus, as TF2 does by
default; untick View > Volume > Mute when not focused, or setsnd_mute_losefocus 0, to keep the sound
(D218).snd_musicvolumeandvoice_scaleare accepted but change nothing yet (B545). - A key that sets a console variable now takes effect immediately. Binds like
bind x "cl_showpos 1", and alias
toggles that set a variable, used to change nothing...