alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
root crates client src gpu pacing.rs
//! Frame pacing from `VK_EXT_present_timing` feedback: frames are shown for an equal number of
//! refresh cycles and each frame is simulated at the time it will be displayed. For low latency,
//! rendering starts as late as the frame's measured cost and the compositor's learned deadline
//! allow, and that deadline is continually probed for a shorter one.
use std::collections::VecDeque;
use std::time::{Duration, Instant};
/// A timestamp of a time domain and the `Instant` it was taken at.
pub(super) type Clock = (u64, Instant);
/// The pacer's nanoseconds at its creation, far from 0 so that earlier times stay positive.
const EPOCH_NANOS: u64 = 1 << 62;
/// Presents after which the compositor's latency is learned, as it starts out with the window
/// opening and with the frames queued before the refresh rate was known.
const WARMUP_PRESENTS: u64 = 120;
/// Fraction of a refresh cycle the margin shrinks by with each frame shown on time.
const MARGIN_DECAY: u64 = 1024;
struct Planned {
id: u64,
start: u64,
/// The planned display time, if aligned to a refresh cycle.
target: Option<u64>,
}
/// Times are nanoseconds on the pacer's own clock.
pub(super) struct Pacer {
epoch: Clock,
/// The duration of a refresh cycle, or its minimum with variable refresh rates. 0 if unknown.
refresh: u64,
/// The step in which display times can change.
quantum: u64,
/// The present id and display time of the latest displayed frame.
anchor: Option<(u64, u64)>,
/// The time each frame is displayed for.
interval: u64,
/// The present id and display time of the frame being rendered.
current: (u64, u64),
/// Time from starting a frame until its present queue operations end, as a decaying maximum.
cost: u64,
/// How long before its target a frame is handed to the presentation engine, which covers the
/// compositor's latency.
margin: u64,
/// Whether presents wait on the CPU, as the presentation engine cannot hold them until their
/// target.
hold: bool,
planned: VecDeque<Planned>,
}
impl Pacer {
pub fn new(hold: bool) -> Self {
Self {
epoch: (EPOCH_NANOS, Instant::now()),
refresh: 0,
quantum: 1,
anchor: None,
interval: 0,
current: (0, 0),
cost: 0,
margin: 0,
hold,
planned: VecDeque::new(),
}
}
pub fn refresh_known(&self) -> bool {
self.refresh != 0
}
/// Takes `VkSwapchainTimingPropertiesEXT::refreshDuration` and `refreshInterval`.
pub fn set_refresh(&mut self, duration: u64, interval: u64) {
if duration != self.refresh {
self.margin = duration / 4;
}
self.refresh = duration;
self.quantum = match interval {
u64::MAX => 1,
0 => duration.max(1),
interval => interval,
};
}
/// Plans frame `id`, shown for at least `min_interval`, and returns when it will be displayed.
pub fn begin(&mut self, id: u64, min_interval: Duration) -> Option<Instant> {
if !self.refresh_known() {
return None;
}
let min_interval = (min_interval.as_nanos() as u64).max(self.refresh);
let cycles = min_interval
.saturating_sub(self.quantum / 16)
.div_ceil(self.quantum);
self.interval = cycles.max(1) * self.quantum;
let now = nanos(self.epoch, Instant::now());
let earliest = now + self.cost;
let planned = self.plan(id);
let target = if planned >= earliest {
self.snap(planned, self.quantum / 2)
} else {
self.snap(earliest, self.quantum - 1)
};
if self.planned.back().is_some_and(|p| p.id == id) {
self.planned.pop_back();
}
self.planned.push_back(Planned {
id,
start: now,
target: self.anchor.map(|_| target),
});
self.current = (id, target);
Some(instant(self.epoch, target))
}
/// The display time planned for frame `id`, if it is the frame being rendered.
pub fn target(&self, id: u64) -> Option<Instant> {
let (current_id, target) = self.current;
(current_id == id && target != 0).then(|| instant(self.epoch, target))
}
/// When to hand frame `id` to the presentation engine, if it would be shown early before.
/// FIFO presentation shows a frame in the first free refresh cycle once it is ready, which
/// comes before the target when frames are shown for longer than a cycle.
pub fn present_time(&self, id: u64) -> Option<Instant> {
let target = self.target(id)?;
self.holds()
.then(|| target - Duration::from_nanos(self.margin))
}
/// When to start rendering the frame after the current one.
pub fn wake(&self) -> Option<Instant> {
let (id, target) = self.current;
(target != 0).then(|| {
let next = self.snap(self.plan(id + 1), self.quantum / 2);
instant(self.epoch, next.saturating_sub(self.cost + self.margin))
})
}
/// Excludes time the frame being rendered spent waiting for the GPU or presentation engine
/// from its cost.
pub fn waited(&mut self, duration: Duration) {
let current = self.current.0;
if let Some(frame) = self.planned.back_mut().filter(|p| p.id == current) {
frame.start += duration.as_nanos() as u64;
}
}
/// Takes when frame `id` finished its present queue operations and when it was displayed.
pub fn feedback(&mut self, id: u64, queue_end: Option<Instant>, displayed: Option<Instant>) {
let [queue_end, displayed] =
[queue_end, displayed].map(|t| t.map(|t| nanos(self.epoch, t)));
while self.planned.front().is_some_and(|p| p.id < id) {
self.planned.pop_front();
}
let frame = self.planned.pop_front_if(|p| p.id == id);
if let Some(frame) = &frame
&& let Some(queue_end) = queue_end
{
let cost = queue_end.saturating_sub(frame.start);
self.cost = cost.max(self.cost - self.cost / 16);
}
let Some(displayed) = displayed else {
return;
};
// A frame handed over on schedule that misses its target although the refresh cycle was
// free, rather than queued behind a late predecessor, shows how long the compositor
// takes: more than its lead plus lateness, minus the refresh cycle it missed. Held frames
// must also not be early, so their lead moves half a refresh cycle past a failure, into
// the window between. Frames on time let the margin shrink towards the deadline.
if id > WARMUP_PRESENTS
&& let Some(target) = frame.and_then(|frame| frame.target)
&& let Some(queue_end) = queue_end.filter(|&queue_end| queue_end <= target)
{
let lead = target - queue_end;
let step = self.refresh / if self.holds() { 2 } else { 4 };
if displayed > target + self.refresh / 2 {
let free = self
.anchor
.is_some_and(|(_, previous)| previous + self.refresh / 2 <= target);
if free && lead + self.refresh / 4 >= self.margin {
let needed = (lead + (displayed - target) + step).saturating_sub(self.refresh);
self.margin = needed.max(self.margin).min(3 * self.refresh);
}
} else if displayed + self.refresh / 2 < target {
let allowed = (lead + self.refresh).saturating_sub(target - displayed + step);
self.margin = self.margin.min(allowed);
} else {
self.margin = self.margin.saturating_sub(self.refresh / MARGIN_DECAY);
}
}
self.anchor = Some((id, displayed));
}
/// The display time of frame `id` that keeps the cadence of earlier frames.
fn plan(&self, id: u64) -> u64 {
let (current_id, current) = self.current;
let cadence = current + id.saturating_sub(current_id) * self.interval;
self.anchor.map_or(cadence, |(anchor_id, displayed)| {
cadence.max(displayed + id.saturating_sub(anchor_id) * self.interval)
})
}
fn holds(&self) -> bool {
self.hold && self.interval >= self.refresh + self.quantum
}
/// Aligns `time` to a refresh cycle, the next one once `time` is `quantum - round` past one.
fn snap(&self, time: u64, round: u64) -> u64 {
let Some((_, displayed)) = self.anchor else {
return time;
};
let cycles = (time.saturating_sub(displayed) + round) / self.quantum;
displayed + cycles * self.quantum
}
}
pub(super) fn nanos((base, epoch): Clock, instant: Instant) -> u64 {
match instant.checked_duration_since(epoch) {
Some(elapsed) => base + elapsed.as_nanos() as u64,
None => base.saturating_sub((epoch - instant).as_nanos() as u64),
}
}
pub(super) fn instant((base, epoch): Clock, nanos: u64) -> Instant {
if nanos >= base {
epoch + Duration::from_nanos(nanos - base)
} else {
epoch
.checked_sub(Duration::from_nanos(base - nanos))
.unwrap_or(epoch)
}
}