diff --git a/crates/horizon-client/src/bin/horizon-ctl/commands.rs b/crates/horizon-client/src/bin/horizon-ctl/commands.rs index f549a162af..af02779d78 100644 --- a/crates/horizon-client/src/bin/horizon-ctl/commands.rs +++ b/crates/horizon-client/src/bin/horizon-ctl/commands.rs @@ -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), diff --git a/crates/horizon-client/src/game/screenshot_sequence.rs b/crates/horizon-client/src/game/screenshot_sequence.rs index d2029e52ad..ccc7f69d08 100644 --- a/crates/horizon-client/src/game/screenshot_sequence.rs +++ b/crates/horizon-client/src/game/screenshot_sequence.rs @@ -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", ] { diff --git a/docs/agent-control.md b/docs/agent-control.md index d2c13027fe..169cc98130 100644 --- a/docs/agent-control.md +++ b/docs/agent-control.md @@ -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 diff --git a/docs/api/shaders.md b/docs/api/shaders.md index 5f09e960bb..7e69cda264 100644 --- a/docs/api/shaders.md +++ b/docs/api/shaders.md @@ -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 diff --git a/docs/horizon-ctl-commands.md b/docs/horizon-ctl-commands.md index 4c9bdf755c..2d3eabccee 100644 --- a/docs/horizon-ctl-commands.md +++ b/docs/horizon-ctl-commands.md @@ -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
`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
`step_ms` (integer): milliseconds between frames on the pack's clock
`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
`step_ms` (number): milliseconds between frames on the pack's clock, more than 0 (16.666667 for 60 fps)
`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)
`--no-world` (boolean): do not copy the world (a server session) | diff --git a/docs/shaders.md b/docs/shaders.md index 297db06ccf..3bf0d22842 100644 --- a/docs/shaders.md +++ b/docs/shaders.md @@ -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.