Komputasi ray-tracing terdistribusi sering mengalami bottleneck bukan pada beban floating-point per pixel, melainkan pada sinkronisasi output dan efisiensi throughput jaringan. Ketika puluhan worker mengirimkan hasil kalkulasi camera ray secara bersamaan ke node agregator, lock contention pada frame akumulator meningkat tajam dan mendegradasi kinerja sistem.
Arsitektur Tiling dan Dekomposisi Camera Rays
Berdasarkan formulasi camera rays standar, setiap ray didefinisikan oleh titik asal $\vec{O}$ dan vektor arah $\vec{D}$ yang dinormalisasi menuju frustum kamera:
\vec{D}(u, v) = \frac{u \cdot \vec{U} + v \cdot \vec{V} + \vec{W}}{\|u \cdot \vec{U} + v \cdot \vec{V} + \vec{W}\|}Alih-alih mendistribusikan ray secara acak (per-pixel sampling), layar dibagi menjadi grid berukuran tetap (misalnya 16x16 atau 32x32 piksel). Strategi tiling ini memberikan dua keuntungan arsitektural:
- Ray Coherence: Ray yang berdekatan memiliki kemungkinan tinggi untuk memotong bounding volume hierarchy (BVH) dan geometri scene yang sama. Ini memaksimalkan L1/L2 cache locality pada CPU worker.
- Batas Akumulasi Terisolasi: Worker memproses satu segmen layar penuh dan mengirimkan satu batch hasil. Ini meminimalkan overhead transmisi TCP/RPC.
Mitigasi Lock Contention: Striped Ring Buffer & Tiling Lock
Masalah umum pada node agregator adalah memperebutkan satu mutex global ketika menuliskan spektrum warna ray ke buffer gambar akhir. Solusi optimalnya menggunakan akumulator berbasis Striped Tiling Lock yang dikombinasikan dengan Ring Buffer Slot.
Daripada mengunci seluruh framebuffer, buffer dipecah ke dalam N lock independen menggunakan hash dari koordinat tile. Worker yang menulis ke Tile A tidak akan memblokir worker yang menulis ke Tile B.
package main
import (
"sync"
"sync/atomic"
)
type TileResult struct {
TileID uint32
X, Y int
W, H int
Pixels []float32 // RGB float32 values
}
type FrameAccumulator struct {
width, height int
stripes uint32
locks []sync.Mutex
buffer []float32
completed atomic.Uint32
}
func NewFrameAccumulator(width, height int, stripeCount uint32) *FrameAccumulator {
return &FrameAccumulator{
width: width,
height: height,
stripes: stripeCount,
locks: make([]sync.Mutex, stripeCount),
buffer: make([]float32, width*height*3),
}
}
// WriteTile menulis data piksel secara aman tanpa mengunci seluruh framebuffer
func (fa *FrameAccumulator) WriteTile(res TileResult) {
stripeIdx := res.TileID % fa.stripes
fa.locks[stripeIdx].Lock()
for row := 0; row < res.H; row++ {
dstIdx := ((res.Y+row)*fa.width + res.X) * 3
srcIdx := row * res.W * 3
copy(fa.buffer[dstIdx:dstIdx+res.W*3], res.Pixels[srcIdx:srcIdx+res.W*3])
}
fa.locks[stripeIdx].Unlock()
fa.completed.Add(1)
}Desain Worker Rust: Cache Scene Lokal dan Idempotensi
Data scene ray-tracing (triangle meshes, BVH, texture) berukuran statis dan berat. Worker harus mengunduh scene descriptor sekali saja via shared memory atau read-only memory-mapped file (mmap), bukan mengambilnya berulang kali dari message broker.
Setiap payload tugas tile harus memuat ID frame, koordinat grid, resolusi, dan sample seed. Karena kegagalan jaringan dapat memicu pengiriman ulang (retry) oleh task queue, eksekusi ray worker harus bersifat idempotent: hasil render tile pada frame dan seed yang sama harus selalu menghasilkan deterministik float values.
use std::sync::Arc;
#[derive(Clone)]
pub struct SceneData {
// BVH and triangle mesh buffers (immutable)
pub nodes: Arc<Vec<u8>>,
}
pub struct TileTask {
pub frame_id: u64,
pub tile_id: u32,
pub x: usize,
pub y: usize,
pub width: usize,
pub height: usize,
pub seed: u64,
}
pub struct Worker {
scene: SceneData,
}
impl Worker {
pub fn new(scene: SceneData) -> Self {
Self { scene }
}
// Eksekusi komputasi ray murni deterministik
pub fn process_tile(&self, task: &TileTask) -> Vec<f32> {
let pixel_count = task.width * task.height;
let mut output = vec![0.0f32; pixel_count * 3];
for py in 0..task.height {
for px in 0..task.width {
let idx = (py * task.width + px) * 3;
// Hitung ray direction berdasarkan frustum dan posisi offset
// Trace ray terhadap &self.scene...
output[idx] = 0.5f32; // R
output[idx + 1] = 0.5f32; // G
output[idx + 2] = 0.5f32; // B
}
}
output
}
}Penanganan Kegagalan Worker dan Verifikasi Latensi Antrian
Untuk menghindari stall render ketika satu worker lambat (straggler) atau mengalami panic, pipeline harus menerapkan pola acknowledgment berbasis timeout:
- At-Least-Once Delivery: Dispatcher menandai tile sebagai inflight ketika diambil worker. Jika tile tidak selesai dalam batas SLA latensi (misal 500ms), worker dianggap hang dan tile dijadwalkan ulang ke worker lain.
- Idempotent Frame Overwrite: Node agregator mengabaikan atau langsung menimpa data tile jika duplikat pesan tiba terlambat, diverifikasi dari
(frame_id, tile_id).
Verifikasi Health dan Latensi Antrian
Verifikasi starvation dan antrian ray dilakukan langsung melalui metrik throughput dan antrian tak terselesaikan pada message broker (seperti Redis/RabbitMQ/NATS):
# Cek kedalaman antrian tile yang belum diproses di Redis
redis-cli LLEN raytracing:tiles:queue
# Pantau latensi transit per-tile menggunakan Prometheus rate counter
rate(raytracing_tile_completed_total[1m])
histogram_quantile(0.99, sum(rate(raytracing_tile_latency_seconds_bucket[5m])) by (le))Jika p99 latensi melonjak sementara utilisasi CPU worker rendah, periksa ukuran buffer striped lock pada agregator atau turunkan resolusi tile (misalnya dari 64x64 menjadi 16x16) untuk mengurangi skew durasi trace antar-region.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!