ray_query

Builds a BLAS for one triangle plus a TLAS, then traces primary rays with inline RayQuery from a [goldy_compute] entry point and writes hits straight into the swapchain. No ray tracing pipeline or shader binding table is involved.

cargo run --features examples --example ray_query

What it demonstrates

  • Acceleration structure build (BLAS and TLAS) inside a scheme
  • Inline ray query from a compute entry point
  • Compute-to-surface output

Notes

The example exits 0 when RuntimeCapabilities::ray_query is false, and on the WebGPU backend, where Slang's WGSL target has no TraceRayInline.

Source

examples/ray_query.rs:

//! Compute ray query — a TLAS of one triangle, primary rays into the swapchain.
//!
//! Skips (exit 0) when `RuntimeCapabilities::ray_query` is false, or on WebGPU
//! (Slang WGSL has no `TraceRayInline`).
//!
//! Run with: cargo run --example ray_query --features examples

use anyhow::Result;
use goldy::{
    types::{BackendType, BufferFlags},
    AccelInstance, AccelerationStructure, Buffer, BufferKind, ComputePipeline, DepositTarget, DepositTransaction,
    Instance, MemoryExchange, NodeAccess, RequestAdapterOptions, RuntimeDescriptor, Scheme, ShaderModule,
    SurfaceConfig, SurfaceExchange, Texture, Transaction,
};
use std::ops::Shr;
use std::sync::Arc;
use std::time::Instant;
use winit::{
    application::ApplicationHandler,
    event::WindowEvent,
    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
    keyboard::{Key, NamedKey},
    window::{Window, WindowId},
};
mod common;
use common::CaptureDump;

const RAY_SHADER: &str = r#"
import goldy_exp;


[goldy_compute]
[numthreads(8, 8, 1)]
void cs_main(BufRO<Uniforms> uniforms_buf, Accel scene, DirectSpatial<float4> output, ThreadId tid) {
    Uniforms u = uniforms_buf[0];
    if (tid.x >= u.width || tid.y >= u.height)
        return;

    float2 uv = (float2(tid.xy) + 0.5) / float2(u.width, u.height);
    float2 ndc = uv * 2.0 - 1.0;
    ndc.y = -ndc.y;

    RayDesc ray;
    ray.Origin = float3(0.0, 0.0, -2.0);
    ray.TMin = 0.001;
    ray.Direction = normalize(float3(ndc.x, ndc.y, 1.0));
    ray.TMax = 100.0;

    RayQuery<RAY_FLAG_FORCE_OPAQUE> q;
    q.TraceRayInline(scene, RAY_FLAG_FORCE_OPAQUE, 0xFF, ray);
    q.Proceed();

    float3 col = float3(0.05, 0.06, 0.12);
    if (q.CommittedStatus() == COMMITTED_TRIANGLE_HIT) {
        float2 bary = q.CommittedTriangleBarycentrics();
        col = float3(bary.x, bary.y, 1.0 - bary.x - bary.y);
        col += 0.15 * sin(u.time);
    }
    output[tid.xy] = float4(col, 1.0);
}
"#;

#[goldy::gpu]
struct Uniforms {
    width: u32,
    height: u32,
    time: f32,
}

const INITIAL_WIDTH: u32 = 800;
const INITIAL_HEIGHT: u32 = 600;

fn main() -> Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
        )
        .init();

    if common::capture_requested() {
        let warmup = warm_gpu()?;
        let mut app = App {
            warmup: Some(warmup),
            state: None,
        };
        app.init(None)?;
        let state = app.state.as_mut().expect("capture state");
        while !state.capture.as_ref().is_none_or(CaptureDump::finished) {
            render_frame(state)?;
        }
        return Ok(());
    }

    println!("Goldy — Compute Ray Query");
    println!("=========================");
    println!("Press Escape to exit\n");

    let warmup = warm_gpu()?;
    let event_loop = EventLoop::new()?;
    event_loop.set_control_flow(ControlFlow::Poll);
    let mut app = App {
        warmup: Some(warmup),
        state: None,
    };
    event_loop.run_app(&mut app)?;
    Ok(())
}

struct GpuWarmup {
    ctx: goldy::Context,
    compute_pipeline: ComputePipeline,
    device: Arc<goldy::Runtime>,
    verts: Buffer,
    blas: AccelerationStructure,
    tlas: AccelerationStructure,
}

fn warm_gpu() -> Result<GpuWarmup> {
    let instance = Instance::new()?;
    let device = Arc::new(
        instance
            .request_adapter(&RequestAdapterOptions::default())?
            .request_runtime(&RuntimeDescriptor::default())?,
    );
    if !device.capabilities().ray_query {
        println!("skip: RuntimeCapabilities::ray_query is false on this adapter");
        std::process::exit(0);
    }
    if device.backend_type() == BackendType::WebGpu {
        println!("skip: WebGPU Slang path has no TraceRayInline");
        std::process::exit(0);
    }
    let ctx = device.create_context()?;
    let shader = ShaderModule::from_slang_with_gpu_types(&device, RAY_SHADER, &[Uniforms::GPU_TYPE])?;
    let compute_pipeline = ComputePipeline::new(&device, &shader)?;
    let positions: [[f32; 3]; 3] = [[0.0, 0.5, 0.0], [-0.7, -0.5, 0.0], [0.7, -0.5, 0.0]];
    let verts =
        device.acquire_buffer_with_data_and_flags(&positions, BufferKind::Scattered, BufferFlags::ACCEL_INPUT)?;
    let blas = AccelerationStructure::blas_triangles(&device, 1, 3, 12)?;
    let tlas = AccelerationStructure::tlas(&device, 1)?;
    Ok(GpuWarmup {
        ctx,
        compute_pipeline,
        device,
        verts,
        blas,
        tlas,
    })
}

#[derive(Default)]
struct App {
    warmup: Option<GpuWarmup>,
    state: Option<RenderState>,
}

struct RenderState {
    window: Option<Arc<Window>>,
    ctx: goldy::Context,
    surface: Option<SurfaceExchange>,
    present: Option<Transaction>,
    capture: Option<CaptureDump>,
    readback: Option<Texture>,
    scheme: Scheme,
    compute_pipeline: ComputePipeline,
    verts: Buffer,
    blas: AccelerationStructure,
    tlas: AccelerationStructure,
    uniform_buffer: Buffer,
    upload_scheme: Scheme,
    uniform_deposit: DepositTransaction,
    start_time: Instant,
    frame_count: u32,
}

#[allow(clippy::too_many_arguments)]
fn record_scheme(
    scheme: &mut Scheme,
    pipeline: &ComputePipeline,
    uniform: &Buffer,
    verts: &Buffer,
    blas: &AccelerationStructure,
    tlas: &AccelerationStructure,
    width: u32,
    height: u32,
    surface: Option<&SurfaceExchange>,
    readback: Option<&Texture>,
) -> Result<Option<Transaction>> {
    scheme.build_blas(blas, verts.whole(), 3, 12, None)?;
    let identity = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0];
    scheme.build_tlas(
        tlas,
        &[AccelInstance {
            blas,
            transform: identity,
            mask: 0xFF,
            custom_index: 0,
        }],
    )?;
    let wg_x = width.div_ceil(8);
    let wg_y = height.div_ceil(8);
    if let Some(surface) = surface {
        let (lease, present) = surface.bind_destination(scheme)?;
        scheme
            .node("rays", pipeline)
            .with_parcel(uniform, NodeAccess::Read)
            .with_parcel(tlas, NodeAccess::Read)
            .with_present(&lease)
            .dispatch(wg_x, wg_y, 1);
        Ok(Some(present))
    } else {
        let target = readback.expect("capture readback");
        scheme
            .node("rays", pipeline)
            .with_parcel(uniform, NodeAccess::Read)
            .with_parcel(tlas, NodeAccess::Read)
            .with_parcel(target, NodeAccess::Write)
            .dispatch(wg_x, wg_y, 1);

        Ok(None)
    }
}

fn output_size(state: &RenderState) -> (u32, u32) {
    if let Some(surface) = &state.surface {
        surface.size()
    } else {
        state.capture.as_ref().expect("capture").size()
    }
}

fn rebuild_scheme(state: &mut RenderState, width: u32, height: u32) {
    let mut scheme = Scheme::new(&state.ctx);
    let present = record_scheme(
        &mut scheme,
        &state.compute_pipeline,
        &state.uniform_buffer,
        &state.verts,
        &state.blas,
        &state.tlas,
        width,
        height,
        state.surface.as_ref(),
        state.readback.as_ref(),
    )
    .expect("failed to record scheme");
    state.present = present;
    state.scheme = scheme;
}

impl App {
    fn init(&mut self, window: Option<Arc<Window>>) -> Result<()> {
        let warmup = self
            .warmup
            .take()
            .ok_or_else(|| anyhow::anyhow!("GPU warmup state missing"))?;
        let GpuWarmup {
            ctx,
            compute_pipeline,
            device,
            verts,
            blas,
            tlas,
        } = warmup;

        let (surface, capture, readback, width, height) = if let Some(window) = window.as_deref() {
            let surface = SurfaceExchange::new(&ctx, window, SurfaceConfig::default())?;
            let (width, height) = surface.size();
            (Some(surface), None, None, width, height)
        } else {
            let capture = CaptureDump::from_env()?;
            let (width, height) = capture.size();
            let readback = common::capture_readback(&device, width, height)?;
            (None, Some(capture), Some(readback), width, height)
        };

        let uniform_buffer = device.acquire_buffer_with_data(
            &[Uniforms {
                width,
                height,
                time: 0.0,
            }],
            BufferKind::Scattered,
        )?;

        let mut scheme = Scheme::new(&ctx);
        let present = record_scheme(
            &mut scheme,
            &compute_pipeline,
            &uniform_buffer,
            &verts,
            &blas,
            &tlas,
            width,
            height,
            surface.as_ref(),
            readback.as_ref(),
        )?;

        let mut upload_scheme = Scheme::new(&ctx);
        let uniform_deposit = MemoryExchange::new(&ctx).bind_deposit(
            &mut upload_scheme,
            DepositTarget::buffer(&uniform_buffer, std::mem::size_of::<Uniforms>() as u64),
        )?;

        self.state = Some(RenderState {
            window,
            ctx,
            surface,
            present,
            capture,
            readback,
            scheme,
            compute_pipeline,
            verts,
            blas,
            tlas,
            uniform_buffer,
            upload_scheme,
            uniform_deposit,
            start_time: Instant::now(),
            frame_count: 0,
        });
        Ok(())
    }
}

impl Drop for RenderState {
    fn drop(&mut self) {
        let elapsed = self.start_time.elapsed().as_secs_f64();
        let fps = if elapsed > 0.0 {
            self.frame_count as f64 / elapsed
        } else {
            0.0
        };
        println!(
            "GOLDY_PERF: frames={} elapsed={elapsed:.2}s avg_fps={fps:.1}",
            self.frame_count
        );
    }
}

impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.state.is_some() {
            return;
        }
        let attrs = common::hidden_window("Goldy — Compute Ray Query", INITIAL_WIDTH, INITIAL_HEIGHT);
        let window = Arc::new(event_loop.create_window(attrs).unwrap());
        if let Err(e) = self.init(Some(window.clone())) {
            tracing::error!("Failed to initialize: {}", e);
            event_loop.exit();
            return;
        }
        if let Some(state) = &mut self.state {
            if let Err(e) = render_frame(state) {
                tracing::error!("First frame error: {e}");
            }
        }
        common::reveal_window(&window);
        window.request_redraw();
    }

    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
        if let Some(state) = &self.state {
            common::exit_if_timed_out(event_loop, state.start_time);
        }
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
        let Some(state) = &mut self.state else {
            return;
        };
        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::KeyboardInput { event, .. } if event.state.is_pressed() => {
                if matches!(event.logical_key.as_ref(), Key::Named(NamedKey::Escape)) {
                    event_loop.exit();
                }
            }
            WindowEvent::Resized(new_size) if new_size.width > 0 && new_size.height > 0 => {
                if let Some(surface) = &state.surface {
                    let _ = surface.resize(new_size.width, new_size.height);
                    rebuild_scheme(state, new_size.width, new_size.height);
                }
                if let Some(window) = &state.window {
                    window.request_redraw();
                }
            }
            WindowEvent::RedrawRequested => {
                if let Err(e) = render_frame(state) {
                    tracing::error!("Render error: {}", e);
                }
                if let Some(window) = &state.window {
                    window.request_redraw();
                }
            }
            _ => {}
        }
    }
}

fn render_frame(state: &mut RenderState) -> Result<()> {
    state.frame_count += 1;
    let (width, height) = output_size(state);
    if width == 0 || height == 0 {
        return Ok(());
    }
    let uniforms = Uniforms {
        width,
        height,
        time: state
            .capture
            .as_ref()
            .map(CaptureDump::time)
            .unwrap_or_else(|| state.start_time.elapsed().as_secs_f32()),
    };
    (&state.uniform_deposit << &uniforms)?;
    state.upload_scheme.submit()?;
    let mut submission = state.scheme.submit()?;
    if let Some(present) = &state.present {
        (&mut submission >> present).take()?;
    } else {
        let pixels = (&mut submission >> state.readback.as_ref().unwrap())
            .take::<u8>()?
            .to_vec();
        state.capture.as_mut().unwrap().write_rgba(&pixels)?;
    }
    Ok(())
}

The example pulls in examples/common.rs — see Shared Helpers.

The Slang source is inline in the example above.