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,
);
}
}