Younes Laaroussi

Cybersecurity and Software Engineer

GPU Performance Engineering: Lessons from Building Vidova AI

When I set out to build Vidova AI, I knew video editing software lived or died by its performance. Users don't tolerate dropped frames. They don't accept laggy previews. They definitely won't wait 10 seconds for a timeline scrub to render. This reality forced me deep into GPU performance engineering—a domain I'd been circling since building my first PC as a teenager.

What I learned about benchmarking, frame-time analysis, and GPU optimization during Vidova's development translates directly to any performance-critical graphics work. Here's what the journey taught me.

The Performance Problem

Video editing is uniquely demanding. Unlike a web application where 100ms latency is acceptable, video demands consistent 16.67ms frame delivery for 60fps playback—or 8.33ms for 120fps. Miss that budget, and users see stutters, dropped frames, and an application that feels broken.

Early Vidova prototypes had a dirty secret: they looked smooth in demos but stuttered under real workloads. Average frame times looked fine. But averages lie.

Frame Timeline (with stutter):
┌────────┬────────┬─────────────────┬────────┬────────┐
│  8ms   │  9ms   │     45ms        │  7ms   │  8ms   │
│ Frame 1│ Frame 2│    Frame 3      │ Frame 4│ Frame 5│
└────────┴────────┴─────────────────┴────────┴────────┘
                   ↑
            User feels this stutter

That 45ms frame in the middle? Users feel it. The average across these five frames is 15.4ms—technically under our 16.67ms budget. But the experience is terrible. One stutter destroys the perception of smoothness.

This is why percentile metrics matter more than averages in graphics performance.

Understanding Frame Time Percentiles

The gaming and graphics industry standardized on percentile measurements because they capture what users actually experience:

MetricWhat It MeasuresWhy It Matters
Average FPSTotal frames / timeMarketing numbers, hides stutters
1% LowBottom 1% of frame timesCatches occasional hitches
p9595th percentile frame timeMost frames are at or below this
p9999th percentile frame timeCaptures rare but noticeable spikes
p99.999.9th percentileThe worst stutters users might see

For Vidova, I targeted these benchmarks:

  • p50 (median): Under 8ms (120fps capable)
  • p95: Under 12ms (comfortable 60fps headroom)
  • p99: Under 16ms (never drop below 60fps)
  • p99.9: Under 33ms (worst case stays above 30fps)
Percentile Targets:
                                                    
  p50 ████████░░░░░░░░░░░░░░░░░░░░░░░░  8ms   ✓ smooth
  p95 ████████████░░░░░░░░░░░░░░░░░░░░ 12ms   ✓ headroom  
  p99 ████████████████░░░░░░░░░░░░░░░░ 16ms   ✓ 60fps floor
p99.9 ████████████████████████████████░ 33ms   ✓ worst case
      |--------|--------|--------|----→ ms
      0        10       20       30

The Benchmarking Pipeline

You can't optimize what you can't measure. I built a benchmarking pipeline that captured frame times during actual video editing workflows—not synthetic tests.

The key insight: benchmark real workflows, not artificial loads. I recorded actual editing sessions—timeline scrubbing, effect application, multi-track compositing—and replayed them during benchmarking. Synthetic benchmarks might show 144fps on an empty timeline, but that tells you nothing about real-world performance.

Capturing Frame Times

For GPU-accelerated rendering, I used timer queries to measure actual GPU execution time:

// Simplified frame time capture concept
class FrameTimeProfiler {
  constructor() {
    this.frameTimes = [];
    this.lastTimestamp = performance.now();
  }

  recordFrame() {
    const now = performance.now();
    const frameTime = now - this.lastTimestamp;
    this.frameTimes.push(frameTime);
    this.lastTimestamp = now;
  }

  getPercentile(p) {
    const sorted = [...this.frameTimes].sort((a, b) => a - b);
    const index = Math.ceil((p / 100) * sorted.length) - 1;
    return sorted[index];
  }

  getReport() {
    return {
      p50: this.getPercentile(50),
      p95: this.getPercentile(95),
      p99: this.getPercentile(99),
      p999: this.getPercentile(99.9),
      avg: this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length
    };
  }
}

GPU Pipeline Optimization

Once I could measure accurately, patterns emerged. Vidova's rendering pipeline had three main bottlenecks: CPU-bound (draw call overhead, buffer uploads), GPU-bound (shader complexity, vertex processing), and memory-bound (texture bandwidth, cache misses).

Bottleneck 1: Draw Call Overhead

Early versions issued hundreds of draw calls per frame—one per video layer, effect, and UI element. Each draw call has CPU overhead that adds up.

The fix: Batching. I grouped similar operations into single draw calls. Text elements batch together. Video layers with identical blend modes batch together. UI elements render in a single pass.

Result: Draw calls dropped from ~400 to ~50 per frame, reducing CPU-side p99 from 8ms to 2ms.

Bottleneck 2: Texture Bandwidth

4K video frames are massive. A single RGBA frame at 3840×2160 is ~33MB uncompressed. Uploading multiple frames per render pass was destroying memory bandwidth.

The fix: Texture streaming with ring buffers. Instead of uploading full frames each render, I maintained a circular buffer of decoded frames on the GPU. The render pass samples from pre-uploaded textures rather than waiting for uploads.

Ring Buffer Strategy:

  Decode Thread          GPU Memory              Render Thread
  ─────────────         ───────────             ─────────────
                     ┌─────────────────┐
   decode(N+2) ────▶ │ N-2  N-1  [N]  N+1  N+2 │ ◀──── sample(N)
                     └─────────────────┘
                            ↑
                     current playhead
                     
  Async upload to future slots while rendering from current

Result: Texture upload stalls eliminated, p99.9 dropped from 45ms to 18ms.

Bottleneck 3: Shader Complexity

Video effects like color grading, blur, and compositing ran in fragment shaders. Complex effect stacks meant expensive per-pixel computation.

The fix: Multi-pass rendering with intermediate render targets. Instead of one mega-shader doing everything, I broke effects into passes. Each pass does one thing efficiently. The GPU's cache stays warm because we're processing the same pixels repeatedly.

Result: Effect-heavy timelines improved from 24fps to 60fps on mid-range GPUs.

Hardware Diversity: The Real Challenge

Building my first PC as a teenager taught me that hardware varies wildly. That lesson proved essential for Vidova. Users run everything from integrated Intel graphics to RTX 4090s. The software needs to perform acceptably across this spectrum.

GPU ClassTarget p99Strategy
High-end (RTX 30/40 series)8msFull quality, all effects
Mid-range (GTX 16, RTX 20)12msReduced preview quality
Integrated (Intel Iris, AMD APU)24msAggressive LOD, proxy editing

The key insight: don't aim for identical performance—aim for acceptable performance at each tier. A GTX 1650 user expects different performance than an RTX 4080 user. Meeting those expectations matters more than raw numbers.

Game Capture Benchmarking

One of Vidova's features is game capture and recording. This required benchmarking not just our own rendering, but also the impact of capture on game performance.

The methodology: run the game without capture to establish baseline p50/p95/p99, then enable capture and measure the delta. If capture overhead exceeds 2ms on p99, something's wrong with the encoder pipeline.

We use hardware-accelerated encoding (NVENC on NVIDIA, VCE on AMD, QuickSync on Intel) to minimize CPU overhead. The key metrics:

  • Capture overhead on p99: Must be under 2ms
  • Encoding latency: Under 4ms for real-time streaming
  • Memory bandwidth impact: Minimal—we share textures rather than copying

Testing across different games revealed patterns: GPU-bound games tolerate capture better than CPU-bound games. Open-world titles with streaming showed more variance than arena shooters with static levels.

Lessons That Transfer

The performance engineering principles from Vidova apply to any graphics-intensive work:

1. Measure Percentiles, Not Averages

Average FPS is a vanity metric. p99 tells you what users actually experience during the worst moments—which are the moments they remember.

2. Benchmark Real Workloads

Synthetic tests are useful for isolation but dangerous for decisions. Always validate with realistic usage patterns.

3. Profile Before Optimizing

Intuition about bottlenecks is usually wrong. I was convinced shader complexity was our main issue; it turned out draw call overhead dominated. Profiling revealed the truth.

4. Design for the Hardware Spectrum

Performance engineering isn't about maximizing one configuration—it's about delivering acceptable experiences across all configurations your users have.

5. Frame Time Variance Matters

A consistent 45fps often feels smoother than 60fps with occasional drops to 30fps. Variance is the enemy of perceived smoothness.

Consistent 45fps:   16ms → 17ms → 16ms → 17ms   ✓ feels smooth
Variable 60fps:     10ms →  8ms → 35ms →  9ms   ✗ feels stuttery

What's Next

Vidova continues to evolve, and so does my understanding of GPU performance. I'm particularly interested in:

  • Temporal techniques: Using frame history for smoother motion and better upscaling
  • Async compute: Overlapping graphics and compute work for better GPU utilization
  • ML-accelerated encoding: Leveraging tensor cores for real-time video processing

The fundamentals don't change, though. Measure accurately. Profile honestly. Optimize what matters. Ship software that respects users' time and hardware.

If you're building performance-critical graphics software, I hope these lessons from the Vidova trenches prove useful. The path from "it works" to "it's fast" is longer than most developers expect—but it's one of the most rewarding engineering challenges I've encountered.