cranpose-render-wgpu 0.1.164

WGPU renderer backend for Cranpose
Documentation
use std::{cell::RefCell, rc::Rc};

use cranpose_app_shell::AppShell;
use cranpose_core::location_key;
use cranpose_foundation::lazy::{LazyItems, LazyListScope, LazyListState, rememberLazyListState};
use cranpose_ui::{
    Color, LinearArrangement, Modifier, RenderEffect, TextStyle, composable,
    widgets::{Box, BoxSpec, LazyColumn, LazyColumnSpec, Text},
};
use cranpose_ui_graphics::{LiquidGlassRect, LiquidGlassSpec, TileMode, liquid_glass_effect};

const FRAME_WIDTH: u32 = 1080;
const FRAME_HEIGHT: u32 = 2244;
const WARMUP_FRAMES: usize = 60;

fn measured_frames() -> usize {
    std::env::var("PROFILE_FRAMES")
        .ok()
        .and_then(|raw| raw.parse().ok())
        .unwrap_or(240)
}
const SCROLL_DELTA_PER_FRAME: f32 = -30.0;

#[composable]
fn CardRow(index: usize) {
    let fill = if index.is_multiple_of(2) {
        Color(0.98, 0.98, 0.99, 1.0)
    } else {
        Color(0.94, 0.95, 0.97, 1.0)
    };
    Box(
        Modifier::empty()
            .fill_max_width()
            .height(280.0)
            .rounded_corners(24.0)
            .shadow(12.0)
            .background(fill)
            .padding(28.0),
        BoxSpec::new(),
        move || {
            Text(
                format!("Receipt {index} — Grocery Market"),
                Modifier::empty(),
                TextStyle::default(),
            );
        },
    );
}

fn chrome_blur_radius() -> f32 {
    std::env::var("PROFILE_BLUR_RADIUS")
        .ok()
        .and_then(|raw| raw.parse().ok())
        .unwrap_or(36.0)
}

fn chrome_glass_effect(rect_width: f32, rect_height: f32) -> RenderEffect {
    let optical = liquid_glass_effect(
        &LiquidGlassRect {
            left: 0.0,
            top: 0.0,
            width: rect_width,
            height: rect_height,
            tint_color: Color(0.9, 0.9, 0.95, 0.35),
        },
        &LiquidGlassSpec {
            corner_radius: 28.0,
            blur_radius: 0.0,
            ..LiquidGlassSpec::default()
        },
        rect_width,
        rect_height,
    );
    RenderEffect::blur_with_edge_treatment(chrome_blur_radius(), TileMode::Mirror).then(optical)
}

#[composable]
fn GlassBar(x: f32, y: f32, width: f32, height: f32) {
    Box(
        Modifier::empty()
            .offset(x, y)
            .width(width)
            .height(height)
            .rounded_corners(28.0)
            .shadow(8.0)
            .backdrop_effect(chrome_glass_effect(width, height))
            .padding(24.0),
        BoxSpec::new(),
        || {
            Box(
                Modifier::empty()
                    .width(180.0)
                    .height(32.0)
                    .background(Color(1.0, 1.0, 1.0, 0.8))
                    .rounded_corners(16.0),
                BoxSpec::new(),
                || {},
            );
        },
    );
}

#[composable]
fn CardListScene(list_state: LazyListState) {
    Box(
        Modifier::empty()
            .size_points(FRAME_WIDTH as f32, FRAME_HEIGHT as f32)
            .background(Color(0.85, 0.86, 0.88, 1.0)),
        BoxSpec::new(),
        move || {
            LazyColumn(
                Modifier::empty().fill_max_size().padding(32.0),
                list_state,
                LazyColumnSpec::new().vertical_arrangement(LinearArrangement::SpacedBy(28.0)),
                move |scope| {
                    scope.items(LazyItems::new(400).key(|i: usize| i as u64), CardRow);
                },
            );
            let bars: usize = std::env::var("PROFILE_BARS")
                .ok()
                .and_then(|raw| raw.parse().ok())
                .unwrap_or(2);
            if bars >= 1 {
                GlassBar(24.0, 40.0, FRAME_WIDTH as f32 - 48.0, 200.0);
            }
            if bars >= 2 {
                GlassBar(
                    24.0,
                    FRAME_HEIGHT as f32 - 240.0,
                    FRAME_WIDTH as f32 - 48.0,
                    200.0,
                );
            }
        },
    );
}

fn main() {
    let instance = wgpu::Instance::default();
    let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
        power_preference: wgpu::PowerPreference::HighPerformance,
        ..wgpu::RequestAdapterOptions::default()
    }))
    .expect("adapter");
    eprintln!(
        "adapter: {} ({:?}) timestamps={}",
        adapter.get_info().name,
        adapter.get_info().backend,
        adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY),
    );
    let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
        label: Some("Pass Timing Profile Device"),
        required_features: cranpose_render_wgpu::optional_device_features(&adapter),
        required_limits: wgpu::Limits::default(),
        experimental_features: wgpu::ExperimentalFeatures::disabled(),
        memory_hints: wgpu::MemoryHints::default(),
        trace: wgpu::Trace::Off,
    }))
    .expect("device");

    let mut renderer = cranpose_render_wgpu::WgpuRenderer::new(&[
        cranpose_render_common::software_text_raster::DEFAULT_SOFTWARE_TEXT_FONT_BYTES,
    ]);
    renderer.init_gpu(
        std::sync::Arc::new(device),
        std::sync::Arc::new(queue),
        wgpu::TextureFormat::Bgra8UnormSrgb,
        adapter.get_info().backend,
        adapter.get_downlevel_capabilities().flags,
    );

    let root_key = location_key(file!(), line!(), column!());
    let list_state: Rc<RefCell<Option<LazyListState>>> = Rc::new(RefCell::new(None));
    let list_state_for_app = Rc::clone(&list_state);
    let mut shell = AppShell::new(renderer, root_key, move || {
        let state = rememberLazyListState();
        *list_state_for_app.borrow_mut() = Some(state);
        CardListScene(state);
    });
    shell.set_viewport(FRAME_WIDTH as f32, FRAME_HEIGHT as f32);
    shell.set_buffer_size(FRAME_WIDTH, FRAME_HEIGHT);
    shell.update();

    let scroll = |shell: &mut AppShell<cranpose_render_wgpu::WgpuRenderer>| {
        let state = list_state
            .borrow()
            .as_ref()
            .copied()
            .expect("list state captured");
        shell.debug_enter_app_context(|| state.dispatch_scroll_delta(SCROLL_DELTA_PER_FRAME));
    };

    let target = {
        let device = shell.renderer().try_device().expect("device");
        device.create_texture(&wgpu::TextureDescriptor {
            label: Some("Profile Render Target"),
            size: wgpu::Extent3d {
                width: FRAME_WIDTH,
                height: FRAME_HEIGHT,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Bgra8UnormSrgb,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        })
    };
    let view = target.create_view(&wgpu::TextureViewDescriptor::default());

    for _ in 0..WARMUP_FRAMES {
        scroll(&mut shell);
        shell.update();
        shell
            .renderer()
            .render(&target, &view, FRAME_WIDTH, FRAME_HEIGHT)
            .expect("frame render");
    }
    let start = std::time::Instant::now();
    let measured_frames = measured_frames();
    for _ in 0..measured_frames {
        scroll(&mut shell);
        shell.update();
        shell
            .renderer()
            .render(&target, &view, FRAME_WIDTH, FRAME_HEIGHT)
            .expect("frame render");
    }
    let elapsed = start.elapsed().as_secs_f64();

    let report = shell.renderer().gpu_pass_timings();
    let stats = shell.renderer().last_frame_stats().expect("frame stats");
    println!(
        "frames={} wall_fps={:.1} report_frames={}",
        measured_frames,
        measured_frames as f64 / elapsed,
        report.frames,
    );
    println!(
        "counters: passes={} blur={} composite={} shadow_hit_px={:.2}MP layer_miss_px={:.2}MP",
        stats.pass_count,
        stats.blur_passes,
        stats.composite_passes,
        stats.shadow_shape_cache_hit_pixels as f64 / 1_000_000.0,
        stats.layer_cache_miss_pixels as f64 / 1_000_000.0,
    );
    let frames = f64::from(report.frames.max(1));
    let total_ms: f64 = report.entries.iter().map(|entry| entry.total_ms).sum();
    println!(
        "gpu_span={:.3}ms/frame occupancy={:.3}ms/frame",
        report.span_ms / frames,
        total_ms / frames,
    );
    for entry in &report.entries {
        println!(
            "  {:<40} {:>8.3}ms/frame  x{:>5.1}",
            entry.label,
            entry.total_ms / frames,
            entry.passes as f64 / frames,
        );
    }
}