Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
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
Download file

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)
    }
}