screenshot-sequence: STEP_MS in decimal milliseconds

16.666667 steps a 60 fps clock. STEP_MS is a finite number more than 0,
and the reply gives it back as given.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Nikita Maximilian GrimmandClaude Opus 5.5 committed 2026-11-06 16:33:15 +01:00
1 parent 8650e51ddb
commit 94a66c81d4
6 files changed
+26 -21

No files matched your search

@@ -589,8 +589,8 @@ pub const COMMANDS: &[Cmd] = &[
["horizon-ctl trace /tmp/frame.json 10", "horizon-ctl trace stop"], &[], mcp: false, local: false),
cmd!("screenshot-sequence", Scripting, "screenshot-sequence FRAMES STEP_MS PATH|cancel", "draw a shader pack's first FRAMES frames on a set clock and write the last as a PNG",
"Needs HORIZON_STILL and a frozen world (docs/agent-control.md). The pack starts over, frame n's clock is n frames and (n - 1) × STEP_MS milliseconds, the PNG is written as screenshot PATH writes it, and the reply comes once it is. Relative paths are taken from horizon-ctl's directory. cancel stops a running sequence.",
["horizon-ctl screenshot-sequence 64 50 frame64.png", "horizon-ctl screenshot-sequence cancel"],
&[p("frames", Ty::Str, "frames to draw, 1 to 10000, or cancel"), opt("step_ms", Ty::Int, "milliseconds between frames on the pack's clock"), opt("path", Ty::Rest, "the PNG to write")], mcp: false, local: false),
["horizon-ctl screenshot-sequence 64 50 frame64.png", "horizon-ctl screenshot-sequence 64 16.666667 frame64.png", "horizon-ctl screenshot-sequence cancel"],
&[p("frames", Ty::Str, "frames to draw, 1 to 10000, or cancel"), opt("step_ms", Ty::Num, "milliseconds between frames on the pack's clock, more than 0 (16.666667 for 60 fps)"), opt("path", Ty::Rest, "the PNG to write")], mcp: false, local: false),
cmd!("render-capture", Scripting, "render-capture on|off|DIR", "keep CPU copies of the meshes the renderer draws (on), or write the next frame's scene to DIR for the scene_replay example",
"on meshes every section and far tile again so the copies are complete (HORIZON_RENDER_CAPTURE=1 keeps them from the start). DIR gets the camera, frame inputs, settings and every resident near section and far tile mesh; entities, particles, weather, the hand and the HUD are left out. See docs/graphics.md.",
["horizon-ctl render-capture on", "horizon-ctl render-capture /tmp/scene"], &[], mcp: false, local: false),
@@ -17,7 +17,7 @@ const READ_ONLY: [&str; 5] = ["screenshot-sequence", "hello", "help", "status",
pub(super) struct Sequence {
frames: u32,
step_ms: u32,
step_ms: f64,
/// The frame drawn next, from 1.
next: u32,
path: PathBuf,
@@ -34,7 +34,7 @@ impl Sequence {
}
/// `FRAMES STEP_MS PATH`; the path is the rest of the line and may hold spaces.
fn parse(args: &str) -> Result<(u32, u32, PathBuf)> {
fn parse(args: &str) -> Result<(u32, f64, PathBuf)> {
let usage = "screenshot-sequence FRAMES STEP_MS PATH, or screenshot-sequence cancel";
let (frames, rest) = args.trim().split_once(char::is_whitespace).ok_or(usage)?;
let (step_ms, path) = rest
@@ -42,12 +42,12 @@ fn parse(args: &str) -> Result<(u32, u32, PathBuf)> {
.split_once(char::is_whitespace)
.ok_or(usage)?;
let frames: u32 = frames.parse().map_err(|_| usage)?;
let step_ms: u32 = step_ms.parse().map_err(|_| usage)?;
let step_ms: f64 = step_ms.parse().map_err(|_| usage)?;
if !(1..=MAX_FRAMES).contains(&frames) {
return Err(format!("FRAMES must be 1 to {MAX_FRAMES}").into());
}
if step_ms == 0 {
return Err("STEP_MS must be more than 0".into());
if !step_ms.is_finite() || step_ms <= 0.0 {
return Err("STEP_MS must be a number of milliseconds more than 0".into());
}
Ok((frames, step_ms, path.trim().into()))
}
@@ -296,14 +296,18 @@ mod tests {
fn the_path_is_the_rest_of_the_line_and_the_counts_are_bounded() {
assert_eq!(
parse(" 64 50 /tmp/a b.png ").unwrap(),
(64, 50, PathBuf::from("/tmp/a b.png"))
(64, 50.0, PathBuf::from("/tmp/a b.png"))
);
assert_eq!(parse("10000 1 x.png").unwrap().0, 10_000);
assert_eq!(parse("64 16.666667 x.png").unwrap().1, 16.666667);
for bad in [
"0 50 x.png",
"10001 50 x.png",
"4 0 x.png",
"4 -1 x.png",
"4 0.0 x.png",
"4 NaN x.png",
"4 inf x.png",
"4 50",
"x 50 a",
] {
+7 -7
View File
@@ -409,13 +409,13 @@ of the frame ([#8750](https://github.com/horizon-rust/horizon/issues/8750)).
FRAMES STEP_MS PATH`: the pack starts over (its buffers, images, shadow maps, smoothed values and
custom uniforms, or a Horizon pack's `history` and eased values) and draws FRAMES frames on a set
clock, frame n at n frames and (n - 1) × STEP_MS milliseconds (0 since the frame before on frame
1, STEP_MS after). The last is written as `screenshot PATH` writes it (with its `-hdr.png` under
HDR output), and the reply, once it is, names the pack and the last frame's clock. Without a pack
it draws the frames all the same. It needs the world frozen, far terrain (LOD Distance), TAA,
frame generation and the shadow cache off and chunk fade 0, and says so otherwise. While it runs,
commands that would change the scene are refused, a change to the camera, the world, the
settings, the pack or the window's size ends it with an error, `screenshot-sequence cancel` stops
it and `quit` quits.
1, STEP_MS after; a decimal, `16.666667` for 60 fps). The last is written as `screenshot PATH`
writes it (with its `-hdr.png` under HDR output), and the reply, once it is, names the pack and
the last frame's clock. Without a pack it draws the frames all the same. It needs the world
frozen, far terrain (LOD Distance), TAA, frame generation and the shadow cache off and chunk fade
0, and says so otherwise. While it runs, commands that would change the scene are refused, a
change to the camera, the world, the settings, the pack or the window's size ends it with an
error, `screenshot-sequence cancel` stops it and `quit` quits.
- Not covered: temporal AA and frame generation (they build on earlier frames: turn them off),
custom entity model animations, native mods, and what eases on the wall clock for a moment
(toasts, the boss bar's fill, chunks fading in, the spyglass): wait for them to settle. Frame
+4 -4
View File
@@ -189,10 +189,10 @@ refused.
`horizon-ctl screenshot-sequence FRAMES STEP_MS PATH`, under `HORIZON_STILL`, starts the running pack over and draws
its first FRAMES frames on a set clock: frame n has `frame_index` n, `time` (n - 1) × STEP_MS / 1000 and `delta_time`
STEP_MS / 1000 (0 on frame 1), and `history` and the eased values start as in a pack just loaded. The last frame is
written as a PNG, the same bytes every run on the same build, GPU and driver. Packs in the `shaders/` format get the
same (`frameCounter`, `frameTimeCounter`, `frameTime`). How to use it:
[the guide](../shaders.md#testing-a-pack-frame-by-frame).
STEP_MS / 1000 (0 on frame 1; STEP_MS may be a decimal, `16.666667` for 60 fps), and `history` and the eased values
start as in a pack just loaded. The last frame is written as a PNG, the same bytes every run on the same build, GPU
and driver. Packs in the `shaders/` format get the same (`frameCounter`, `frameTimeCounter`, `frameTime`). How to use
it: [the guide](../shaders.md#testing-a-pack-frame-by-frame).
## Guide, reference and examples
+1 -1
View File
@@ -144,7 +144,7 @@ Every command `horizon-ctl` takes, its MCP tool in `horizon-ctl mcp` (the launch
| `stats [reset] [raw]` | `stats` | Frame-time summary since the last reset | `reset` (boolean): start counting again<br>`raw` (boolean): every frame time too (frame_ms, cpu_ms, gpu_ms) |
| `gpu-passes [on\|off]` | CLI only | Time Horizon's own render passes on the GPU, shown in F3 and in stats and bench | `mode` (on, off): on or off; without it, only reports |
| `trace [FILE [SECONDS] \| stop]` | CLI only | Write every thread's timed spans to FILE as a timeline that ui.perfetto.dev opens, for SECONDS or until stop | none |
| `screenshot-sequence FRAMES STEP_MS PATH\|cancel` | CLI only | Draw a shader pack's first FRAMES frames on a set clock and write the last as a PNG | `frames` (string, required): frames to draw, 1 to 10000, or cancel<br>`step_ms` (integer): milliseconds between frames on the pack's clock<br>`path` (text): the PNG to write |
| `screenshot-sequence FRAMES STEP_MS PATH\|cancel` | CLI only | Draw a shader pack's first FRAMES frames on a set clock and write the last as a PNG | `frames` (string, required): frames to draw, 1 to 10000, or cancel<br>`step_ms` (number): milliseconds between frames on the pack's clock, more than 0 (16.666667 for 60 fps)<br>`path` (text): the PNG to write |
| `render-capture on\|off\|DIR` | CLI only | Keep CPU copies of the meshes the renderer draws (on), or write the next frame's scene to DIR for the scene_replay example | none |
| `fly X1 Y1 Z1 X2 Y2 Z2 SECONDS [PITCH [raw]]` | CLI only | Fly the camera along a line, then reply with the frame summary | none |
| `record-start [NAME] [--no-world]` | `record_start` | Record input, state and the world for a bug repro | `name` (string): folder name after the timestamp (default session)<br>`--no-world` (boolean): do not copy the world (a server session) |
+2 -1
View File
@@ -73,7 +73,8 @@ horizon-ctl screenshot-sequence 64 50 frame64.png
starts the pack over as if just loaded (black `history`, eased values from the start) and draws 64 frames 50 ms
apart on its clock: frame n has `frame_index` n, `time` (n - 1) × 0.05 and `delta_time` 0.05 (0 on frame 1). The
64th is written to `frame64.png` (and `frame64-hdr.png` with HDR output on) before the reply. The same build, GPU
64th is written to `frame64.png` (and `frame64-hdr.png` with HDR output on) before the reply. STEP_MS may be a
decimal: `16.666667` steps a 60 fps clock. The same build, GPU
and driver write the same bytes every run, so two versions of a pack compare by a pixel diff. `screenshot-sequence
cancel` stops one.