use anyhow::{Result, bail};
use log::{error, info};
use regex::Regex;
use std::{
ffi::OsStr,
io::prelude::*,
path::{Path, PathBuf},
time::Duration,
};
use crate::{ReplayOutput, TraceTool, replay_command, snapshot::SnapshotResult};
pub struct RenderdocPyTrace {
file: PathBuf,
name: String,
}
impl RenderdocPyTrace {
pub fn new(root: &Path, file: &Path) -> RenderdocPyTrace {
RenderdocPyTrace {
file: file.to_owned(),
name: crate::relative_test_name(root, file),
}
}
}
static RENDERDOC_WRAPPER_SCRIPT: &[u8] = include_bytes!("gpu-trace-perf-renderdoc-wrapper.py");
static RENDERDOC_SNAPSHOT_SCRIPT: &[u8] = include_bytes!("gpu-trace-perf-renderdoc-snapshot.py");
impl TraceTool for RenderdocPyTrace {
fn replay(&self, wrapper: Option<&str>, envs: &[(String, String)]) -> Result<ReplayOutput> {
let renderdoc_command: &[_] = &[
OsStr::new("python3"),
OsStr::new("-"), self.file.as_os_str(),
];
let mut command = replay_command(renderdoc_command, wrapper, envs);
command.stdin(std::process::Stdio::piped());
command.stdout(std::process::Stdio::piped());
command.stderr(std::process::Stdio::piped());
let mut child = command.spawn().expect("failed to start python3");
let stdin = child.stdin.as_mut().expect("Failed to open stdin");
stdin
.write_all(RENDERDOC_WRAPPER_SCRIPT)
.expect("failed to write to python's stdin");
let output = child.wait_with_output().expect("failed to read stdout");
if !output.status.success() {
let stderr = std::str::from_utf8(&output.stderr).unwrap();
if stderr.contains("API is unsupported") {
bail!("renderdoc reported API (likely window system) unsupported, skipping trace");
}
error!("Failed to start renderdoc:");
error!("{}", stderr);
error!("command: {:?}", command);
if stderr.contains("FileNotFound") {
info!(
"TIP: Failure to find a file with a space in its name probably means your wrapper script didn't quote the arguments"
)
}
bail!("Failed to start renderdoc");
}
Ok(ReplayOutput::from(output))
}
fn fps(&self, output: &ReplayOutput) -> Result<f64> {
parse_renderdoc_wrapper_output(&output.stdout).map(|x| x as f64)
}
fn name(&self) -> &str {
&self.name
}
fn can_snapshot(&self) -> bool {
true
}
fn snapshot(&self, output_dir: &str, loops: u32, timeout: Duration) -> Result<SnapshotResult> {
let output_dir_path = self.output_dir(output_dir)?.unwrap();
let loops_str = loops.to_string();
let renderdoc_command: &[_] = &[
OsStr::new("python3"),
OsStr::new("-"), self.file.as_os_str(),
output_dir_path.as_os_str(),
OsStr::new("--loops"),
OsStr::new(&loops_str),
];
let command = replay_command(renderdoc_command, None, &[]);
let start_time = std::time::Instant::now();
let output =
self.run_replay_command_with_timeout(command, Some(RENDERDOC_SNAPSHOT_SCRIPT), timeout);
if output.exit_code != 0 {
if output.stderr.contains("API is unsupported") {
bail!("renderdoc reported API (likely window system) unsupported, skipping trace");
}
error!("Failed to run renderdoc snapshot: {}", output.stderr);
}
let mut files = Vec::new();
for i in 1..=loops {
let relative = PathBuf::from(format!("snapshot{i:04}.png"));
if output_dir_path.join(&relative).exists() {
files.push(relative);
}
}
Ok(SnapshotResult {
files,
output,
runtime: start_time.elapsed(),
})
}
}
pub struct RenderdocUtraceTrace {
file: PathBuf,
name: String,
is_directx: bool,
}
fn guess_filename_directx(file: &Path) -> bool {
let file_str = file.to_string_lossy();
[
"dx8", "dx9", "dx10", "dx11", "dx12", "d3d8", "d3d9", "d3d10", "d3d11", "d3d12", "dxgi",
]
.iter()
.any(|x| file_str.contains(x))
}
impl RenderdocUtraceTrace {
pub fn new(root: &Path, file: &Path) -> RenderdocUtraceTrace {
RenderdocUtraceTrace {
file: file.to_owned(),
name: crate::relative_test_name(root, file),
is_directx: guess_filename_directx(file),
}
}
}
impl TraceTool for RenderdocUtraceTrace {
fn replay(&self, wrapper: Option<&str>, envs: &[(String, String)]) -> Result<ReplayOutput> {
let args: &[_] = if self.is_directx {
&[
OsStr::new("wine"),
OsStr::new("renderdoccmd.exe"),
OsStr::new("replay"),
self.file.as_os_str(),
OsStr::new("-l"),
OsStr::new("3"),
]
} else {
&[
OsStr::new("renderdoccmd"),
OsStr::new("replay"),
self.file.as_os_str(),
OsStr::new("-l"),
OsStr::new("3"),
]
};
let command = replay_command(args, wrapper, envs);
let output: ReplayOutput = self.run_replay_command(command);
if output.exit_code != 0 {
if output.stderr.contains("API is unsupported") {
bail!("renderdoc reported API (likely window system) unsupported, skipping trace");
}
if output.stderr.contains("FileNotFound") {
println!(
"TIP: Failure to find a file with a space in its name probably means your wrapper script didn't quote the arguments"
)
}
}
Ok(output)
}
fn fps(&self, _output: &ReplayOutput) -> Result<f64> {
unreachable!("shouldn't be called");
}
fn name(&self) -> &str {
&self.name
}
}
fn parse_renderdoc_wrapper_output(output: &str) -> Result<f32> {
lazy_static! {
static ref CALL_RE: Regex = Regex::new("EID [0-9]*: (.*)").unwrap();
}
let mut total = 0.0;
for line in output.lines() {
if let Some(cap) = CALL_RE.captures(line) {
match cap[1].parse::<f32>() {
Ok(time) => total += time,
_ => {
bail!("Failed to parse renderdoc time event '{line}'");
}
}
}
}
if total == 0.0 || total.is_nan() {
bail!("Bad total time {total}");
}
Ok(1.0 / total)
}
#[cfg(test)]
mod tests {
use super::*;
use assert_approx_eq::assert_approx_eq;
#[test]
fn test_renderdoc_parsing() {
let renderdoc_input = "
Counter 1 (GPU Duration):
Time taken for this event on the GPU, as measured by delta between two GPU timestamps.
Returns 8 byte CompType.Double, representing CounterUnit.Seconds
Counter 2000000 (N vertices submitted):
N vertices submitted
Returns 8 byte CompType.UInt, representing CounterUnit.Absolute
EID 52: 0.000045
EID 370: 0.000004
EID 407: 0.000006
";
assert_approx_eq!(
parse_renderdoc_wrapper_output(renderdoc_input).unwrap(),
1.0 / (0.000_045 + 0.000_004 + 0.000_006),
0.000_001
);
}
#[test]
fn test_renderdoc_nan_parsing() {
let renderdoc_input = "
EID 52: 0.000045
EID 370: nan
EID 407: 0.000006
";
assert!(parse_renderdoc_wrapper_output(renderdoc_input).is_err());
}
#[test]
fn test_guess_filename_directx() {
assert!(guess_filename_directx(Path::new(
"d3d11-renderdoc/witcher3_medium_1.rdc"
)));
assert!(!guess_filename_directx(Path::new(
"/home/anholt/src/traces-db/supertuxkart/supertuxkart-menu.rdc"
)));
assert!(!guess_filename_directx(Path::new(
"/home/anholt/src/traces-db/godot/Material Testers.x86_64_2020.04.08_13.38_frame799.rdc"
)));
}
}