#![allow(dead_code)]
use std::{
cmp::Ordering,
collections::VecDeque,
hash::{Hash, Hasher},
ops::Add,
ops::Div,
ops::DivAssign,
ops::Mul,
};
use wgpu::util::DeviceExt;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn monotonic_now_seconds() -> f64 {
use std::sync::OnceLock;
use std::time::Instant;
static START: OnceLock<Instant> = OnceLock::new();
START.get_or_init(Instant::now).elapsed().as_secs_f64()
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn monotonic_now_seconds() -> f64 {
web_sys::window()
.and_then(|w| w.performance())
.map(|p| p.now() / 1000.0)
.unwrap_or(0.0)
}
pub(crate) struct DrawingContext<'a> {
pub(crate) rpass: &'a mut wgpu::RenderPass<'a>,
pub(crate) pipeline: &'a wgpu::RenderPipeline,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Debug)]
#[allow(dead_code)]
pub(crate) struct UVTransform {
pub(crate) uv_offset: [f32; 2],
pub(crate) uv_scale: [f32; 2],
pub(crate) tint: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Debug)]
#[allow(dead_code)]
pub(crate) struct Vertex {
pub(crate) position: [f32; 2],
pub(crate) tex_coords: [f32; 2], }
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
#[allow(dead_code)]
pub(crate) struct TransformUniform {
pub(crate) transform: [[f32; 4]; 4], }
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
#[allow(dead_code)]
pub(crate) struct InstanceRaw {
pub(crate) model: [[f32; 4]; 4],
pub(crate) uv_offset: [f32; 2],
pub(crate) uv_scale: [f32; 2],
pub(crate) msdf_px_range: f32,
pub(crate) _msdf_pad: [f32; 3],
pub(crate) tint: [f32; 4],
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
#[allow(dead_code)]
pub(crate) struct RectInstanceRaw {
pub(crate) model: [[f32; 4]; 4],
pub(crate) color: [f32; 4],
pub(crate) corner_radius_px: f32,
pub(crate) border_thickness_px: f32,
pub(crate) _pad0: [f32; 2],
pub(crate) border_color: [f32; 4],
pub(crate) rect_size_px: [f32; 2],
pub(crate) _pad1: [f32; 2],
pub(crate) _pad2: [f32; 4],
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
#[allow(dead_code)]
pub(crate) struct GlowInstanceRaw {
pub(crate) model: [[f32; 4]; 4],
pub(crate) color: [f32; 4],
pub(crate) rect_size_px: [f32; 2],
pub(crate) corner_radius_px: f32,
pub(crate) glow_radius_px: f32,
pub(crate) sigma: f32,
pub(crate) max_alpha: f32,
pub(crate) mode: f32,
pub(crate) border_width: f32,
pub(crate) _pad: [f32; 4],
}
#[derive(Debug, Clone, Copy)]
pub struct Size {
pub width: f32,
pub height: f32,
}
impl Size {
pub fn new(width: f32, height: f32) -> Self {
Self { width, height }
}
}
pub fn create_unit_quad_buffers(device: &wgpu::Device) -> (wgpu::Buffer, wgpu::Buffer) {
let vertices: [Vertex; 4] = [
Vertex {
position: [0.0, 0.0],
tex_coords: [0.0, 0.0],
},
Vertex {
position: [1.0, 0.0],
tex_coords: [1.0, 0.0],
},
Vertex {
position: [1.0, 1.0],
tex_coords: [1.0, 1.0],
},
Vertex {
position: [0.0, 1.0],
tex_coords: [0.0, 1.0],
},
];
let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("unit-quad-vertices"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let ibuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("unit-quad-indices"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
(vbuf, ibuf)
}
pub fn create_centered_quad_buffers(device: &wgpu::Device) -> (wgpu::Buffer, wgpu::Buffer) {
let vertices: [Vertex; 4] = [
Vertex {
position: [-1.0, -1.0],
tex_coords: [0.0, 0.0],
},
Vertex {
position: [1.0, -1.0],
tex_coords: [1.0, 0.0],
},
Vertex {
position: [1.0, 1.0],
tex_coords: [1.0, 1.0],
},
Vertex {
position: [-1.0, 1.0],
tex_coords: [0.0, 1.0],
},
];
let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("centered-quad-vertices"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let ibuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("centered-quad-indices"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
(vbuf, ibuf)
}
impl Add<f32> for Size {
type Output = Size;
fn add(self, rhs: f32) -> Self::Output {
Size {
width: self.width + rhs,
height: self.height + rhs,
}
}
}
impl Mul<f32> for Size {
type Output = Size;
fn mul(self, rhs: f32) -> Self::Output {
Size {
width: self.width * rhs,
height: self.height * rhs,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Position {
pub x: f32,
pub y: f32,
}
impl Default for Position {
fn default() -> Self {
Position { x: 0.0, y: 0.0 }
}
}
impl PartialEq for Position {
fn eq(&self, other: &Self) -> bool {
self.x == other.x && self.y == other.y
}
}
impl DivAssign<f32> for Position {
fn div_assign(&mut self, rhs: f32) {
self.x /= rhs;
self.y /= rhs;
}
}
impl Div<f32> for Position {
type Output = Position;
fn div(self, factor: f32) -> Self::Output {
Position {
x: self.x / factor,
y: self.y / factor,
}
}
}
impl Eq for Position {}
impl Hash for Position {
fn hash<H: Hasher>(&self, state: &mut H) {
let precision = 100.0; let x = (self.x * precision).round() as i32;
let y = (self.y * precision).round() as i32;
x.hash(state);
y.hash(state);
}
}
impl Mul<f32> for Position {
type Output = Position;
fn mul(self, factor: f32) -> Self::Output {
Position {
x: self.x * factor,
y: self.y * factor,
}
}
}
impl Add<f32> for Position {
type Output = Position;
fn add(self, other: f32) -> Self::Output {
Position {
x: self.x + other,
y: self.y + other,
}
}
}
impl Add<Position> for Position {
type Output = Position;
fn add(self, rhs: Position) -> Self::Output {
Position {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
impl std::ops::Sub<Position> for Position {
type Output = Position;
fn sub(self, rhs: Position) -> Self::Output {
Position {
x: self.x - rhs.x,
y: self.y - rhs.y,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Rectangle {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl Rectangle {
pub fn padded_contains(&self, position: Position, padding: f32) -> bool {
position.x >= self.x + padding
&& position.x <= self.x + self.width - padding
&& position.y >= self.y + padding
&& position.y <= self.y + self.height - padding
}
pub fn contains(&self, position: Position) -> bool {
position.x >= self.x
&& position.x <= self.x + self.width
&& position.y >= self.y
&& position.y <= self.y + self.height
}
pub fn pos(&self) -> Position {
Position {
x: self.x,
y: self.y,
}
}
pub fn size(&self) -> Size {
Size {
width: self.width,
height: self.height,
}
}
pub fn set_pos(&mut self, pos: Position) {
self.x = pos.x;
self.y = pos.y;
}
pub fn pad(rec: &Rectangle, padding: f32) -> Rectangle {
Rectangle::new(
rec.x - padding,
rec.y - padding,
rec.width + (padding * 2.0),
rec.height + (padding * 2.0),
)
}
pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn new_square(x: f32, y: f32, side_length: f32) -> Self {
Self {
x,
y,
width: side_length,
height: side_length,
}
}
}
impl Add<f32> for Rectangle {
type Output = Rectangle;
fn add(self, other: f32) -> Self::Output {
Rectangle::new(self.x, self.y, self.width + other, self.height + other)
}
}
impl Mul<f32> for Rectangle {
type Output = Rectangle;
fn mul(self, factor: f32) -> Self::Output {
Rectangle::new(self.x, self.y, self.width * factor, self.height * factor)
}
}
impl Div<f32> for Rectangle {
type Output = Rectangle;
fn div(self, factor: f32) -> Self::Output {
Rectangle::new(self.x, self.y, self.width / factor, self.height / factor)
}
}
impl PartialEq for Rectangle {
fn eq(&self, other: &Self) -> bool {
const EPSILON: f32 = 1e-6;
(self.x - other.x).abs() < EPSILON
&& (self.y - other.y).abs() < EPSILON
&& (self.width - other.width).abs() < EPSILON
&& (self.height - other.height).abs() < EPSILON
}
}
#[derive(Copy, Clone, Debug, Default)]
pub struct MouseInfo {
pub is_rmb_clicked: bool,
pub is_lmb_clicked: bool,
pub is_mmb_clicked: bool,
pub mouse_pos: Position,
pub scroll_dx: f32,
pub scroll_dy: f32,
}
#[derive(Debug)]
pub struct FrameTimeMetrics {
buffer: VecDeque<f32>, capacity: usize,
last_report_secs: f64,
report_period_secs: f32,
}
impl FrameTimeMetrics {
pub fn new(capacity: usize, report_period_secs: f32) -> Self {
Self {
buffer: VecDeque::with_capacity(capacity),
capacity,
last_report_secs: monotonic_now_seconds(),
report_period_secs,
}
}
pub fn record(&mut self, delta_seconds: f32) {
if self.buffer.len() == self.capacity {
self.buffer.pop_front();
}
self.buffer.push_back(delta_seconds);
}
fn percentile(sorted_ms: &[f32], p: f32) -> f32 {
if sorted_ms.is_empty() {
return 0.0;
}
let clamped = p.clamp(0.0, 100.0);
let rank = (clamped / 100.0) * ((sorted_ms.len() - 1) as f32);
let lower = rank.floor() as usize;
let upper = rank.ceil() as usize;
match lower.cmp(&upper) {
Ordering::Equal => sorted_ms[lower],
_ => {
let w = rank - (lower as f32);
sorted_ms[lower] * (1.0 - w) + sorted_ms[upper] * w
}
}
}
pub fn stats(&self) -> Option<(f32, f32, f32, f32)> {
if self.buffer.is_empty() {
return None;
}
let mut ms: Vec<f32> = self.buffer.iter().map(|s| s * 1000.0).collect();
ms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let p50 = Self::percentile(&ms, 50.0);
let p95 = Self::percentile(&ms, 95.0);
let p99 = Self::percentile(&ms, 99.0);
let avg_dt = self.buffer.iter().copied().sum::<f32>() / (self.buffer.len() as f32);
let avg_fps = if avg_dt > 0.0 { 1.0 / avg_dt } else { 0.0 };
Some((p50, p95, p99, avg_fps))
}
pub fn maybe_report(&mut self) -> Option<String> {
let now = monotonic_now_seconds();
let elapsed = (now - self.last_report_secs).max(0.0) as f32;
if elapsed < self.report_period_secs {
return None;
}
self.last_report_secs = now;
self.stats().map(|(p50, p95, p99, fps)| {
format!(
"frame_metrics p50_ms={:.2} p95_ms={:.2} p99_ms={:.2} avg_fps={:.1} window_frames={}",
p50, p95, p99, fps, self.buffer.len()
)
})
}
}
#[cfg(test)]
mod tests {
use super::{Position, Rectangle};
#[test]
fn padded_contains_shrinks_hit_region_uniformly() {
let rect = Rectangle::new(10.0, 20.0, 100.0, 60.0);
assert!(!rect.padded_contains(Position { x: 10.0, y: 50.0 }, 8.0));
assert!(!rect.padded_contains(Position { x: 50.0, y: 20.0 }, 8.0));
assert!(rect.padded_contains(Position { x: 60.0, y: 50.0 }, 8.0));
}
}