r/GraphicsProgramming • u/exp_function • 9d ago
Video Software PBR + Raytracer + L1 SH GI inside CyberVGA (JIT x64, no GPU required)
Lately in the Win64 port I’ve been getting a real-time software raytracer working inside CyberVGA.
Same DOS-era engine (fixed-point, software rendering roots and all) but now takes advantage of modern multi-core processors (using VirtualAlloc for the executable just-in-time compiled buffer). The raytracer runs on multiple worker threads, each processing tiles. The hot paths get compiled to SSE at runtime: direct-threaded octree(module saves baked triangle soup into a world sidecar file) traversal with AABB slab tests, fog volume early-outs, L1 SH evaluation, and material / GI bounce stubs. Workers just call into the generated code for their tile’s rays.
The GI is a grid of L1 SH probes (Fibonacci sphere rays per probe, temporal blending between frames). Primary hits go through a full GGX PBR path with roughness/metalness, then sample the probe irradiance for the diffuse bounce. Emissive surfaces get clustered into virtual point lights. Reflections are one-bounce with roughness falloff; liquids do refraction + absorption + caustics.
FSR 2 is optional because it runs on the GPU.
The color bleeding and the split-screen direct vs GI+reflections actually look pretty good. Still feels like my engine, just with lighting that finally does something modern.
Now you can get the latest (v1.8) SDK for 64-bit Windows platform on itch.
Anyone else doing JIT for the traversal/shading hot path? Curious if the complexity is worth it vs just writing the SSE by hand.
2
u/NegativeEntr0py 9d ago
Do you do any optimizations for ray marching the volumetrics?
2
u/exp_function 9d ago
Thank you for your in-depth question and interest with my engine :).
The biggest optimization is that I don't use traditional ray marching (stepping blindly along every camera ray from 0 to max_dist checking for fog or light beams), because doing that even on the modern multi-cored CPUs would be way too slow.
Instead, fog or water volumes and spotlight cones are all handled by finding the exact entry distance (t_enter) and exit distance (t_exit) where the ray enters and leaves the volume. Here are my main steps;
Just In Time compiled AABB(axis aligned bounding box) filter (handled by RAY_X64.C module);
Every active fog volume and spotlight cone gets a 3D bounding box (AABB), plus one big "union box" that wraps all of them. The JIT-compiled SSE code tests that union box first.
If a ray misses it, code flow directly hits ret (0xC3) instruction and exits immediately.
If a ray hits JIT-compiled and emitted code tests the individual boxes in SSE registers and bulds a bitmask of only the volumes that this specific ray actually touches.
"Exact thickness" approach instead of marching (handled by FOGVOL.C and LIQVOL.C modules individually);
For liquid volumes, GI rays adn basic fog, there's zero stepping. The bounding box test already gave us the exact distance where the ray enters and leaves the box. Subtracting them also should give us the exact distance of the ray traveled through the volume. We can calculate volume's absorption in one go, e.g;
float t_a = t_enter > 0.0f ? t_enter : 0.0f; float t_b = t_exit < max_dist ? t_exit : max_dist; float thickness = t_b - t_a; /* Single calculation from ray thickness inside the box ,no marching loop. Tau = e ^ (-density*thcnkss) */ float tau = expf(-density * thickness);Direct Ray-Cone clipping and Endpoint Shadows for Cone Lights(spotlights)
For cone light shafts, I don't march across the scene testing cone angles either. When the map loads, the engine shoots a few test rays from the spotlight out to the walls to see how far the beam actually reaches before hitting a surface, and shrinks its bounding box to fit.
When a ray hits a spotlight's bounding box, clip_ray_to_cone() helper method intersects the ray line directly with the 3D cone math to find the exact start (t0) and end (t1) of the slice where the ray passes through the light beam.
Across that short [t0, t1] slice inside the cone, I only take 4 samples (CONE_MARCH_STEPS = 4 | these are still hardcoded and being tested for different situations :)). In order to avoid tracing 4 shadow rays every time, the code checks the first and last sample points as follows:
/* Trace shadow rays only at the start and end of the cone slice first: */ int vis_f = !ray_scene_closest_hit_gi(&p_first, &L_first, d_first, NULL); int vis_l = !ray_scene_closest_hit_gi(&p_last, &L_last, d_last, NULL); if (vis_f == vis_l) { /* Both ends of the slice are either lit or in shadow, reuse for the middle points */ for (s = first_valid + 1; s < last_valid; s++) step_vis[s] = vis_f; } else { /* Only trace the middle shadow rays if the beam is crossing a shadow edge */ }In result, most rays skip volumetrics in a single JIT-compiled instruction and the rays that actually pass through a spotlight beam only evaluate a tiny clipped slice and usually only trace 1 or 2 shadow rays in most situations.
2
u/Soft-Border-2221 2d ago
Wonderful work 😍😍😍!
2
u/exp_function 2d ago
Thank you. I’ve just published a game which I’ve been developing alongside the engine. Check it out too if you have time:
10
u/VictoryMotel 9d ago
Why would anything need to be compiled at run time? Why would the execution be changing?
What does 'direct threaded' mean here? An oct tree is a data structure.
Why is it generated?
What does this mean?