alpha2-render 0.5.0

Render graph and effect registry for alpha2.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
//! Stable, host-readable renderer diagnostics.

use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use alpha2_gpu::{Capabilities, CapabilityTier};

use crate::{
    PipelineCacheStats, ReadbackBufferPoolStats, TexturePoolStats, UniformBufferPoolStats,
    UploadTexturePoolStats,
};

/// Source of one pass duration.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TimingSource {
    /// Hardware timestamp query resolved by wgpu.
    GpuTimestamp,
    /// Wall-clock time spent encoding one pass on the CPU.
    CpuEncoding,
}

/// Most recent timing for one named render pass.
///
/// Pass timing is opt-in; see [`crate::Renderer::set_pass_timing_enabled`].
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct PassTiming {
    /// Stable pass label.
    pub label: &'static str,
    /// Duration in nanoseconds.
    pub nanoseconds: u64,
    /// Measurement source.
    pub source: TimingSource,
}

/// Adapter metadata recorded beside reproducible benchmark samples.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AdapterDiagnostics {
    /// Platform adapter name.
    pub name: String,
    /// Selected graphics backend.
    pub backend: String,
    /// Driver name.
    pub driver: String,
    /// Driver version/details.
    pub driver_info: String,
    /// Capability tier driving deterministic degradation.
    pub tier: CapabilityTier,
    /// Whether hardware timestamps are active.
    pub timestamp_queries: bool,
    /// The DX12 shader compiler; empty on other backends and in browsers.
    pub shader_compiler: String,
}

/// Snapshot of render performance, cache, memory, and scheduling behavior.
#[derive(Clone, Debug, PartialEq)]
pub struct RenderDiagnostics {
    /// Reproducibility metadata.
    pub adapter: AdapterDiagnostics,
    /// Latest bounded pass timing history. Empty unless pass timing is enabled.
    pub passes: Vec<PassTiming>,
    /// Intermediate texture allocation/reuse counters.
    pub textures: TexturePoolStats,
    /// Source-upload texture allocation/reuse and live-byte counters.
    pub upload_textures: UploadTexturePoolStats,
    /// Mapped readback-buffer allocation/reuse and live-byte counters.
    pub readback_buffers: ReadbackBufferPoolStats,
    /// Submission uniform-arena allocation/reuse and live-byte counters.
    pub uniform_buffers: UniformBufferPoolStats,
    /// Runtime and built-in pipeline cache counts.
    pub pipelines: PipelineCacheStats,
    /// Pipeline creations and their CPU time; a first draw that compiles shaders stalls its frame.
    pub pipeline_creation: PipelineCreationStats,
    /// Kept graph results: budget, occupancy, hits, misses, invalidations.
    pub result_cache: crate::ResultCacheStats,
    /// Frames presented by the caller.
    pub presented_frames: u64,
    /// Requests that could not render a new frame.
    pub dropped_frames: u64,
    /// Presentations that held a previous frame.
    pub repeated_frames: u64,
    /// Accumulated host decoder wait time.
    pub decoder_wait: Duration,
    /// Active filter sample budget, possibly below the request (see [`Self::degradations`]).
    pub max_filter_samples: u32,
    /// Active temporal subframe count.
    pub motion_blur_subframes: u32,
    /// Fallbacks applied to the caller's request, such as a reduced filter sample budget.
    pub degradations: Vec<crate::CapabilityDiagnostic>,
    /// Non-source nodes encoded by completed graph executions.
    pub graph_nodes: u64,
    /// Actual render passes encoded by completed graph executions.
    pub graph_passes: u64,
    /// Queue submissions issued by completed graph executions.
    pub graph_submissions: u64,
    /// Queue submissions issued by source presentation/conversion.
    pub source_submissions: u64,
    /// Source passes converting a decoded picture into the working space.
    pub source_conversions: u64,
    /// Decoded 4:2:0 pictures copied plane for plane into NV12 targets.
    pub yuv_repacks: u64,
    /// Queue submissions issued by output packing and readback.
    pub readback_submissions: u64,
    /// Queue submissions combining graph execution with output readback.
    pub frame_submissions: u64,
    /// Actual render passes across graph, source, and readback work.
    pub render_passes: u64,
    /// GPU copy commands encoded for output readback.
    pub copy_operations: u64,
    /// Logical texel bytes copied by output readback commands.
    pub copy_bytes: u64,
}

/// Pipelines this renderer created and the time it took.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct PipelineCreationStats {
    /// Pipelines created.
    pub created: u64,
    /// Microseconds their creation took in all.
    pub create_micros_total: u64,
    /// Microseconds the slowest one took.
    pub create_micros_max: u64,
}

/// How frames since the last [`crate::Renderer::take_gpu_submission_stats`] were split into
/// submissions.
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct GpuSubmissionStats {
    /// Frames rendered.
    pub frames: u64,
    /// Frames submitted in more than one submission.
    pub split_frames: u64,
    /// The most submissions one frame took.
    pub max_per_frame: u32,
    /// Longest predicted GPU time of one submission, in milliseconds, among planned frames.
    pub max_predicted_ms: Option<f64>,
}

/// One pipeline creation for tracing.
#[derive(Clone, Debug, PartialEq)]
pub struct PipelineCreation {
    /// An effect's ID or the composite program's label.
    pub label: String,
    /// When its creation began.
    pub started: web_time::Instant,
    /// When it ended.
    pub finished: web_time::Instant,
}

/// Reproducible metadata envelope written beside benchmark measurements.
#[derive(Clone, Debug, PartialEq)]
pub struct BenchmarkMetadata {
    /// Caller-defined project or fixture identity.
    pub project: String,
    /// Version of the alpha2 render crate.
    pub alpha2_version: &'static str,
    /// Adapter and driver identity.
    pub adapter: AdapterDiagnostics,
    /// Output width and height.
    pub dimensions: (u32, u32),
    /// Output scale used by the caller.
    pub resolution_scale: f32,
    /// Filter sample budget.
    pub max_filter_samples: u32,
    /// Temporal sample count.
    pub motion_blur_subframes: u32,
    /// Texture byte budget.
    pub texture_budget_bytes: u64,
}

/// Entries retained while pass timing is enabled.
const PASS_HISTORY: usize = 256;

/// Pipeline creations kept until taken; oldest are dropped first.
const PIPELINE_HISTORY: usize = 256;

#[derive(Debug, Default)]
pub(crate) struct DiagnosticsState {
    passes: VecDeque<PassTiming>,
    /// Incremented when timing turns off, so late GPU timestamp reads are dropped.
    timing_epoch: u64,
    presented_frames: u64,
    dropped_frames: u64,
    repeated_frames: u64,
    decoder_wait: Duration,
    graph_nodes: u64,
    graph_passes: u64,
    graph_submissions: u64,
    source_submissions: u64,
    source_conversions: u64,
    yuv_repacks: u64,
    readback_submissions: u64,
    frame_submissions: u64,
    render_passes: u64,
    copy_operations: u64,
    copy_bytes: u64,
    pipeline_creation: PipelineCreationStats,
    pipeline_creations: VecDeque<PipelineCreation>,
    gpu_submissions: GpuSubmissionStats,
}

pub(crate) type SharedDiagnostics = Arc<Mutex<DiagnosticsState>>;

#[derive(Copy, Clone)]
pub(crate) struct ResourceDiagnostics {
    pub(crate) textures: TexturePoolStats,
    pub(crate) upload_textures: UploadTexturePoolStats,
    pub(crate) readback_buffers: ReadbackBufferPoolStats,
    pub(crate) uniform_buffers: UniformBufferPoolStats,
    pub(crate) pipelines: PipelineCacheStats,
    pub(crate) result_cache: crate::ResultCacheStats,
}

/// See [`crate::Renderer::pass_timing_epoch`].
pub(crate) fn timing_epoch(state: &SharedDiagnostics) -> u64 {
    state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
        .timing_epoch
}

/// Records one pass duration unless timing was turned off since `epoch`.
pub(crate) fn record_pass(
    state: &SharedDiagnostics,
    epoch: u64,
    label: &'static str,
    duration: Duration,
    source: TimingSource,
) {
    let mut state = state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    if state.timing_epoch != epoch {
        return;
    }
    if state.passes.len() == PASS_HISTORY {
        state.passes.pop_front();
    }
    state.passes.push_back(PassTiming {
        label,
        nanoseconds: u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX),
        source,
    });
}

pub(crate) fn record_graph_work(
    state: &SharedDiagnostics,
    nodes: u64,
    render_passes: u64,
    standalone_submission: bool,
) {
    let mut state = state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    state.graph_nodes = state.graph_nodes.saturating_add(nodes);
    state.graph_passes = state.graph_passes.saturating_add(render_passes);
    if standalone_submission {
        state.graph_submissions = state.graph_submissions.saturating_add(1);
    }
    state.render_passes = state.render_passes.saturating_add(render_passes);
}

pub(crate) fn record_source_work(
    state: &SharedDiagnostics,
    render_passes: u64,
    standalone_submission: bool,
    conversion: bool,
) {
    let mut state = state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    state.source_conversions = state
        .source_conversions
        .saturating_add(u64::from(conversion));
    if standalone_submission {
        state.source_submissions = state.source_submissions.saturating_add(1);
    }
    state.render_passes = state.render_passes.saturating_add(render_passes);
}

pub(crate) fn record_readback_work(
    state: &SharedDiagnostics,
    render_passes: u64,
    copy_operations: u64,
    copy_bytes: u64,
    standalone_submission: bool,
) {
    let mut state = state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    if standalone_submission {
        state.readback_submissions = state.readback_submissions.saturating_add(1);
    }
    state.render_passes = state.render_passes.saturating_add(render_passes);
    state.copy_operations = state.copy_operations.saturating_add(copy_operations);
    state.copy_bytes = state.copy_bytes.saturating_add(copy_bytes);
}

pub(crate) fn record_frame_submission(state: &SharedDiagnostics) {
    let mut state = state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    state.frame_submissions = state.frame_submissions.saturating_add(1);
}

/// Drops pass history and ends the timing run.
pub(crate) fn end_timing_run(state: &SharedDiagnostics) {
    let mut state = state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    state.passes.clear();
    state.timing_epoch = state.timing_epoch.wrapping_add(1);
}

pub(crate) fn snapshot(
    state: &SharedDiagnostics,
    capabilities: &Capabilities,
    resources: &ResourceDiagnostics,
    max_filter_samples: u32,
    motion_blur_subframes: u32,
    degradations: Vec<crate::CapabilityDiagnostic>,
) -> RenderDiagnostics {
    let state = state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    RenderDiagnostics {
        adapter: AdapterDiagnostics {
            name: capabilities.adapter_name.clone(),
            backend: capabilities.backend.clone(),
            driver: capabilities.driver.clone(),
            driver_info: capabilities.driver_info.clone(),
            tier: capabilities.tier,
            timestamp_queries: capabilities.timestamp_queries,
            shader_compiler: capabilities.shader_compiler.clone(),
        },
        passes: state.passes.iter().copied().collect(),
        textures: resources.textures,
        upload_textures: resources.upload_textures,
        readback_buffers: resources.readback_buffers,
        uniform_buffers: resources.uniform_buffers,
        pipelines: resources.pipelines,
        pipeline_creation: state.pipeline_creation,
        result_cache: resources.result_cache,
        presented_frames: state.presented_frames,
        dropped_frames: state.dropped_frames,
        repeated_frames: state.repeated_frames,
        decoder_wait: state.decoder_wait,
        max_filter_samples,
        motion_blur_subframes,
        degradations,
        graph_nodes: state.graph_nodes,
        graph_passes: state.graph_passes,
        graph_submissions: state.graph_submissions,
        source_submissions: state.source_submissions,
        source_conversions: state.source_conversions,
        yuv_repacks: state.yuv_repacks,
        readback_submissions: state.readback_submissions,
        frame_submissions: state.frame_submissions,
        render_passes: state.render_passes,
        copy_operations: state.copy_operations,
        copy_bytes: state.copy_bytes,
    }
}

/// Runs `create`, recording its duration under `label`.
pub(crate) fn timed_pipeline_creation<T>(
    state: &SharedDiagnostics,
    label: impl FnOnce() -> String,
    create: impl FnOnce() -> T,
) -> T {
    let started = web_time::Instant::now();
    let created = create();
    let finished = web_time::Instant::now();
    let micros = u64::try_from(finished.duration_since(started).as_micros()).unwrap_or(u64::MAX);
    let mut state = state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    let totals = &mut state.pipeline_creation;
    totals.created = totals.created.saturating_add(1);
    totals.create_micros_total = totals.create_micros_total.saturating_add(micros);
    totals.create_micros_max = totals.create_micros_max.max(micros);
    if state.pipeline_creations.len() == PIPELINE_HISTORY {
        state.pipeline_creations.pop_front();
    }
    state.pipeline_creations.push_back(PipelineCreation {
        label: label(),
        started,
        finished,
    });
    created
}

pub(crate) fn take_pipeline_creations(state: &SharedDiagnostics) -> Vec<PipelineCreation> {
    state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
        .pipeline_creations
        .drain(..)
        .collect()
}

/// Records how one frame was submitted and its longest predicted submission.
pub(crate) fn record_frame_plan(
    state: &SharedDiagnostics,
    frame: &crate::FrameSubmissions,
    max_predicted_ms: Option<f64>,
) {
    let mut state = state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    let totals = &mut state.gpu_submissions;
    totals.frames = totals.frames.saturating_add(1);
    totals.split_frames = totals
        .split_frames
        .saturating_add(u64::from(frame.submissions > 1));
    totals.max_per_frame = totals.max_per_frame.max(frame.submissions);
    if let Some(ms) = max_predicted_ms {
        totals.max_predicted_ms = Some(totals.max_predicted_ms.map_or(ms, |most| most.max(ms)));
    }
}

/// Submission counts since the last call, then resets them.
pub(crate) fn take_gpu_submission_stats(state: &SharedDiagnostics) -> GpuSubmissionStats {
    std::mem::take(
        &mut state
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .gpu_submissions,
    )
}

pub(crate) fn record_yuv_repack(state: &SharedDiagnostics) {
    let mut state = state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    state.yuv_repacks = state.yuv_repacks.saturating_add(1);
}

pub(crate) fn record_frame(state: &SharedDiagnostics, presented: bool, repeated: bool) {
    let mut state = state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    state.presented_frames += u64::from(presented);
    state.dropped_frames += u64::from(!presented);
    state.repeated_frames += u64::from(repeated);
}

pub(crate) fn record_decoder_wait(state: &SharedDiagnostics, duration: Duration) {
    let mut state = state
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    state.decoder_wait = state.decoder_wait.saturating_add(duration);
}