//! # Launch overhead microbenchmark — `launch!()` vs raw `cuLaunchKernel` (NF3)
//!
//! Verifies the OxiCUDA Launch blueprint quality gate **NF3**: the
//! [`launch!()`](oxicuda_launch::launch) macro must add **less than 1 µs**
//! of overhead above a hand-rolled `cuLaunchKernel` FFI call.
//!
//! ## Methodology
//!
//! For each iteration we launch a no-op PTX kernel (`noop_kernel`) on a
//! 1×1 grid with a 1-thread block. Two paths are exercised:
//!
//! 1. **`launch_macro_warm`** — invokes the [`launch!()`](oxicuda_launch::launch)
//!    macro with the same `Kernel`, `Stream`, and zero-argument tuple `()`.
//! 2. **`cu_launch_kernel_warm`** — calls the raw `cuLaunchKernel` FFI
//!    function pointer directly with `null_mut()` for both `kernel_params`
//!    and `extra`.
//!
//! All non-launch work (driver lookup, module compile, kernel resolution,
//! stream creation) is hoisted outside `b.iter(...)`. The closure body is
//! the launch call only, so criterion's median measures launch overhead.
//!
//! Because the launch is asynchronous, neither path blocks on completion —
//! this isolates the **launch-time** overhead, which is what NF3 targets.
//!
//! ## Platform behaviour
//!
//! * **Linux / Windows with NVIDIA driver 525+** — full benchmark runs.
//! * **macOS / no GPU / no driver** — every bench function returns early
//!   with `eprintln!("skip: no GPU")`. The skip guard is the **first**
//!   statement in each bench function, before any allocation.
//!
//! Run with:
//! ```bash
//! cargo bench -p oxicuda-launch --bench launch_overhead
//! ```

use std::ffi::c_void;
use std::hint::black_box;
use std::sync::Arc;

use criterion::{Criterion, criterion_group, criterion_main};
use oxicuda_driver::loader::try_driver;
use oxicuda_driver::{Context, Device, Module, Stream, init};
use oxicuda_launch::{Kernel, KernelArgs, launch};

// ---------------------------------------------------------------------------
// No-op PTX kernel
// ---------------------------------------------------------------------------

/// Minimal PTX kernel that does nothing — used to isolate launch overhead.
///
/// Compute-capability `sm_80` covers Ampere / Ada / Hopper; on older GPUs
/// the JIT will downgrade. We deliberately avoid PTX features that depend
/// on a specific architecture so any modern driver can JIT this.
const NOOP_PTX: &str = "//
// Generated by OxiCUDA Launch overhead bench fixture
//
.version 7.0
.target sm_80
.address_size 64

.visible .entry noop_kernel()
{
    ret;
}
";

const KERNEL_NAME: &str = "noop_kernel";

// ---------------------------------------------------------------------------
// Setup helper
// ---------------------------------------------------------------------------

/// Bundles all GPU resources required to launch the no-op kernel.
///
/// Constructed once per bench function (outside `b.iter`) and re-used
/// across iterations.
struct LaunchHarness {
    /// Keeps the `Context` alive for the lifetime of `Stream` and `Module`.
    _ctx: Arc<Context>,
    /// The compiled no-op kernel.
    kernel: Kernel,
    /// Asynchronous command queue used for every launch.
    stream: Stream,
}

/// Initialise the driver, pick GPU 0, compile the no-op PTX, and return a
/// fully-prepared launch harness.
///
/// Returns `None` on any platform without a usable CUDA driver (macOS,
/// CI without GPU, etc.) — the caller skips the benchmark in that case.
fn setup_harness() -> Option<LaunchHarness> {
    init().ok()?;
    let device = Device::get(0).ok()?;
    let ctx = Arc::new(Context::new(&device).ok()?);
    let module = Arc::new(Module::from_ptx(NOOP_PTX).ok()?);
    let kernel = Kernel::from_module(module, KERNEL_NAME).ok()?;
    let stream = Stream::new(&ctx).ok()?;
    Some(LaunchHarness {
        _ctx: ctx,
        kernel,
        stream,
    })
}

// ---------------------------------------------------------------------------
// Bench: launch!() macro path (warm)
// ---------------------------------------------------------------------------

/// Measures the steady-state overhead of the [`launch!()`](oxicuda_launch::launch)
/// macro on a no-op kernel.
///
/// All construction (`LaunchParams`, argument-pointer collection, tracing
/// span guard) happens inside the macro expansion — that is exactly what
/// we want to time. Setup is hoisted out so we measure launch only.
fn bench_launch_macro_warm(c: &mut Criterion) {
    let harness = match setup_harness() {
        Some(h) => h,
        None => {
            eprintln!("skip: no GPU");
            return;
        }
    };

    let mut group = c.benchmark_group("launch_overhead");
    group.bench_function("launch_macro_warm", |b| {
        b.iter(|| {
            // The macro expands to LaunchParams::new(...) + Kernel::launch().
            // black_box on the kernel reference prevents the optimizer from
            // hoisting the macro's parameter construction outside the loop.
            let r = launch!(
                black_box(&harness.kernel),
                grid(1u32),
                block(1u32),
                &harness.stream,
                &()
            );
            black_box(r.ok());
        });
    });
    group.finish();
}

// ---------------------------------------------------------------------------
// Bench: raw cuLaunchKernel path (warm)
// ---------------------------------------------------------------------------

/// Measures the raw `cuLaunchKernel` FFI call overhead with identical
/// launch parameters as the macro bench.
///
/// We hoist `try_driver()` and `KernelArgs::as_param_ptrs()` out of the
/// closure so the inner body is just the unsafe FFI call plus an error
/// check — mirroring the work the macro path also performs.
fn bench_cu_launch_kernel_warm(c: &mut Criterion) {
    let harness = match setup_harness() {
        Some(h) => h,
        None => {
            eprintln!("skip: no GPU");
            return;
        }
    };

    let driver = match try_driver() {
        Ok(d) => d,
        Err(_) => {
            eprintln!("skip: no GPU");
            return;
        }
    };

    // Empty argument vector — the no-op kernel takes no parameters. We
    // construct it once outside the closure to mirror the macro path,
    // which builds the same vector via `KernelArgs::as_param_ptrs`.
    let mut param_ptrs: Vec<*mut c_void> = ().as_param_ptrs();
    let func_raw = harness.kernel.function().raw();
    let stream_raw = harness.stream.raw();

    let mut group = c.benchmark_group("launch_overhead");
    group.bench_function("cu_launch_kernel_warm", |b| {
        b.iter(|| {
            // Identical 1×1×1 grid / 1×1×1 block / 0 shared mem as the
            // macro path. `null_mut()` for the param array is valid when
            // the kernel takes no arguments; passing an empty `Vec`'s
            // `as_mut_ptr()` yields the same effective behaviour.
            let result = unsafe {
                (driver.cu_launch_kernel)(
                    func_raw,
                    1, // grid_dim_x
                    1, // grid_dim_y
                    1, // grid_dim_z
                    1, // block_dim_x
                    1, // block_dim_y
                    1, // block_dim_z
                    0, // shared_mem_bytes
                    stream_raw,
                    param_ptrs.as_mut_ptr(),
                    std::ptr::null_mut(), // extra
                )
            };
            black_box(result);
        });
    });
    group.finish();
}

// ---------------------------------------------------------------------------
// Bench: paired comparison group
// ---------------------------------------------------------------------------

/// Runs both paths back-to-back inside a single benchmark group so that
/// criterion's median statistics are produced under identical sample
/// counts and warmup conditions, making the macro-vs-raw delta directly
/// comparable.
///
/// This is the data point that NF3 (< 1 µs delta) is evaluated against.
fn bench_paired_comparison(c: &mut Criterion) {
    let harness = match setup_harness() {
        Some(h) => h,
        None => {
            eprintln!("skip: no GPU");
            return;
        }
    };

    let driver = match try_driver() {
        Ok(d) => d,
        Err(_) => {
            eprintln!("skip: no GPU");
            return;
        }
    };

    let mut param_ptrs: Vec<*mut c_void> = ().as_param_ptrs();
    let func_raw = harness.kernel.function().raw();
    let stream_raw = harness.stream.raw();

    let mut group = c.benchmark_group("launch_overhead_paired");

    // Pin both paths to the same sample count so the medians compare
    // directly. 100 is the criterion default; we set it explicitly to
    // document the NF3 measurement protocol.
    group.sample_size(100);

    group.bench_function("macro", |b| {
        b.iter(|| {
            let r = launch!(
                black_box(&harness.kernel),
                grid(1u32),
                block(1u32),
                &harness.stream,
                &()
            );
            black_box(r.ok());
        });
    });

    group.bench_function("raw_ffi", |b| {
        b.iter(|| {
            let result = unsafe {
                (driver.cu_launch_kernel)(
                    func_raw,
                    1,
                    1,
                    1,
                    1,
                    1,
                    1,
                    0,
                    stream_raw,
                    param_ptrs.as_mut_ptr(),
                    std::ptr::null_mut(),
                )
            };
            black_box(result);
        });
    });

    group.finish();
}

// ---------------------------------------------------------------------------
// Criterion wiring
// ---------------------------------------------------------------------------

criterion_group!(
    launch_overhead_benches,
    bench_launch_macro_warm,
    bench_cu_launch_kernel_warm,
    bench_paired_comparison,
);
criterion_main!(launch_overhead_benches);
