#![allow(dead_code)]
#![allow(missing_docs)]
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum TaskPriority {
RealTime = 4,
High = 3,
#[default]
Normal = 2,
Low = 1,
Background = 0,
}
#[derive(Debug, Clone)]
pub struct ComputeTask {
pub name: String,
pub workgroup_size: [u32; 3],
pub dispatch_count: [u32; 3],
pub dependencies: Vec<String>,
pub priority: TaskPriority,
pub estimated_ms: f64,
}
impl ComputeTask {
pub fn new_1d(name: impl Into<String>, dispatch_x: u32) -> Self {
Self {
name: name.into(),
workgroup_size: [64, 1, 1],
dispatch_count: [dispatch_x, 1, 1],
dependencies: vec![],
priority: TaskPriority::Normal,
estimated_ms: 1.0,
}
}
pub fn new_2d(name: impl Into<String>, dispatch_x: u32, dispatch_y: u32) -> Self {
Self {
name: name.into(),
workgroup_size: [8, 8, 1],
dispatch_count: [dispatch_x, dispatch_y, 1],
dependencies: vec![],
priority: TaskPriority::Normal,
estimated_ms: 1.0,
}
}
pub fn total_workgroups(&self) -> u64 {
self.dispatch_count[0] as u64
* self.dispatch_count[1] as u64
* self.dispatch_count[2] as u64
}
pub fn total_invocations(&self) -> u64 {
self.total_workgroups()
* self.workgroup_size[0] as u64
* self.workgroup_size[1] as u64
* self.workgroup_size[2] as u64
}
pub fn depends_on(mut self, dep: impl Into<String>) -> Self {
self.dependencies.push(dep.into());
self
}
pub fn with_priority(mut self, priority: TaskPriority) -> Self {
self.priority = priority;
self
}
pub fn with_estimated_ms(mut self, ms: f64) -> Self {
self.estimated_ms = ms;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct TaskGraph {
tasks: HashMap<String, ComputeTask>,
}
impl TaskGraph {
pub fn new() -> Self {
Self::default()
}
pub fn add_task(&mut self, task: ComputeTask) {
self.tasks.insert(task.name.clone(), task);
}
pub fn remove_task(&mut self, name: &str) {
self.tasks.remove(name);
}
pub fn len(&self) -> usize {
self.tasks.len()
}
pub fn is_empty(&self) -> bool {
self.tasks.is_empty()
}
pub fn topological_sort(&self) -> Result<Vec<String>, String> {
let mut in_degree: HashMap<&str, usize> = HashMap::new();
let mut rev: HashMap<&str, Vec<&str>> = HashMap::new();
for (name, task) in &self.tasks {
in_degree.entry(name.as_str()).or_insert(0);
for dep in &task.dependencies {
if !self.tasks.contains_key(dep.as_str()) {
continue;
}
rev.entry(dep.as_str()).or_default().push(name.as_str());
*in_degree.entry(name.as_str()).or_insert(0) += 1;
}
}
let mut queue: VecDeque<&str> = in_degree
.iter()
.filter(|(_, d)| **d == 0)
.map(|(&n, _)| n)
.collect();
let mut queue_vec: Vec<&str> = queue.drain(..).collect();
queue_vec.sort();
queue.extend(queue_vec);
let mut order = Vec::new();
while let Some(name) = queue.pop_front() {
order.push(name.to_owned());
if let Some(dependents) = rev.get(name) {
let mut next: Vec<&str> = dependents
.iter()
.filter_map(|&d| {
let deg = in_degree.get_mut(d)?;
*deg -= 1;
if *deg == 0 { Some(d) } else { None }
})
.collect();
next.sort();
queue.extend(next);
}
}
if order.len() != self.tasks.len() {
let cycle_node = self
.tasks
.keys()
.find(|n| !order.contains(*n))
.cloned()
.unwrap_or_else(|| "unknown".to_owned());
Err(cycle_node)
} else {
Ok(order)
}
}
pub fn critical_path(&self) -> Vec<String> {
let order = match self.topological_sort() {
Ok(o) => o,
Err(_) => return vec![],
};
let mut eft: HashMap<&str, f64> = HashMap::new();
let mut pred: HashMap<&str, &str> = HashMap::new();
for name in &order {
let task = &self.tasks[name.as_str()];
let dep_max = task
.dependencies
.iter()
.filter_map(|d| eft.get(d.as_str()).copied())
.fold(0.0f64, f64::max);
let ef = dep_max + task.estimated_ms;
eft.insert(name.as_str(), ef);
if let Some(best_pred) = task
.dependencies
.iter()
.filter_map(|d| {
let t = eft.get(d.as_str()).copied()?;
Some((d.as_str(), t))
})
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(d, _)| d)
{
pred.insert(name.as_str(), best_pred);
}
}
let end = order.iter().max_by(|a, b| {
eft.get(a.as_str())
.unwrap_or(&0.0)
.partial_cmp(eft.get(b.as_str()).unwrap_or(&0.0))
.expect("operation should succeed")
});
let mut path = Vec::new();
let mut cur = match end {
Some(s) => s.as_str(),
None => return vec![],
};
loop {
path.push(cur.to_owned());
match pred.get(cur) {
Some(&p) => cur = p,
None => break,
}
}
path.reverse();
path
}
pub fn has_cycle(&self) -> bool {
self.topological_sort().is_err()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BarrierType {
ReadAfterWrite,
WriteAfterRead,
WriteAfterWrite,
}
#[derive(Debug, Clone)]
pub struct ResourceBarrier {
pub producer: String,
pub consumer: String,
pub barrier_type: BarrierType,
pub resource: String,
}
impl ResourceBarrier {
pub fn raw(
producer: impl Into<String>,
consumer: impl Into<String>,
resource: impl Into<String>,
) -> Self {
Self {
producer: producer.into(),
consumer: consumer.into(),
barrier_type: BarrierType::ReadAfterWrite,
resource: resource.into(),
}
}
pub fn war(
producer: impl Into<String>,
consumer: impl Into<String>,
resource: impl Into<String>,
) -> Self {
Self {
producer: producer.into(),
consumer: consumer.into(),
barrier_type: BarrierType::WriteAfterRead,
resource: resource.into(),
}
}
}
#[derive(Debug, Default)]
pub struct TaskScheduler {
pub barriers: Vec<ResourceBarrier>,
}
impl TaskScheduler {
pub fn new() -> Self {
Self::default()
}
pub fn add_barrier(&mut self, barrier: ResourceBarrier) {
self.barriers.push(barrier);
}
pub fn schedule(&self, graph: &TaskGraph) -> Result<Vec<String>, String> {
graph.topological_sort()
}
pub fn batch_schedule(&self, graph: &TaskGraph) -> Result<Vec<Vec<String>>, String> {
let order = self.schedule(graph)?;
let tasks = &graph.tasks;
let mut depth: HashMap<&str, usize> = HashMap::new();
for name in &order {
let task = &tasks[name.as_str()];
let d = task
.dependencies
.iter()
.filter_map(|dep| depth.get(dep.as_str()).copied())
.max()
.map(|m| m + 1)
.unwrap_or(0);
depth.insert(name.as_str(), d);
}
let max_depth = depth.values().copied().max().unwrap_or(0);
let mut batches: Vec<Vec<String>> = vec![vec![]; max_depth + 1];
for name in &order {
let d = *depth.get(name.as_str()).unwrap_or(&0);
batches[d].push(name.clone());
}
Ok(batches)
}
}
#[derive(Debug, Clone)]
pub struct WorkloadBalancer {
pub budget_ms: f64,
pending: Vec<(ComputeTask, f64)>,
}
impl WorkloadBalancer {
pub fn new(budget_ms: f64) -> Self {
Self {
budget_ms,
pending: vec![],
}
}
pub fn submit(&mut self, task: ComputeTask) {
let cost = task.estimated_ms;
self.pending.push((task, cost));
}
pub fn extract_frame_work(&mut self) -> Vec<ComputeTask> {
self.pending.sort_by(|a, b| {
b.0.priority
.cmp(&a.0.priority)
.then(b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
});
let mut remaining = self.budget_ms;
let mut this_frame = Vec::new();
let mut leftover = Vec::new();
for (task, cost) in self.pending.drain(..) {
if cost <= remaining || this_frame.is_empty() {
remaining -= cost;
this_frame.push(task);
} else {
leftover.push((task, cost));
}
}
self.pending = leftover;
this_frame
}
pub fn pending_count(&self) -> usize {
self.pending.len()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AsyncState {
Pending,
Running,
Done,
Failed(String),
}
#[derive(Debug, Clone)]
pub struct AsyncResult {
pub name: String,
pub state: AsyncState,
pub output: Vec<u8>,
}
impl AsyncResult {
pub fn is_complete(&self) -> bool {
matches!(self.state, AsyncState::Done | AsyncState::Failed(_))
}
}
#[derive(Debug, Default)]
pub struct AsyncCompute {
results: Vec<AsyncResult>,
}
impl AsyncCompute {
pub fn new() -> Self {
Self::default()
}
pub fn submit(&mut self, task: &ComputeTask) -> usize {
let idx = self.results.len();
self.results.push(AsyncResult {
name: task.name.clone(),
state: AsyncState::Pending,
output: vec![],
});
idx
}
pub fn tick(&mut self) {
for r in &mut self.results {
match r.state {
AsyncState::Pending => r.state = AsyncState::Running,
AsyncState::Running => {
r.state = AsyncState::Done;
r.output = vec![0u8; 4]; }
_ => {}
}
}
}
pub fn poll(&self, idx: usize) -> Option<&AsyncResult> {
self.results.get(idx)
}
pub fn drain_completed(&mut self) -> Vec<AsyncResult> {
let mut done = Vec::new();
let mut remaining = Vec::new();
for r in self.results.drain(..) {
if r.is_complete() {
done.push(r);
} else {
remaining.push(r);
}
}
self.results = remaining;
done
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PipelineStage {
Top,
Vertex,
Fragment,
Compute,
Transfer,
ColorAttachment,
ShaderRead,
Bottom,
}
#[derive(Debug, Clone)]
pub struct PipelineBarrier {
pub src_stage: PipelineStage,
pub dst_stage: PipelineStage,
pub label: String,
pub color_to_shader_read: bool,
}
impl PipelineBarrier {
pub fn color_attachment_to_shader_read(label: impl Into<String>) -> Self {
Self {
src_stage: PipelineStage::ColorAttachment,
dst_stage: PipelineStage::ShaderRead,
label: label.into(),
color_to_shader_read: true,
}
}
pub fn compute_to_compute(label: impl Into<String>) -> Self {
Self {
src_stage: PipelineStage::Compute,
dst_stage: PipelineStage::Compute,
label: label.into(),
color_to_shader_read: false,
}
}
pub fn is_compute_read_hazard(&self) -> bool {
self.src_stage == PipelineStage::Compute
&& matches!(
self.dst_stage,
PipelineStage::ShaderRead | PipelineStage::Fragment
)
}
}
#[derive(Debug, Clone)]
pub struct GpuTimestampQuery {
pub label: String,
pub start_ns: u64,
pub end_ns: u64,
active: bool,
}
impl GpuTimestampQuery {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
start_ns: 0,
end_ns: 0,
active: false,
}
}
pub fn begin(&mut self, now_ns: u64) {
self.start_ns = now_ns;
self.active = true;
}
pub fn end(&mut self, now_ns: u64) {
self.end_ns = now_ns;
self.active = false;
}
pub fn elapsed_us(&self) -> f64 {
(self.end_ns.saturating_sub(self.start_ns)) as f64 / 1_000.0
}
pub fn elapsed_ms(&self) -> f64 {
self.elapsed_us() / 1_000.0
}
pub fn is_active(&self) -> bool {
self.active
}
}
#[derive(Debug, Default)]
pub struct TimestampPool {
queries: Vec<GpuTimestampQuery>,
}
impl TimestampPool {
pub fn new() -> Self {
Self::default()
}
pub fn begin(&mut self, label: impl Into<String>, now_ns: u64) -> usize {
let mut q = GpuTimestampQuery::new(label);
q.begin(now_ns);
let idx = self.queries.len();
self.queries.push(q);
idx
}
pub fn end(&mut self, idx: usize, now_ns: u64) {
if let Some(q) = self.queries.get_mut(idx) {
q.end(now_ns);
}
}
pub fn elapsed_ms(&self, idx: usize) -> f64 {
self.queries.get(idx).map(|q| q.elapsed_ms()).unwrap_or(0.0)
}
pub fn total_ms(&self) -> f64 {
self.queries
.iter()
.filter(|q| !q.is_active())
.map(|q| q.elapsed_ms())
.sum()
}
pub fn reset(&mut self) {
self.queries.clear();
}
}
#[derive(Debug, Clone)]
pub struct FrameResource {
pub name: String,
pub size: usize,
pub first_use: usize,
pub last_use: usize,
pub offset: usize,
}
#[derive(Debug, Clone)]
pub struct FramePass {
pub name: String,
pub reads: Vec<String>,
pub writes: Vec<String>,
pub barriers: Vec<PipelineBarrier>,
}
impl FramePass {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
reads: vec![],
writes: vec![],
barriers: vec![],
}
}
pub fn reads(mut self, res: impl Into<String>) -> Self {
self.reads.push(res.into());
self
}
pub fn writes(mut self, res: impl Into<String>) -> Self {
self.writes.push(res.into());
self
}
pub fn barrier(mut self, b: PipelineBarrier) -> Self {
self.barriers.push(b);
self
}
}
#[derive(Debug, Default)]
pub struct FrameGraph {
passes: Vec<FramePass>,
resources: HashMap<String, FrameResource>,
}
impl FrameGraph {
pub fn new() -> Self {
Self::default()
}
pub fn add_pass(&mut self, pass: FramePass) {
let idx = self.passes.len();
for res in pass.reads.iter().chain(pass.writes.iter()) {
let e = self.resources.entry(res.clone()).or_insert(FrameResource {
name: res.clone(),
size: 0,
first_use: idx,
last_use: idx,
offset: 0,
});
if idx < e.first_use {
e.first_use = idx;
}
if idx > e.last_use {
e.last_use = idx;
}
}
self.passes.push(pass);
}
pub fn declare_resource(&mut self, name: impl Into<String>, size: usize) {
let name = name.into();
let e = self.resources.entry(name.clone()).or_insert(FrameResource {
name: name.clone(),
size: 0,
first_use: usize::MAX,
last_use: 0,
offset: 0,
});
e.size = size;
}
pub fn alias_resources(&mut self) {
let names: Vec<String> = {
let mut v: Vec<String> = self.resources.keys().cloned().collect();
v.sort();
v
};
let mut allocations: Vec<(usize, usize, usize)> = Vec::new(); let pass_count = self.passes.len();
for name in &names {
if let Some(res) = self.resources.get_mut(name) {
if res.first_use > pass_count {
continue;
}
let mut found = None;
for (off, end, sz) in &mut allocations {
if *end < res.first_use && *sz >= res.size {
found = Some(*off);
*end = res.last_use;
break;
}
}
if let Some(off) = found {
res.offset = off;
} else {
let off: usize = allocations.iter().map(|(o, _, s)| o + s).max().unwrap_or(0);
res.offset = off;
let (last_use, size) = (res.last_use, res.size);
allocations.push((off, last_use, size));
}
}
}
}
pub fn peak_memory(&self) -> usize {
self.resources
.values()
.map(|r| r.offset + r.size)
.max()
.unwrap_or(0)
}
pub fn pass_count(&self) -> usize {
self.passes.len()
}
pub fn barriers_for_pass(&self, idx: usize) -> &[PipelineBarrier] {
self.passes
.get(idx)
.map(|p| p.barriers.as_slice())
.unwrap_or(&[])
}
pub fn all_barriers(&self) -> Vec<&PipelineBarrier> {
self.passes.iter().flat_map(|p| p.barriers.iter()).collect()
}
pub fn resources_for_pass(&self, idx: usize) -> Vec<&str> {
if let Some(pass) = self.passes.get(idx) {
pass.reads
.iter()
.chain(pass.writes.iter())
.map(|s| s.as_str())
.collect::<HashSet<_>>()
.into_iter()
.collect()
} else {
vec![]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_priority_ordering() {
assert!(TaskPriority::RealTime > TaskPriority::High);
assert!(TaskPriority::High > TaskPriority::Normal);
assert!(TaskPriority::Normal > TaskPriority::Low);
assert!(TaskPriority::Low > TaskPriority::Background);
}
#[test]
fn test_compute_task_invocations_1d() {
let t = ComputeTask::new_1d("particles", 100);
assert_eq!(t.total_invocations(), 6400);
}
#[test]
fn test_compute_task_invocations_2d() {
let t = ComputeTask::new_2d("shadows", 8, 8);
assert_eq!(t.total_invocations(), 4096);
}
#[test]
fn test_compute_task_depends_on() {
let t = ComputeTask::new_1d("B", 1).depends_on("A");
assert!(t.dependencies.contains(&"A".to_owned()));
}
#[test]
fn test_compute_task_priority() {
let t = ComputeTask::new_1d("t", 1).with_priority(TaskPriority::High);
assert_eq!(t.priority, TaskPriority::High);
}
#[test]
fn test_task_graph_topo_sort_simple() {
let mut g = TaskGraph::new();
g.add_task(ComputeTask::new_1d("A", 1));
g.add_task(ComputeTask::new_1d("B", 1).depends_on("A"));
g.add_task(ComputeTask::new_1d("C", 1).depends_on("B"));
let order = g.topological_sort().unwrap();
let pos: HashMap<&str, usize> = order
.iter()
.enumerate()
.map(|(i, s)| (s.as_str(), i))
.collect();
assert!(pos["A"] < pos["B"]);
assert!(pos["B"] < pos["C"]);
}
#[test]
fn test_task_graph_topo_sort_diamond() {
let mut g = TaskGraph::new();
g.add_task(ComputeTask::new_1d("A", 1));
g.add_task(ComputeTask::new_1d("B", 1).depends_on("A"));
g.add_task(ComputeTask::new_1d("C", 1).depends_on("A"));
g.add_task(ComputeTask::new_1d("D", 1).depends_on("B").depends_on("C"));
let order = g.topological_sort().unwrap();
assert_eq!(order.len(), 4);
}
#[test]
fn test_task_graph_cycle_detection() {
let mut g = TaskGraph::new();
g.add_task(ComputeTask::new_1d("A", 1).depends_on("B"));
g.add_task(ComputeTask::new_1d("B", 1).depends_on("A"));
assert!(g.has_cycle());
}
#[test]
fn test_task_graph_critical_path() {
let mut g = TaskGraph::new();
g.add_task(ComputeTask::new_1d("A", 1).with_estimated_ms(1.0));
g.add_task(
ComputeTask::new_1d("B", 1)
.depends_on("A")
.with_estimated_ms(2.0),
);
g.add_task(ComputeTask::new_1d("C", 1).with_estimated_ms(10.0));
let cp = g.critical_path();
assert!(cp.contains(&"C".to_owned()));
}
#[test]
fn test_task_graph_empty_topo() {
let g = TaskGraph::new();
let order = g.topological_sort().unwrap();
assert!(order.is_empty());
}
#[test]
fn test_scheduler_schedule() {
let mut g = TaskGraph::new();
g.add_task(ComputeTask::new_1d("X", 1));
g.add_task(ComputeTask::new_1d("Y", 1).depends_on("X"));
let sched = TaskScheduler::new();
let order = sched.schedule(&g).unwrap();
assert_eq!(order.len(), 2);
}
#[test]
fn test_scheduler_batch_schedule() {
let mut g = TaskGraph::new();
g.add_task(ComputeTask::new_1d("A", 1));
g.add_task(ComputeTask::new_1d("B", 1));
g.add_task(ComputeTask::new_1d("C", 1).depends_on("A").depends_on("B"));
let sched = TaskScheduler::new();
let batches = sched.batch_schedule(&g).unwrap();
assert!(batches[0].len() >= 2);
assert!(batches.len() >= 2);
}
#[test]
fn test_resource_barrier_raw() {
let b = ResourceBarrier::raw("write_task", "read_task", "position_buffer");
assert_eq!(b.barrier_type, BarrierType::ReadAfterWrite);
assert_eq!(b.resource, "position_buffer");
}
#[test]
fn test_resource_barrier_war() {
let b = ResourceBarrier::war("reader", "writer", "depth");
assert_eq!(b.barrier_type, BarrierType::WriteAfterRead);
}
#[test]
fn test_workload_balancer_respects_budget() {
let mut wb = WorkloadBalancer::new(10.0);
wb.submit(ComputeTask::new_1d("A", 1).with_estimated_ms(3.0));
wb.submit(ComputeTask::new_1d("B", 1).with_estimated_ms(4.0));
wb.submit(ComputeTask::new_1d("C", 1).with_estimated_ms(6.0));
let frame = wb.extract_frame_work();
let total: f64 = frame.iter().map(|t| t.estimated_ms).sum();
assert!(total <= 10.0 + 6.0);
}
#[test]
fn test_workload_balancer_priority_order() {
let mut wb = WorkloadBalancer::new(5.0);
wb.submit(
ComputeTask::new_1d("low", 1)
.with_priority(TaskPriority::Low)
.with_estimated_ms(2.0),
);
wb.submit(
ComputeTask::new_1d("rt", 1)
.with_priority(TaskPriority::RealTime)
.with_estimated_ms(2.0),
);
let frame = wb.extract_frame_work();
assert_eq!(frame[0].name, "rt");
}
#[test]
fn test_workload_balancer_pending_count() {
let mut wb = WorkloadBalancer::new(1.0);
for i in 0..5 {
wb.submit(ComputeTask::new_1d(format!("t{i}"), 1).with_estimated_ms(1.0));
}
wb.extract_frame_work();
assert!(wb.pending_count() < 5);
}
#[test]
fn test_async_compute_submit_poll() {
let mut ac = AsyncCompute::new();
let task = ComputeTask::new_1d("sim", 64);
let idx = ac.submit(&task);
let r = ac.poll(idx).unwrap();
assert_eq!(r.state, AsyncState::Pending);
}
#[test]
fn test_async_compute_tick_to_done() {
let mut ac = AsyncCompute::new();
let task = ComputeTask::new_1d("sim", 1);
let idx = ac.submit(&task);
ac.tick(); ac.tick(); assert_eq!(ac.poll(idx).unwrap().state, AsyncState::Done);
}
#[test]
fn test_async_compute_drain_completed() {
let mut ac = AsyncCompute::new();
let t = ComputeTask::new_1d("t", 1);
ac.submit(&t);
ac.tick();
ac.tick();
let done = ac.drain_completed();
assert_eq!(done.len(), 1);
assert!(ac.poll(0).is_none()); }
#[test]
fn test_pipeline_barrier_color_to_shader_read() {
let b = PipelineBarrier::color_attachment_to_shader_read("gbuffer");
assert!(b.color_to_shader_read);
assert_eq!(b.src_stage, PipelineStage::ColorAttachment);
assert_eq!(b.dst_stage, PipelineStage::ShaderRead);
}
#[test]
fn test_pipeline_barrier_compute_to_compute() {
let b = PipelineBarrier::compute_to_compute("particles");
assert_eq!(b.src_stage, PipelineStage::Compute);
assert!(!b.is_compute_read_hazard()); }
#[test]
fn test_pipeline_barrier_compute_read_hazard() {
let b = PipelineBarrier {
src_stage: PipelineStage::Compute,
dst_stage: PipelineStage::ShaderRead,
label: "test".to_owned(),
color_to_shader_read: false,
};
assert!(b.is_compute_read_hazard());
}
#[test]
fn test_timestamp_query_elapsed() {
let mut q = GpuTimestampQuery::new("render");
q.begin(1_000_000); q.end(2_000_000); assert!((q.elapsed_ms() - 1.0).abs() < 1e-6);
}
#[test]
fn test_timestamp_query_is_active() {
let mut q = GpuTimestampQuery::new("x");
assert!(!q.is_active());
q.begin(0);
assert!(q.is_active());
q.end(100);
assert!(!q.is_active());
}
#[test]
fn test_timestamp_pool_total() {
let mut pool = TimestampPool::new();
let i0 = pool.begin("a", 0);
pool.end(i0, 1_000_000);
let i1 = pool.begin("b", 0);
pool.end(i1, 2_000_000);
let total = pool.total_ms();
assert!((total - 3.0).abs() < 1e-6, "total={total}");
}
#[test]
fn test_timestamp_pool_reset() {
let mut pool = TimestampPool::new();
pool.begin("x", 0);
pool.reset();
assert!((pool.total_ms()).abs() < 1e-10);
}
#[test]
fn test_frame_graph_add_pass() {
let mut fg = FrameGraph::new();
fg.add_pass(FramePass::new("gbuffer").writes("color").writes("depth"));
fg.add_pass(
FramePass::new("lighting")
.reads("color")
.reads("depth")
.writes("hdr"),
);
assert_eq!(fg.pass_count(), 2);
}
#[test]
fn test_frame_graph_resource_lifetime() {
let mut fg = FrameGraph::new();
fg.declare_resource("color", 1024 * 1024 * 4);
fg.add_pass(FramePass::new("p0").writes("color"));
fg.add_pass(FramePass::new("p1").reads("color"));
let res = &fg.resources["color"];
assert_eq!(res.first_use, 0);
assert_eq!(res.last_use, 1);
}
#[test]
fn test_frame_graph_aliasing() {
let mut fg = FrameGraph::new();
fg.declare_resource("A", 1024);
fg.declare_resource("B", 1024);
fg.add_pass(FramePass::new("p0").writes("A"));
fg.add_pass(FramePass::new("p1").reads("A"));
fg.add_pass(FramePass::new("p2").writes("B"));
fg.alias_resources();
let peak = fg.peak_memory();
assert!(peak > 0);
}
#[test]
fn test_frame_graph_barriers() {
let mut fg = FrameGraph::new();
fg.add_pass(
FramePass::new("render")
.barrier(PipelineBarrier::color_attachment_to_shader_read("test")),
);
let barriers = fg.barriers_for_pass(0);
assert_eq!(barriers.len(), 1);
}
#[test]
fn test_frame_graph_all_barriers() {
let mut fg = FrameGraph::new();
fg.add_pass(FramePass::new("p0").barrier(PipelineBarrier::compute_to_compute("c0")));
fg.add_pass(FramePass::new("p1").barrier(PipelineBarrier::compute_to_compute("c1")));
assert_eq!(fg.all_barriers().len(), 2);
}
}