Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions sdk/replay/BlackBox.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
//! person's input and any tape's. Each dvui event becomes a fixed record in `tape.events`' rings:
//! pointer motion and buttons where they were, in the window's pixels, wheel ticks, keys down,
//! repeated and up with their modifiers, text as it arrived. Focus and position events are left
//! out: dvui makes them from the others, and a replay's dvui will again. A frame costs a copy of its
//! events (ReleaseFast, measured: 0.5 ns a frame with none, 5 ns with four); the rings are 1.3 MB,
//! made the first time it records.
//! out: dvui makes them from the others, and a replay's dvui will again. A frame costs a copy of
//! its events (ReleaseFast, measured: 0.5 ns a frame with none, 5 ns with four); the rings are 1.3
//! MB, made the first time it records.
//!
//! Any dvui app can keep one: dvui and `tape` only. Fizzy writes its stretches as captures
//! (`fizzy.capture`) and replays them (plans/RECORDER_PLAN.md, step 3). A stream that is not dvui's
Expand Down Expand Up @@ -68,9 +68,27 @@ pub fn record(self: *BlackBox, gpa: std.mem.Allocator) void {
fn toRecord(e: *const dvui.Event) ?events.Event {
return switch (e.evt) {
.mouse => |me| switch (me.action) {
.motion => .{ .kind = .motion, .button = @backingInt(me.button), .mod = @backingInt(me.mod), .x = me.p.x, .y = me.p.y },
.press => .{ .kind = .press, .button = @backingInt(me.button), .mod = @backingInt(me.mod), .x = me.p.x, .y = me.p.y },
.release => .{ .kind = .release, .button = @backingInt(me.button), .mod = @backingInt(me.mod), .x = me.p.x, .y = me.p.y },
.motion => .{
.kind = .motion,
.button = @backingInt(me.button),
.mod = @backingInt(me.mod),
.x = me.p.x,
.y = me.p.y,
},
.press => .{
.kind = .press,
.button = @backingInt(me.button),
.mod = @backingInt(me.mod),
.x = me.p.x,
.y = me.p.y,
},
.release => .{
.kind = .release,
.button = @backingInt(me.button),
.mod = @backingInt(me.mod),
.x = me.p.x,
.y = me.p.y,
},
.wheel_x => |ticks| .{ .kind = .wheel_x, .mod = @backingInt(me.mod), .x = ticks },
.wheel_y => |ticks| .{ .kind = .wheel_y, .mod = @backingInt(me.mod), .x = ticks },
.focus, .position => null,
Expand Down
11 changes: 8 additions & 3 deletions sdk/replay/Capture.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
//! and every window at once, laid out as they lie on the desktop round the main one, by `all`.
//!
//! Asked for like a `Snapshot` (`request`, then `get` once taken): the next frame the app draws
//! is read back at its end (`endFrame`) and kept, as the raw premultiplied pixels it drew, until the
//! next is taken. Nothing is encoded here: the frame pays for one read back and one copy, and
//! is read back at its end (`endFrame`) and kept, as the raw premultiplied pixels it drew, until
//! the next is taken. Nothing is encoded here: the frame pays for one read back and one copy, and
//! whoever wants an image file (the agent's bridge, in its own process) scales and encodes it.
const Capture = @This();

Expand Down Expand Up @@ -148,7 +148,12 @@ const max_side: f32 = 8192;
/// Every window, each where it lies round the main window, a later one over an earlier.
fn everyWindow(arena: std.mem.Allocator, sources: []const Source) !Pixels {
var span: dvui.Rect.Physical = .{ .w = @floatFromInt(sources[0].w), .h = @floatFromInt(sources[0].h) };
for (sources[1..]) |s| span = span.unionWith(.{ .x = s.at.x, .y = s.at.y, .w = @floatFromInt(s.w), .h = @floatFromInt(s.h) });
for (sources[1..]) |s| span = span.unionWith(.{
.x = s.at.x,
.y = s.at.y,
.w = @floatFromInt(s.w),
.h = @floatFromInt(s.h),
});
const w: u32 = @intFromFloat(@min(@round(span.w), max_side));
const h: u32 = @intFromFloat(@min(@round(span.h), max_side));
const out = try arena.alloc(u8, @as(usize, w) * h * 4);
Expand Down
9 changes: 8 additions & 1 deletion sdk/replay/Input.zig
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,14 @@ fn pressChord(c: chord.Chord) void {
// dvui keeps the last key event's modifiers as the window's, and every pointer event after
// carries them: let go of the modifier, as a hand does, or the next click is a Ctrl-click.
if (mod != .none) {
const modifier: dvui.enums.Key = if (c.mods.command) .left_command else if (c.mods.ctrl) .left_control else if (c.mods.alt) .left_alt else .left_shift;
const modifier: dvui.enums.Key = if (c.mods.command)
.left_command
else if (c.mods.ctrl)
.left_control
else if (c.mods.alt)
.left_alt
else
.left_shift;
addKey(cw, .{ .code = modifier, .mod = .none, .action = .up });
}
}
Expand Down
26 changes: 13 additions & 13 deletions sdk/replay/LiveDriver.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
//! landed. The other way to play a tape besides a demo, and a driver of its own rather than a
//! mode of the `Player` (`plans/AGENTS_PLAN.md`, "Driving a live tape"):
//!
//! | | a demo (`Player`) | a live tape (this) |
//! |-----------------------|-------------------------------------|-----------------------------------|
//! | starts from | a keyframe; the session set aside | the app as it is (`Check.live`) |
//! | pacing | the tape's authored times | each op once the app has settled |
//! | a person steps in | pause, and replay on resume | stop, and say before which op |
//! | going back | seek: snapshots, keyframes | none — undo, through commands |
//! | | a demo (`Player`) | a live tape (this) |
//! | --- | --- | --- |
//! | starts from | a keyframe; the session set aside | the app as it is (`Check.live`) |
//! | pacing | the tape's authored times | each op once the app has settled |
//! | a person steps in | pause, and replay on resume | stop, and say before which op |
//! | going back | seek: snapshots, keyframes | none — undo, through commands |
//!
//! Consumers: a plugin's tests, a macro, a scripted tutorial, anything driving fizzy the way a
//! person does. Plugins reach it through the `automation` service (`Service`).
Expand All @@ -18,13 +18,13 @@
//! live tape while a demo is loaded, and fizzy's `Demo` a demo while a live tape plays).
//!
//! **Pacing.** Authored times are ignored: each frame the driver asks the sequencer for the next
//! moment anything happens (`Sequencer.nextAt`) and applies it, and goes on only while the stage
//! is idle — nothing it was asked to load still loading. Except a drag: a glide with a button
//! held plays at its authored pace, a frame at a time, as a hand moves (`drag_step_ms`). The sequencer still yields after
//! everything the app has to draw before the next op can land (a press, a key, the end of a
//! glide), so a click on a menu the last click opened finds it open. A tape's own waits hold as
//! they do in a demo, and one that gives up stops the tape: a live tape that has lost its place
//! is better stopped and reported than carried on blind.
//! moment anything happens (`Sequencer.nextAt`) and applies it, and goes on only while the stage is
//! idle — nothing it was asked to load still loading. Except a drag: a glide with a button held
//! plays at its authored pace, a frame at a time, as a hand moves (`drag_step_ms`). The sequencer
//! still yields after everything the app has to draw before the next op can land (a press, a key,
//! the end of a glide), so a click on a menu the last click opened finds it open. A tape's own
//! waits hold as they do in a demo, and one that gives up stops the tape: a live tape that has lost
//! its place is better stopped and reported than carried on blind.
//!
//! **Interruption.** A real click, tap, scroll, key or typed text stops it, and the person's
//! input goes on to do what they meant. Real pointer motion is held off while it plays, as a
Expand Down
69 changes: 52 additions & 17 deletions sdk/replay/Player.zig
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,15 @@
//! and the backend's clock is moved on by as much (`frames`' `clock`) so dvui's stays monotonic
//! and continuous after. The player's own chrome (the transport bar) runs on the wall (`wallNs`).
//!
//! **Closing** always leaves the app as it is (`Stage.keep`), and "Revert to before" (`revert`) is the one way back to the session
//! the demo set aside. `play` after a person changed things carries on from there as a new branch
//! (`branched`) rather than replaying over their work; seeking back is the history.
//! **Closing** always leaves the app as it is (`Stage.keep`), and "Revert to before" (`revert`) is
//! the one way back to the session the demo set aside. `play` after a person changed things carries
//! on from there as a new branch (`branched`) rather than replaying over their work; seeking back
//! is the history.
//!
//! **Interruption**: while playing, a real click, tap, scroll or key pauses the demo — a pointer
//! event goes on to do what the person meant, the first key is only taken as "stop". Real pointer
//! motion is held off (the tape owns the pointer while it plays). Anything real that reaches the app
//! while paused marks the app `diverged` from the tape.
//! motion is held off (the tape owns the pointer while it plays). Anything real that reaches the
//! app while paused marks the app `diverged` from the tape.
const Player = @This();

const std = @import("std");
Expand Down Expand Up @@ -227,8 +228,8 @@ pub const Transport = struct {
/// physical, as far round it as the pointer is on it: what the bar hides into, and a press
/// there brings it back. Null when it was not drawn.
rest: ?dvui.Rect.Physical = null,
/// Sent into where it rests (its tab): the bar stays there, playing or not, until that brings it
/// back (a press here, the pointer held on it in the overlay).
/// Sent into where it rests (its tab): the bar stays there, playing or not, until that brings
/// it back (a press here, the pointer held on it in the overlay).
tucked: bool = false,
/// The speed slider, label and line (`Sequencer.speed`), whose notches are `Sequencer.speeds`.
speed: dvui.Rect.Physical = .{},
Expand Down Expand Up @@ -373,7 +374,8 @@ pub fn revert(self: *Player) void {
self.dismiss(.revert);
}

/// Stop at once and keep the app as it is, as `close` does: the command's way, with no bar to run back.
/// Stop at once and keep the app as it is, as `close` does: the command's way, with no bar to run
/// back.
pub fn stop(self: *Player) void {
self.release(.keep);
}
Expand Down Expand Up @@ -717,7 +719,10 @@ fn verify(self: *Player, s: Snapshot) void {
if (got == want) return;
self.mismatches += 1;
const name = if (self.tape()) |t| t.name else "?";
log.warn("demo '{s}': replaying to {d:.0} ms did not reach what playing did — the tape does not replay exactly", .{ name, s.at });
log.warn("demo '{s}': replaying to {d:.0} ms did not reach what playing did — the tape does not replay exactly", .{
name,
s.at,
});
}

fn dropSnapshots(self: *Player) void {
Expand All @@ -733,7 +738,10 @@ fn dropSnapshots(self: *Player) void {
pub fn frame(self: *Player) void {
// Whether this run is seen, once the seek it asks for (or lands) is known: a run while a seek
// catches up draws nothing, but the one `frames` ends it with.
defer self.run_unseen = if (self.set_unseen) |set| set((self.state == .seeking or self.catching_up) and !self.showing) else false;
defer self.run_unseen = if (self.set_unseen) |set|
set((self.state == .seeking or self.catching_up) and !self.showing)
else
false;
if (self.owned == null) return;
// Widgets name themselves for the tape while one is loaded (`anchor`).
anchor.want();
Expand Down Expand Up @@ -814,7 +822,12 @@ pub fn frame(self: *Player) void {
/// the runs took the app's clock past the wall — and, a seek still in flight, to the moment it
/// goes on from — so the next frame carries on from there. Without one a silent run steps the
/// clock the least dvui accepts, a microsecond, and timers wait for the wall.
pub fn frames(self: *Player, win: *dvui.Window, frame_fn: *const fn () anyerror!dvui.App.Result, clock: ?*i128) anyerror!dvui.App.Result {
pub fn frames(
self: *Player,
win: *dvui.Window,
frame_fn: *const fn () anyerror!dvui.App.Result,
clock: ?*i128,
) anyerror!dvui.App.Result {
// Counted before the run, which may land the seek, and after it, which may have asked for one.
const was_seeking = self.state == .seeking;
if (was_seeking) self.seek_stats.shown += 1;
Expand Down Expand Up @@ -869,7 +882,10 @@ fn runAt(self: *const Player, win: *dvui.Window, clock: ?*i128, base_ns: i128, b
// Waiting on a glide's target to stop moving (`targetStill`), a run is a frame of motion where
// the backend's clock can be moved on: on a microsecond's clock, a place sliding open would
// never move between runs, and look still.
const soonest = win.frame_time_ns + if (self.aiming != null and clock != null) msToNs(Sequencer.path_step_ms) else std.time.ns_per_us;
const soonest = win.frame_time_ns + if (self.aiming != null and clock != null)
msToNs(Sequencer.path_step_ms)
else
std.time.ns_per_us;
if (clock == null or self.state != .seeking) return soonest;
return @max(soonest, base_ns + msToNs(self.nextMoment() - base_ms));
}
Expand Down Expand Up @@ -1057,7 +1073,10 @@ fn transportTakes(self: *Player, e: *dvui.Event, me: dvui.Event.Mouse) bool {
self.seq.setSpeedIndex(tr.speedAt(me.p.x));
} else if (tr.track.contains(me.p)) {
tr.scrub = tr.timeAt(me.p.x, self.duration());
self.scrub_after = if (self.state == .playing or (self.state == .seeking and self.after_seek == .play)) .play else .pause;
self.scrub_after = if (self.state == .playing or (self.state == .seeking and self.after_seek == .play))
.play
else
.pause;
self.scrubbed_to = null;
} else if (tr.prev.contains(me.p)) {
self.stepChapter(-1);
Expand Down Expand Up @@ -1204,11 +1223,27 @@ fn failed(ctx: *anyopaque, failure: Sequencer.Failure) void {
const at = if (self.tape()) |t| t.ops[failure.op].at else 0;
switch (failure.what) {
.wait => |until| switch (until) {
.idle => dvui.log.warn("demo '{s}': gave up waiting for the app to settle (op {d}, {d} ms); skipping {d} ops to the next chapter", .{ name, failure.op, at, failure.skipped }),
.shown => |tag| dvui.log.warn("demo '{s}': gave up waiting for '{s}' to be drawn (op {d}, {d} ms); skipping {d} ops to the next chapter", .{ name, tag, failure.op, at, failure.skipped }),
.gone => |tag| dvui.log.warn("demo '{s}': gave up waiting for '{s}' to go (op {d}, {d} ms); skipping {d} ops to the next chapter", .{ name, tag, failure.op, at, failure.skipped }),
.idle => dvui.log.warn(
"demo '{s}': gave up waiting for the app to settle (op {d}, {d} ms); " ++
"skipping {d} ops to the next chapter",
.{ name, failure.op, at, failure.skipped },
),
.shown => |tag| dvui.log.warn(
"demo '{s}': gave up waiting for '{s}' to be drawn (op {d}, {d} ms); " ++
"skipping {d} ops to the next chapter",
.{ name, tag, failure.op, at, failure.skipped },
),
.gone => |tag| dvui.log.warn(
"demo '{s}': gave up waiting for '{s}' to go (op {d}, {d} ms); " ++
"skipping {d} ops to the next chapter",
.{ name, tag, failure.op, at, failure.skipped },
),
},
.target => |t| dvui.log.warn("demo '{s}': '{s}' was never drawn to move to (op {d}, {d} ms); skipping {d} ops to the next chapter", .{ name, t.tag, failure.op, at, failure.skipped }),
.target => |t| dvui.log.warn(
"demo '{s}': '{s}' was never drawn to move to (op {d}, {d} ms); " ++
"skipping {d} ops to the next chapter",
.{ name, t.tag, failure.op, at, failure.skipped },
),
}
}

Expand Down
Loading
Loading