A 3D software rasterizer implemented twice — once on the CPU with OpenMP, once on the GPU with CUDA — to directly compare performance across architectures.
The CPU version uses bounding box rasterization parallelized across 6 cores with OpenMP. The GPU version uses tile-based deferred rendering, binning triangles into 32x32 pixel tiles and assigning each tile to a CUDA block, giving 262,144 threads running simultaneously with the z-buffer kept in shared memory to avoid global memory round-trips.
Tested on a 15,066 triangle model at 1024x1024 with toon, grayscale, and tangent-space normal map shaders. Hardware: RTX 5060 Ti, Ryzen 5 7600X.
| Metric | CPU | GPU | Speedup |
|---|---|---|---|
| Total render time | 0.48s | 0.11s | 4.2x |
| Time per triangle | 31.9 us | 7.3 us | 4.4x |
| Compute time | ~470ms | ~87us | 5,402x |
| Memory transfer | N/A | ~722us | N/A |
The 5,402x compute speedup versus the 4.2x end-to-end speedup shows the workload is latency-bound at this model size — memory allocation dominates roughly 90% of CUDA API time. A larger model would push GPU utilisation above the 33% measured here.
The CPU renderer calculates a bounding box per triangle and tests every pixel inside it for containment using barycentric coordinates, with OpenMP parallelising the outer loop. Memory access patterns are irregular which hurts cache efficiency.
The GPU renderer pre-sorts triangles into screen-space tiles before the kernel launches. Each CUDA block owns one tile and keeps its z-buffer in shared memory, eliminating cache misses on depth testing. Threads within a block process pixels in the tile in parallel.
Key differences:
- 12 CPU threads vs 262,144 GPU threads
- 50 GB/s CPU memory bandwidth vs 900 GB/s GPU
- Barycentric coordinate testing (CPU) vs edge function testing (GPU)
- Global memory z-buffer (CPU) vs shared memory z-buffer (GPU)
cd cpu
mkdir build && cd build
cmake ..
make
./renderer ../obj/diablo3_pose/diablo3_pose.objcd cuda_gpu
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make
./renderer ../obj/diablo3_pose/diablo3_pose.objperf record ./cpu_renderer
perf report# System-level
nsys profile --output=gpu_profile ./renderer ../obj/diablo3_pose/diablo3_pose.obj
# Kernel-level
ncu --section ComputeWorkloadAnalysis \
--section MemoryWorkloadAnalysis \
--section SpeedOfLight \
--apply-rules yes \
--export gpu_kernel_profile \
./renderer ../obj/diablo3_pose/diablo3_pose.obj- Persistent kernels and better memory coalescing on the GPU
- SIMD intrinsics and cache-aware data layouts on the CPU
- Larger scene to saturate GPU utilisation and measure scaling
Built with C++20, CUDA 13.1, OpenMP, and CMake.