← Relighting

examples/relight_pixels.rs

Build-time source snapshot · Plain text

1//! Focused native GPU image checks. This is not a frame-rate benchmark.2use pajama_gaussian_lab::relight::{RelightFrame as Frame, RelightPass};3use std::{error::Error, fs};45fn main() -> Result<(), Box<dyn Error>> {6    pollster::block_on(run())7}89async fn run() -> Result<(), Box<dyn Error>> {10    let mut descriptor = wgpu::InstanceDescriptor::new_without_display_handle();11    if cfg!(target_os = "windows") {12        descriptor.backends = wgpu::Backends::DX12;13    }14    let instance = wgpu::Instance::new(descriptor);15    let adapter = instance16        .request_adapter(&wgpu::RequestAdapterOptions {17            power_preference: wgpu::PowerPreference::HighPerformance,18            compatible_surface: None,19            force_fallback_adapter: false,20            apply_limit_buckets: false,21        })22        .await?;23    let info = adapter.get_info();24    let (device, queue) = adapter25        .request_device(&wgpu::DeviceDescriptor::default())26        .await?;27    let (width, height) = (256_u32, 192_u32);28    let extent = wgpu::Extent3d {29        width,30        height,31        depth_or_array_layers: 1,32    };33    let format = if std::env::args().any(|arg| arg == "--srgb") {34        wgpu::TextureFormat::Rgba8UnormSrgb35    } else {36        wgpu::TextureFormat::Rgba8Unorm37    };38    let texture = device.create_texture(&wgpu::TextureDescriptor {39        label: Some("relight-pixel-target"),40        size: extent,41        mip_level_count: 1,42        sample_count: 1,43        dimension: wgpu::TextureDimension::D2,44        format,45        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,46        view_formats: &[],47    });48    let target = texture.create_view(&Default::default());49    let stride = (width * 4).div_ceil(256) * 256;50    let readback = device.create_buffer(&wgpu::BufferDescriptor {51        label: Some("relight-readback"),52        size: u64::from(stride * height),53        usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,54        mapped_at_creation: false,55    });56    let mut renderer = RelightPass::new(&device, format);57    let base = Frame::default();58    let cases = [59        ("combined", base),60        (61            "light-right",62            Frame {63                azimuth: 1.0,64                ..base65            },66        ),67        (68            "dark",69            Frame {70                intensity: 0.0,71                ..base72            },73        ),74        ("diffuse", Frame { mode: 1, ..base }),75        (76            "metal-diffuse",77            Frame {78                mode: 1,79                metallic: 1.0,80                ..base81            },82        ),83        ("specular", Frame { mode: 2, ..base }),84        (85            "rough-specular",86            Frame {87                mode: 2,88                roughness: 0.9,89                ..base90            },91        ),92        ("albedo", Frame { mode: 4, ..base }),93        (94            "albedo-dark",95            Frame {96                mode: 4,97                intensity: 0.0,98                azimuth: 2.0,99                ..base100            },101        ),102        ("deform", Frame { time: 0.25, ..base }),103        (104            "bad-normals",105            Frame {106                time: 0.25,107                wrong_normals: true,108                ..base109            },110        ),111        ("orbit", Frame { yaw: 0.8, ..base }),112        ("normals", Frame { mode: 3, ..base }),113    ];114    fs::create_dir_all("artifacts/relight")?;115    let mut images = std::collections::BTreeMap::new();116    let mut reports = Vec::new();117    for (name, frame) in cases {118        let mut encoder = device.create_command_encoder(&Default::default());119        renderer.render(120            &device,121            &queue,122            &mut encoder,123            &target,124            [width, height],125            frame,126        )?;127        encoder.copy_texture_to_buffer(128            wgpu::TexelCopyTextureInfo {129                texture: &texture,130                mip_level: 0,131                origin: wgpu::Origin3d::ZERO,132                aspect: wgpu::TextureAspect::All,133            },134            wgpu::TexelCopyBufferInfo {135                buffer: &readback,136                layout: wgpu::TexelCopyBufferLayout {137                    offset: 0,138                    bytes_per_row: Some(stride),139                    rows_per_image: Some(height),140                },141            },142            extent,143        );144        queue.submit([encoder.finish()]);145        let (send, receive) = std::sync::mpsc::channel();146        readback147            .slice(..)148            .map_async(wgpu::MapMode::Read, move |result| {149                let _ = send.send(result);150            });151        device.poll(wgpu::PollType::wait_indefinitely())?;152        receive.recv()??;153        let mapped = readback.slice(..).get_mapped_range()?;154        let mut rgba = Vec::new();155        for row in mapped.chunks_exact(stride as usize) {156            rgba.extend_from_slice(&row[..(width * 4) as usize]);157        }158        drop(mapped);159        readback.unmap();160        let colored = rgba161            .chunks_exact(4)162            .filter(|p| p[..3].iter().any(|v| *v > 3))163            .count();164        let center = rgba[((height / 2 * width + width / 2) * 4) as usize..][..4].to_vec();165        let mut png = png::Encoder::new(166            fs::File::create(format!("artifacts/relight/{name}.png"))?,167            width,168            height,169        );170        png.set_color(png::ColorType::Rgba);171        png.set_depth(png::BitDepth::Eight);172        png.write_header()?.write_image_data(&rgba)?;173        reports174            .push(serde_json::json!({"name":name,"colored_pixels":colored,"center_rgba":center}));175        images.insert(name, rgba);176    }177    let changed = |a: &str, b: &str| {178        images[a]179            .iter()180            .zip(&images[b])181            .filter(|(a, b)| a.abs_diff(**b) > 2)182            .count()183    };184    assert_eq!(185        images["albedo"], images["albedo-dark"],186        "Albedo must not contain light"187    );188    assert_eq!(189        images["dark"], images["metal-diffuse"],190        "Metal has zero diffuse term"191    );192    for (a, b) in [193        ("combined", "light-right"),194        ("combined", "dark"),195        ("specular", "rough-specular"),196        ("deform", "bad-normals"),197        ("combined", "orbit"),198    ] {199        assert!(200            changed(a, b) > 500,201            "{a}/{b}: insufficient image difference"202        );203    }204    let center = ((height / 2 * width + width / 2) * 4) as usize;205    for (channel, expected) in [195_u8, 104, 60].into_iter().enumerate() {206        assert!(207            images["albedo"][center + channel].abs_diff(expected) <= 1,208            "Base color must be sRGB encoded exactly once"209        );210    }211    assert!(images["combined"][center] > 80, "Expected a lit surface");212    let report = serde_json::json!({"schema":"pajama.relight.pixel-check.v1","adapter":info.name,"backend":format!("{:?}",info.backend),"resolution":[width,height],"passed":true,"cases":reports,"claims":["light changes radiance","roughness changes highlight","albedo independent of light","metal diffuse zero","normal transform changes shading","camera changes pixels"],"note":"Native offscreen checks; not all-platform certification or paper reproduction."});213    fs::write(214        "artifacts/relight/report.json",215        serde_json::to_string_pretty(&report)?,216    )?;217    println!("{}", serde_json::to_string_pretty(&report)?);218    Ok(())219}220