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
5 changes: 2 additions & 3 deletions examples/frameclock_simulated/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,10 @@ fn main() {
now,
predicted_present: Some(predicted_present),
refresh_interval: Some(REFRESH_INTERVAL.ticks()),
frame_index,
output,
prev_actual_present: if frame_index > 0 { Some(now) } else { None },
};
let tick_event = FrameTickEvent::from(&tick);
let tick_event = FrameTickEvent::new(frame_index, &tick);
summary.record_frame_tick(&tick_event);
diagnostics.frame_tick(&tick_event);

Expand All @@ -107,7 +106,7 @@ fn main() {
hints,
DisplayTiming::from_tick(&tick, REFRESH_INTERVAL),
);
let plan = scheduler.plan(opportunity, FrameDemand::ANIMATION);
let plan = scheduler.plan(opportunity, FrameDemand::ANIMATION, frame_index);
let plan_event = FramePlanEvent::new(&plan, scheduler.safety_margin_ticks());
summary.record_frame_plan(&plan_event);
diagnostics.frame_plan(&plan_event);
Expand Down
5 changes: 2 additions & 3 deletions examples/trace_demo/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ fn main() {
now: HostTime(now_ticks),
predicted_present: Some(HostTime(now_ticks + refresh_interval)),
refresh_interval: Some(refresh_interval),
frame_index,
output: OutputId(0),
prev_actual_present: if frame_index > 0 {
// Previous frame presented on time.
Expand All @@ -61,7 +60,7 @@ fn main() {
},
};

let tick_event = FrameTickEvent::from(&tick);
let tick_event = FrameTickEvent::new(frame_index, &tick);
pretty.on_frame_tick(&tick_event);
recorder.on_frame_tick(&tick_event);

Expand All @@ -85,7 +84,7 @@ fn main() {
hints,
DisplayTiming::from_tick(&tick, Duration(refresh_interval)),
);
let plan = scheduler.plan(opportunity, FrameDemand::ANIMATION);
let plan = scheduler.plan(opportunity, FrameDemand::ANIMATION, frame_index);
let plan_end = HostTime(now_ticks + 100_000);

let plan_event = FramePlanEvent::new(&plan, scheduler.safety_margin_ticks());
Expand Down
3 changes: 2 additions & 1 deletion examples/wayland_layers/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const DEFAULT_W: u32 = 800;
const DEFAULT_H: u32 = 600;
const NUM_LAYERS: usize = 5;
const LAYER_SIZE: u32 = 80;
const UNOBSERVED_FRAME_INDEX: u64 = 0;

/// ARGB colors for the five layers.
const COLORS: [[u8; 4]; NUM_LAYERS] = [
Expand Down Expand Up @@ -164,7 +165,7 @@ fn main() {
let build_start = frameclock_wayland::now();

let opportunity = frameclock_wayland::frame_opportunity(tick, Duration(16_666_667));
let plan = scheduler.plan(opportunity, FrameDemand::ANIMATION);
let plan = scheduler.plan(opportunity, FrameDemand::ANIMATION, UNOBSERVED_FRAME_INDEX);

let elapsed_nanos = plan.sample_time.ticks().saturating_sub(start_nanos);
let t = elapsed_nanos as f64 / 1_000_000_000.0;
Expand Down
3 changes: 2 additions & 1 deletion examples/wayland_lotta_layers/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const DEFAULT_W: u32 = 1024;
const DEFAULT_H: u32 = 768;
const NUM_GROUPS: usize = 10;
const LAYERS_PER_GROUP: usize = 10;
const UNOBSERVED_FRAME_INDEX: u64 = 0;

/// Returns an `[r, g, b]` triple in 0–255 for a given index using
/// golden-angle hue spacing.
Expand Down Expand Up @@ -194,7 +195,7 @@ fn main() {
let build_start = frameclock_wayland::now();

let opportunity = frameclock_wayland::frame_opportunity(tick, Duration(16_666_667));
let plan = scheduler.plan(opportunity, FrameDemand::ANIMATION);
let plan = scheduler.plan(opportunity, FrameDemand::ANIMATION, UNOBSERVED_FRAME_INDEX);

let elapsed_nanos = plan.sample_time.ticks().saturating_sub(start_nanos);
let t = elapsed_nanos as f64 / 1_000_000_000.0;
Expand Down
2 changes: 1 addition & 1 deletion examples/web_video/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ fn on_tick(state: &Rc<RefCell<VideoState>>, tick: FrameTick) {
let present_bucket = (phase_target * emu_refresh_hz).floor().max(0.0) as u64;
let timecode_text = format!(
"F {:06} | PT_BUCKET {:08} | beat {:05} | timing {}",
tick.frame_index, present_bucket, beat_idx, presentation_timing_label
plan.frame_index, present_bucket, beat_idx, presentation_timing_label
);
s.ui.timecode.set_text_content(Some(&timecode_text));

Expand Down
15 changes: 9 additions & 6 deletions examples/windows_layers/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,23 +270,24 @@ unsafe extern "system" fn wnd_proc(
fn on_tick() {
let Some(s) = state_mut() else { return };

s.frame_index += 1;
let tick = make_tick(REFRESH_NS, s.frame_index, s.prev_present_time);
let frame_index = tick.frame_index;
let frame_index = s.frame_index;
s.frame_index = s.frame_index.saturating_add(1);
let tick = make_tick(REFRESH_NS, s.prev_present_time);

// Resolve previous frame's feedback.
if let Some(pending) = s.pending_feedback.take() {
let pending_frame_index = pending.plan.frame_index;
let feedback = pending.resolve(tick.prev_actual_present);
s.scheduler.observe(&feedback);
s.recorder.on_present_feedback(&PresentFeedbackEvent {
frame_index: frame_index.saturating_sub(1),
frame_index: pending_frame_index,
actual_present: tick.prev_actual_present,
missed_deadline: feedback.missed_deadline,
pacing_overrun: feedback.pacing_overrun,
});
}

let tick_event = FrameTickEvent::from(&tick);
let tick_event = FrameTickEvent::new(frame_index, &tick);
s.recorder.on_frame_tick(&tick_event);

// --- Plan phase ---
Expand All @@ -303,7 +304,9 @@ fn on_tick() {
hints,
DisplayTiming::from_tick(&tick, Duration(16_666_667)),
);
let plan = s.scheduler.plan(opportunity, FrameDemand::ANIMATION);
let plan = s
.scheduler
.plan(opportunity, FrameDemand::ANIMATION, frame_index);

let plan_end = backend::now();
s.recorder.on_phase_end(&PhaseEndEvent {
Expand Down
15 changes: 9 additions & 6 deletions examples/windows_lotta_layers/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,23 +272,24 @@ unsafe extern "system" fn wnd_proc(
fn on_tick() {
let Some(s) = state_mut() else { return };

s.frame_index += 1;
let tick = make_tick(REFRESH_NS, s.frame_index, s.prev_present_time);
let frame_index = tick.frame_index;
let frame_index = s.frame_index;
s.frame_index = s.frame_index.saturating_add(1);
let tick = make_tick(REFRESH_NS, s.prev_present_time);

// Resolve previous frame's feedback.
if let Some(pending) = s.pending_feedback.take() {
let pending_frame_index = pending.plan.frame_index;
let feedback = pending.resolve(tick.prev_actual_present);
s.scheduler.observe(&feedback);
s.recorder.on_present_feedback(&PresentFeedbackEvent {
frame_index: frame_index.saturating_sub(1),
frame_index: pending_frame_index,
actual_present: tick.prev_actual_present,
missed_deadline: feedback.missed_deadline,
pacing_overrun: feedback.pacing_overrun,
});
}

let tick_event = FrameTickEvent::from(&tick);
let tick_event = FrameTickEvent::new(frame_index, &tick);
s.recorder.on_frame_tick(&tick_event);

// --- Plan phase ---
Expand All @@ -305,7 +306,9 @@ fn on_tick() {
hints,
DisplayTiming::from_tick(&tick, Duration(16_666_667)),
);
let plan = s.scheduler.plan(opportunity, FrameDemand::ANIMATION);
let plan = s
.scheduler
.plan(opportunity, FrameDemand::ANIMATION, frame_index);

let plan_end = backend::now();
s.recorder.on_phase_end(&PhaseEndEvent {
Expand Down
18 changes: 3 additions & 15 deletions examples/winit_paced_redraw/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ struct WindowState {

struct SurfaceFrameClock {
driver: FrameDriver,
frame_index: u64,
output: OutputId,
}

Expand Down Expand Up @@ -188,12 +187,8 @@ impl WindowState {
// Plain winit does not expose a future present timestamp here, so this
// example uses a pacing-only opportunity with a conservative "submit
// by around the next refresh" boundary.
let opportunity = FrameOpportunity::pacing_only(
now,
REFRESH_INTERVAL,
self.surface_clock.frame_index,
self.surface_clock.output,
);
let opportunity =
FrameOpportunity::pacing_only(now, REFRESH_INTERVAL, self.surface_clock.output);
self.surface_clock.driver.begin_frame(opportunity).result
}

Expand Down Expand Up @@ -221,7 +216,6 @@ impl WindowState {
// content frame and ask for fresh demand if the work still
// matters.
let retry_demand = summary.demand;
self.surface_clock.frame_index += 1;
if !retry_demand.is_empty() {
self.request_frame(retry_demand);
} else if self.surface_clock.driver.has_pending_demand() {
Expand Down Expand Up @@ -263,7 +257,7 @@ impl WindowState {
.summary
.expect("pacing-only submission should resolve immediately");

if self.surface_clock.frame_index.is_multiple_of(60) {
if plan.frame_index.is_multiple_of(60) {
self.window.set_title(&format!(
"Frameclock + winit: mode={} sample={}ms x={}",
work_mode.label(),
Expand All @@ -283,11 +277,6 @@ impl WindowState {
);
}

// `frame_index` is a per-output content-frame id. Advance it after the
// active frame is submitted, not for every frame-start wake that only
// releases a queued plan.
self.surface_clock.frame_index += 1;

// If lower-priority demand was retained behind the queued frame we
// just consumed, wake winit again so the driver can plan it on a fresh
// turn. The host does not inspect or rank those demand bits itself.
Expand Down Expand Up @@ -333,7 +322,6 @@ impl SurfaceFrameClock {
// policy can still raise depth if repeated overruns show that this is
// too aggressive.
driver: FrameDriver::new(config),
frame_index: 0,
output,
}
}
Expand Down
19 changes: 11 additions & 8 deletions frameclock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Use `FrameDemand` as the host-owned reason a frame is needed. Request
dragging, `ANIMATION` while a visual timeline is active, and `BACKGROUND` for
deferrable visual work. With `FrameDriver`, call `request(demand)` when those
causes arrive; with the low-level scheduler, pass the demand to
`Scheduler::plan(opportunity, demand)`.
`Scheduler::plan(opportunity, demand, frame_index)`.

Demand also remains attached to the selected `FramePlan`. Once
`FrameBeginResult::Ready` returns, use `frame.plan().demand` to choose the app's
Expand Down Expand Up @@ -66,10 +66,13 @@ redraw requests, renderer submission, and native presentation resources. Use
`FrameDriver::next_frame_start` as one wake source to merge with app timers.
After submitting or discarding an `ActiveFrame`, hosts should request another
redraw when `FrameDriver::has_pending_demand()` is still true.
`FrameTick::frame_index` is host-owned per output and identifies one planned
content frame. Hosts using `FrameDriver` normally increment it after an
`ActiveFrame` is submitted or discarded, not every time a frame-start wake
fires while a plan is queued.

`FrameDriver` also owns the retained content-frame counter. It advances that
counter only when a planned frame becomes ready or expires, so queued plans
that are preempted or cleared do not leave diagnostic gaps. The reported
`FramePlan::frame_index` is used for submit/drop summaries. Low-level
`Scheduler` integrations own their own content-frame id and pass it explicitly
to `Scheduler::plan` and `FrameTickEvent::new`.

The lower-level `Scheduler` remains available for custom integrations. Event
structs and `FrameTimingSummaryBuilder` live under `frameclock::diagnostics`
Expand Down Expand Up @@ -117,7 +120,6 @@ driver.request(FrameDemand::ANIMATION);
let opportunity = FrameOpportunity::pacing_only(
HostTime(1_000_000),
Duration(16_666_667),
1,
OutputId(0),
);

Expand Down Expand Up @@ -198,8 +200,9 @@ The split also tightens names around timing semantics:
- `FramePlan::present_time` is now `FramePlan::target_present`.
- `FramePlan::frame_start` is now the scheduler-selected time to wake or start
app-side frame work before `FramePlan::commit_deadline`.
- `Scheduler::plan` now takes a `FrameOpportunity` plus `FrameDemand` so
display timing facts and demand remain explicit policy inputs.
- `Scheduler::plan` now takes a `FrameOpportunity`, `FrameDemand`, and explicit
frame index so display timing facts, demand, and lifecycle identity remain
explicit policy inputs.
- `FrameDemand::dominant_class` and `FrameDemand::preempts` expose the demand
ordering used by the scheduler.
- `FrameDriver` owns pending demand and queued frame-start plans for hosts that
Expand Down
Loading
Loading