#![cfg(feature = "profile")]
mod common;
use crate::common::shared;
use dynamis_gpu::{
BindingKind, BindingSpec, ComputeProgram, ComputeRecorder, GpuPassTiming, GpuRequest, GpuTimer,
PipelineHandle, SubmissionEncoder, WarmupBudget,
};
use wgpu::{BindGroupEntry, BufferUsages};
const BURN_KERNEL: &str = r#"
@group(0) @binding(0) var<storage, read_write> sink: array<u32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3u) {
var value = gid.x;
for (var i = 0u; i < 512u; i = i + 1u) {
value = value * 1664525u + 1013904223u;
}
sink[gid.x % 4096u] = value;
}
"#;
fn burn_pipeline() -> PipelineHandle {
let context = shared();
let bindings = [BindingSpec {
binding: 0,
kind: BindingKind::ReadWriteStorage,
}];
let pipeline = context.declare(ComputeProgram::new(
"burn",
BURN_KERNEL,
"main",
&[&bindings[..]],
));
context.warmup(WarmupBudget::All);
pipeline
}
fn burn_group(pipeline: &PipelineHandle) -> wgpu::BindGroup {
let context = shared();
let sink = context.device().create_buffer(&wgpu::BufferDescriptor {
label: Some("burn sink"),
size: 4096 * 4,
usage: BufferUsages::STORAGE,
mapped_at_creation: false,
});
pipeline.create_bind_group(
context.device(),
0,
&[BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &sink,
offset: 0,
size: None,
}),
}],
)
}
#[test]
fn the_default_request_negotiates_timestamp_queries() {
assert!(
shared().supports_pass_timing(),
"{:?} must expose timestamp queries to a profile build",
shared().adapter_info()
);
assert!(
!GpuRequest::PROFILING_FEATURES.is_empty(),
"a profile build must ask the device for the timing features"
);
}
#[test]
fn pass_timings_split_a_step_by_stage() {
let context = shared();
let labels: &'static [&'static str] = &["idle", "busy"];
let mut timer = GpuTimer::new(
context.device(),
labels,
context.timestamp_period_ns(),
"test",
);
let pipeline = burn_pipeline();
let group = burn_group(&pipeline);
let mut reported: Option<Vec<GpuPassTiming>> = None;
for _ in 0..6 {
let mut encoder = SubmissionEncoder::new(context.device(), "timing frame");
{
let mut idle = ComputeRecorder::begin_timed(
&mut encoder,
labels[0],
Some(timer.writes(0)),
context.workgroups_per_row(),
);
idle.record(pipeline.pipeline(), &[], 0);
}
{
let mut busy = ComputeRecorder::begin_timed(
&mut encoder,
labels[1],
Some(timer.writes(1)),
context.workgroups_per_row(),
);
busy.record(pipeline.pipeline(), &[&group], 4096);
}
if let Some(timings) = timer.capture(&mut encoder, &[true, true]) {
reported = Some(timings);
encoder.submit(context.queue());
break;
}
encoder.submit(context.queue());
}
let timings = reported.expect("a double-buffered timer must report a completed frame");
assert_eq!(timings.len(), 2);
for timing in &timings {
assert!(
timing.nanoseconds >= 0.0,
"{} measured a negative duration",
timing.label
);
}
assert!(
timings[1].nanoseconds > 0.0,
"a dispatched pass must record measurable time"
);
assert!(
timings[1].nanoseconds > timings[0].nanoseconds,
"per-pass attribution must separate an empty pass ({}) from real work ({})",
timings[0].nanoseconds,
timings[1].nanoseconds
);
}
#[test]
fn a_pass_that_never_opened_is_never_reported() {
let context = shared();
let labels: &'static [&'static str] = &["idle", "busy"];
let mut timer = GpuTimer::new(
context.device(),
labels,
context.timestamp_period_ns(),
"skipped",
);
let pipeline = burn_pipeline();
let group = burn_group(&pipeline);
let mut reported: Option<Vec<GpuPassTiming>> = None;
for _ in 0..6 {
let mut encoder = SubmissionEncoder::new(context.device(), "skipped frame");
{
let mut busy = ComputeRecorder::begin_timed(
&mut encoder,
labels[1],
Some(timer.writes(1)),
context.workgroups_per_row(),
);
busy.record(pipeline.pipeline(), &[&group], 4096);
}
if let Some(timings) = timer.capture(&mut encoder, &[false, true]) {
reported = Some(timings);
encoder.submit(context.queue());
break;
}
encoder.submit(context.queue());
}
let timings = reported.expect("a double-buffered timer must report a completed frame");
assert_eq!(
timings
.iter()
.map(|timing| timing.label)
.collect::<Vec<_>>(),
&["busy"],
"a pass that never opened must stay out of the profile"
);
}
#[test]
fn timing_slot_out_of_range_is_rejected() {
let context = shared();
let labels: &'static [&'static str] = &["only"];
let timer = GpuTimer::new(
context.device(),
labels,
context.timestamp_period_ns(),
"bounds",
);
assert_eq!(timer.pass_count(), 1);
let error = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| timer.writes(1)));
assert!(error.is_err(), "an unknown pass slot must abort");
}
#[test]
fn a_timer_requires_at_least_one_pass() {
let context = shared();
let error = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
GpuTimer::new(context.device(), &[], 1.0, "empty")
}));
assert!(error.is_err(), "a zero-pass timer is meaningless");
}
#[test]
fn milliseconds_match_the_reported_nanoseconds() {
let timing = GpuPassTiming {
label: "pass",
nanoseconds: 2_500_000.0,
};
assert_eq!(timing.milliseconds(), 2.5);
}