graphic design tool
git clone https://git.lucas.co/cce-designer.git
src/vk_smoke.rs (18.8K)
1 //! Smoke test for the ash renderer: opens its own XDG window and drives
2 //! `VkRenderer` with designer-style 2D primitives — rounded window corners,
3 //! alpha-blended quads, a circle-clipped quad, a blur-behind plate (negative
4 //! alpha), and an animated color to prove continuous presentation.
5 //!
6 //! Run inside a Wayland session:
7 //! cargo run -p cce-designer --bin vk-smoke
8
9 use cce_ui::vk;
10
11 use smithay_client_toolkit::{
12 compositor::{CompositorHandler, CompositorState},
13 delegate_compositor, delegate_output, delegate_registry, delegate_xdg_shell,
14 delegate_xdg_window,
15 output::{OutputHandler, OutputState},
16 registry::{ProvidesRegistryState, RegistryState},
17 registry_handlers,
18 shell::{
19 xdg::{
20 window::{Window as XdgWindow, WindowConfigure, WindowDecorations, WindowHandler},
21 XdgShell,
22 },
23 WaylandSurface,
24 },
25 };
26 use wayland_client::{
27 globals::registry_queue_init,
28 protocol::{wl_output, wl_surface},
29 Connection, Proxy, QueueHandle,
30 };
31 use calloop_wayland_source::WaylandSource;
32
33 use cce_ui::engine::{quad_vertices, Vertex};
34 use glam::{Mat4, Vec3};
35 use cce_ui::cosmic_text::{Attrs, Buffer as TextBuffer, Family, Metrics, Shaping};
36 use cce_ui::cosmic_text::{FontSystem, SwashCache};
37 use vk::{Frame2D, ImageQuad, RtCamera, RtMaterial, RtTriangle, SceneDraw, TextSpan, Vertex3D, VkRenderer};
38
39 struct SmokeApp {
40 registry_state: RegistryState,
41 output_state: OutputState,
42 window: Option<XdgWindow>,
43 renderer: Option<VkRenderer>,
44 exit: bool,
45 configured: bool,
46 logical_size: (u32, u32),
47 scale: f64,
48 }
49
50 impl SmokeApp {
51 fn apply_size(&mut self) {
52 if let Some(renderer) = &mut self.renderer {
53 let pw = (self.logical_size.0 as f64 * self.scale) as u32;
54 let ph = (self.logical_size.1 as f64 * self.scale) as u32;
55 renderer.resize(pw, ph);
56 }
57 }
58 }
59
60 impl CompositorHandler for SmokeApp {
61 fn scale_factor_changed(
62 &mut self,
63 _conn: &Connection,
64 _qh: &QueueHandle<Self>,
65 surface: &wl_surface::WlSurface,
66 scale_factor: i32,
67 ) {
68 surface.set_buffer_scale(scale_factor);
69 self.scale = scale_factor as f64;
70 self.apply_size();
71 }
72
73 fn transform_changed(
74 &mut self,
75 _conn: &Connection,
76 _qh: &QueueHandle<Self>,
77 _surface: &wl_surface::WlSurface,
78 _new_transform: wl_output::Transform,
79 ) {
80 }
81
82 fn frame(
83 &mut self,
84 _conn: &Connection,
85 _qh: &QueueHandle<Self>,
86 _surface: &wl_surface::WlSurface,
87 _time: u32,
88 ) {
89 }
90
91 fn surface_enter(
92 &mut self,
93 _conn: &Connection,
94 _qh: &QueueHandle<Self>,
95 _surface: &wl_surface::WlSurface,
96 _output: &wl_output::WlOutput,
97 ) {
98 }
99
100 fn surface_leave(
101 &mut self,
102 _conn: &Connection,
103 _qh: &QueueHandle<Self>,
104 _surface: &wl_surface::WlSurface,
105 _output: &wl_output::WlOutput,
106 ) {
107 }
108 }
109
110 impl OutputHandler for SmokeApp {
111 fn output_state(&mut self) -> &mut OutputState {
112 &mut self.output_state
113 }
114
115 fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
116 fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
117 fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
118 }
119
120 impl WindowHandler for SmokeApp {
121 fn configure(
122 &mut self,
123 _conn: &Connection,
124 _qh: &QueueHandle<Self>,
125 _window: &XdgWindow,
126 configure: WindowConfigure,
127 _serial: u32,
128 ) {
129 if let (Some(w), Some(h)) = configure.new_size {
130 self.logical_size = (w.get(), h.get());
131 self.apply_size();
132 }
133 self.configured = true;
134 }
135
136 fn request_close(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _window: &XdgWindow) {
137 self.exit = true;
138 }
139 }
140
141 impl ProvidesRegistryState for SmokeApp {
142 fn registry(&mut self) -> &mut RegistryState {
143 &mut self.registry_state
144 }
145
146 registry_handlers![OutputState];
147 }
148
149 delegate_compositor!(SmokeApp);
150 delegate_output!(SmokeApp);
151 delegate_xdg_shell!(SmokeApp);
152 delegate_xdg_window!(SmokeApp);
153 delegate_registry!(SmokeApp);
154
155 /// Shape a line the same way the app does (bundled control-label font).
156 fn make_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> TextBuffer {
157 let mut buffer = TextBuffer::new(font_system, Metrics::new(size, size * 1.4));
158 let family = cce_ui::layout::control_label_font_parsed().0;
159 buffer.set_size(font_system, Some(2000.0), Some(200.0));
160 buffer.set_text(
161 font_system,
162 text,
163 Attrs::new().family(Family::Name(&family)),
164 Shaping::Advanced,
165 );
166 buffer.shape_until_scroll(font_system, true);
167 buffer
168 }
169
170 fn quad3(v: &mut Vec<Vertex3D>, a: [f32; 3], b: [f32; 3], c: [f32; 3], d: [f32; 3], col: [f32; 3]) {
171 for p in [a, b, c, a, c, d] {
172 v.push(Vertex3D { position: p, color: col });
173 }
174 }
175
176 /// Colored cube, CCW-from-outside winding (the 3D pipeline culls back faces).
177 fn cube_verts() -> Vec<Vertex3D> {
178 let s = 0.5;
179 let mut v = Vec::new();
180 quad3(&mut v, [-s, -s, s], [s, -s, s], [s, s, s], [-s, s, s], [0.85, 0.35, 0.30]);
181 quad3(&mut v, [s, -s, -s], [-s, -s, -s], [-s, s, -s], [s, s, -s], [0.30, 0.55, 0.90]);
182 quad3(&mut v, [s, -s, s], [s, -s, -s], [s, s, -s], [s, s, s], [0.90, 0.75, 0.25]);
183 quad3(&mut v, [-s, -s, -s], [-s, -s, s], [-s, s, s], [-s, s, -s], [0.35, 0.80, 0.45]);
184 quad3(&mut v, [-s, s, s], [s, s, s], [s, s, -s], [-s, s, -s], [0.80, 0.80, 0.85]);
185 quad3(&mut v, [-s, -s, -s], [s, -s, -s], [s, -s, s], [-s, -s, s], [0.45, 0.40, 0.60]);
186 v
187 }
188
189 /// Floor grid on the XZ plane, both windings (visible from above and below).
190 fn grid_verts() -> Vec<Vertex3D> {
191 let col = [0.22, 0.22, 0.28];
192 let (half, t) = (2.0, 0.01);
193 let mut v = Vec::new();
194 for i in -4i32..=4 {
195 let p = i as f32 * 0.5;
196 quad3(&mut v, [-half, 0.0, p - t], [half, 0.0, p - t], [half, 0.0, p + t], [-half, 0.0, p + t], col);
197 quad3(&mut v, [-half, 0.0, p + t], [half, 0.0, p + t], [half, 0.0, p - t], [-half, 0.0, p - t], col);
198 quad3(&mut v, [p - t, 0.0, -half], [p + t, 0.0, -half], [p + t, 0.0, half], [p - t, 0.0, half], col);
199 quad3(&mut v, [p + t, 0.0, -half], [p - t, 0.0, -half], [p - t, 0.0, half], [p + t, 0.0, half], col);
200 }
201 v
202 }
203
204 /// The designer's viewport-background quad: NDC corners, drawn
205 /// `screen_space`, filling the scissor with the (linear) bg color at the far
206 /// plane.
207 fn viewport_bg_verts() -> Vec<Vertex3D> {
208 let color = cce_ui::colors::to_linear_rgb([0.10, 0.10, 0.13]);
209 let mut v = Vec::new();
210 quad3(&mut v, [-1.0, -1.0, 0.0], [1.0, -1.0, 0.0], [1.0, 1.0, 0.0], [-1.0, 1.0, 0.0], color);
211 v
212 }
213
214 /// The RT-mode demo scene (`CCE_VK_SMOKE_RT=1`): the reference cube on a gray
215 /// floor beneath an emissive panel — enough for visible indirect light, color
216 /// bleed, and soft shadows once the accumulation converges.
217 fn rt_demo_scene() -> (Vec<RtTriangle>, Vec<RtMaterial>) {
218 let mut tris: Vec<RtTriangle> = Vec::new();
219 let mut mats: Vec<RtMaterial> = Vec::new();
220 let mat = |mats: &mut Vec<RtMaterial>, albedo: [f32; 3], emission: [f32; 3]| -> u32 {
221 let m = RtMaterial { albedo, emission };
222 if let Some(i) = mats.iter().position(|x| *x == m) {
223 i as u32
224 } else {
225 mats.push(m);
226 (mats.len() - 1) as u32
227 }
228 };
229
230 // The cube, from the same triangle list the raster pass draws.
231 for tri in cube_verts().chunks_exact(3) {
232 let material = mat(&mut mats, tri[0].color, [0.0; 3]);
233 tris.push(RtTriangle { p0: tri[0].position, p1: tri[1].position, p2: tri[2].position, material });
234 }
235
236 let quad = |tris: &mut Vec<RtTriangle>, c: [[f32; 3]; 4], material: u32| {
237 tris.push(RtTriangle { p0: c[0], p1: c[1], p2: c[2], material });
238 tris.push(RtTriangle { p0: c[0], p1: c[2], p2: c[3], material });
239 };
240 let floor = mat(&mut mats, [0.55, 0.55, 0.58], [0.0; 3]);
241 quad(
242 &mut tris,
243 [[-6.0, -0.5, -6.0], [6.0, -0.5, -6.0], [6.0, -0.5, 6.0], [-6.0, -0.5, 6.0]],
244 floor,
245 );
246 let panel = mat(&mut mats, [0.0; 3], [6.0, 5.6, 5.0]);
247 quad(
248 &mut tris,
249 [[-1.0, 2.2, -1.0], [1.0, 2.2, -1.0], [1.0, 2.2, 1.0], [-1.0, 2.2, 1.0]],
250 panel,
251 );
252 (tris, mats)
253 }
254
255 /// Designer-style 2D chrome in logical coordinates. No full-window background —
256 /// the 3D viewport (copied in as the backdrop) shows through everywhere the UI
257 /// doesn't paint, exactly like the app.
258 fn build_scene(lw: f32, lh: f32, scale: f32, t: f32) -> Vec<Vertex> {
259 let mut verts: Vec<Vertex> = Vec::new();
260
261 // Header bar + side panel chrome.
262 verts.extend(quad_vertices(0.0, 0.0, lw, 36.0, lw, lh, [0.13, 0.13, 0.17, 1.0]));
263 verts.extend(quad_vertices(0.0, 36.0, 56.0, lh - 36.0, lw, lh, [0.11, 0.11, 0.145, 1.0]));
264
265 // Floating alpha-blended quads over the 3D scene.
266 let palette = [[0.90, 0.35, 0.30, 0.9], [0.95, 0.75, 0.25, 0.9], [0.35, 0.80, 0.45, 0.9]];
267 for (i, color) in palette.iter().enumerate() {
268 verts.extend(quad_vertices(70.0 + i as f32 * 50.0, 46.0, 40.0, 40.0, lw, lh, *color));
269 }
270
271 // Animated quad: hue-cycled color proves per-frame uploads.
272 let pulse = |phase: f32| 0.5 + 0.5 * (t * 2.0 + phase).sin();
273 verts.extend(quad_vertices(70.0, 96.0, 80.0, 30.0, lw, lh, [pulse(0.0), pulse(2.1), pulse(4.2), 1.0]));
274
275 // Circle-clipped quad (clip center/radius in physical pixels).
276 let (ccx, ccy, ccr) = (320.0f32, 90.0f32, 26.0f32);
277 let clip = [ccx * scale, ccy * scale, ccr * scale];
278 for v in quad_vertices(ccx - 30.0, ccy - 30.0, 60.0, 60.0, lw, lh, [0.85, 0.45, 0.85, 1.0]) {
279 verts.push(Vertex { clip_circle: clip, ..v });
280 }
281
282 // Blur-behind plate (negative alpha): blurs the real 3D backdrop beneath it.
283 verts.extend(quad_vertices(110.0, 140.0, 200.0, 70.0, lw, lh, [0.45, 0.55, 0.95, -0.55]));
284
285 verts
286 }
287
288 fn main() {
289 env_logger::Builder::from_default_env()
290 .filter_level(log::LevelFilter::Info)
291 .init();
292
293 let conn = Connection::connect_to_env().expect("No Wayland display");
294 let (globals, event_queue) = registry_queue_init::<SmokeApp>(&conn).unwrap();
295 let qh = event_queue.handle();
296
297 let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
298 let xdg_shell_state = XdgShell::bind(&globals, &qh).unwrap();
299 let output_state = OutputState::new(&globals, &qh);
300
301 let mut app = SmokeApp {
302 registry_state: RegistryState::new(&globals),
303 output_state,
304 window: None,
305 renderer: None,
306 exit: false,
307 configured: false,
308 logical_size: (720, 460),
309 scale: 1.0,
310 };
311
312 let mut event_loop = calloop::EventLoop::<SmokeApp>::try_new().unwrap();
313 WaylandSource::new(conn.clone(), event_queue)
314 .insert(event_loop.handle())
315 .unwrap();
316
317 // Roundtrip so outputs (and their scales) are known.
318 event_loop.dispatch(std::time::Duration::ZERO, &mut app).unwrap();
319 app.scale = cce_ui::wayland::detect_scale_factor(&app.output_state);
320
321 let wl_surface = compositor_state.create_surface(&qh);
322 wl_surface.set_buffer_scale(app.scale as i32);
323 let window = xdg_shell_state.create_window(wl_surface.clone(), WindowDecorations::None, &qh);
324 window.set_title("vk-smoke");
325 window.set_app_id("cce-designer-vk-smoke");
326 window.set_min_size(Some((360, 240)));
327 window.commit();
328 app.window = Some(window);
329
330 // Wait for the first configure before creating the swapchain (a Vulkan
331 // present attaches a buffer, which is illegal pre-configure).
332 while !app.configured && !app.exit {
333 event_loop
334 .dispatch(std::time::Duration::from_millis(16), &mut app)
335 .unwrap();
336 }
337
338 let display_ptr = conn.backend().display_id().as_ptr() as *mut std::ffi::c_void;
339 let surface_ptr = wl_surface.id().as_ptr() as *mut std::ffi::c_void;
340 let pw = (app.logical_size.0 as f64 * app.scale) as u32;
341 let ph = (app.logical_size.1 as f64 * app.scale) as u32;
342 let radius = cce_ui::color::root_plate_corner_radius() * app.scale as f32;
343 // A lost surface is the compositor going away under the test, not a
344 // renderer fault: say so and fail, rather than panic.
345 match unsafe { VkRenderer::try_new(display_ptr, surface_ptr, pw, ph, radius) } {
346 Ok(r) => app.renderer = Some(r),
347 Err(lost) => {
348 log::error!("vk-smoke: {lost}");
349 std::process::exit(1);
350 }
351 }
352 log::info!("vk-smoke: renderer up at {pw}x{ph} (scale {})", app.scale);
353
354 // 3D meshes for the viewport scene.
355 let (bg_mesh, grid_mesh, cube_mesh) = {
356 let r = app.renderer.as_mut().unwrap();
357 (
358 r.create_mesh(&viewport_bg_verts()),
359 r.create_mesh(&grid_verts()),
360 r.create_mesh(&cube_verts()),
361 )
362 };
363
364 // Text stack: same bundled fonts as the app, shaped once up front.
365 let mut font_system = cce_ui::create_font_system();
366 let mut swash_cache = SwashCache::new();
367 let title_buf = make_buffer(&mut font_system, "vk-smoke — ash text stage", 14.0);
368 let body_buf = make_buffer(
369 &mut font_system,
370 "cosmic-text shaping → swash raster → vulkan glyph atlas",
371 13.0,
372 );
373 let clipped_buf = make_buffer(
374 &mut font_system,
375 "this line is clipped mid-glyph by span bounds ###########",
376 13.0,
377 );
378
379 // User image: a checkerboard drawn between the animated quad (under) and
380 // the blur plate (over) via z_before.
381 let mut px = vec![0u8; 64 * 64 * 4];
382 for y in 0..64usize {
383 for x in 0..64usize {
384 let on = ((x / 8) + (y / 8)) % 2 == 0;
385 let i = (y * 64 + x) * 4;
386 px[i..i + 4].copy_from_slice(if on { &[240, 90, 60, 255] } else { &[40, 200, 220, 255] });
387 }
388 }
389 let checker = vk::upload_rgba(px, 64, 64);
390
391 // RT mode: the pane runs the phase-2 compute path tracer instead of the
392 // raster 3D pass. Static camera, so progressive accumulation visibly
393 // converges from noise to a clean render.
394 let rt_mode = std::env::var_os("CCE_VK_SMOKE_RT").is_some();
395 if rt_mode {
396 let (tris, mats) = rt_demo_scene();
397 log::info!("vk-smoke: RT mode — {} triangles, {} materials", tris.len(), mats.len());
398 app.renderer.as_mut().unwrap().set_rt_scene(&tris, &mats);
399 }
400
401 let start = std::time::Instant::now();
402 while !app.exit {
403 event_loop
404 .dispatch(std::time::Duration::from_millis(16), &mut app)
405 .unwrap();
406 if app.exit {
407 break;
408 }
409 let (lw, lh) = (app.logical_size.0 as f32, app.logical_size.1 as f32);
410 let t = start.elapsed().as_secs_f32();
411 let s = app.scale as f32;
412 let verts = build_scene(lw, lh, s, t);
413 let pulse = 0.75 + 0.25 * (t * 3.0).sin();
414 let spans = [
415 TextSpan {
416 buffer: &title_buf,
417 left: 12.0 * s,
418 top: 9.0 * s,
419 scale: s,
420 bounds: None,
421 default_color: [0.92, 0.92, 0.95, 1.0],
422 rotation: None,
423 clip_circle: [0.0; 3],
424 clip_extents: [0.0; 2],
425 },
426 TextSpan {
427 buffer: &body_buf,
428 left: 120.0 * s,
429 top: 158.0 * s,
430 scale: s,
431 bounds: None,
432 // Animated color: proves per-frame vertex rebuilds.
433 default_color: [pulse, 0.80, 0.55, 1.0],
434 rotation: None,
435 clip_circle: [0.0; 3],
436 clip_extents: [0.0; 2],
437 },
438 TextSpan {
439 buffer: &clipped_buf,
440 left: 66.0 * s,
441 top: 215.0 * s,
442 scale: s,
443 bounds: Some([
444 (66.0 * s) as i32,
445 (215.0 * s) as i32,
446 (260.0 * s) as i32,
447 (233.0 * s) as i32,
448 ]),
449 default_color: [0.70, 0.85, 1.00, 1.0],
450 rotation: None,
451 clip_circle: [0.0; 3],
452 clip_extents: [0.0; 2],
453 },
454 ];
455 // build_scene vertex layout: blur plate is the last 6 verts; the image
456 // sorts just before it (above everything else, beneath the plate).
457 let image_quads = [ImageQuad {
458 image: checker,
459 rect: (250.0 * s, 115.0 * s, 90.0 * s, 90.0 * s),
460 alpha: 1.0,
461 z_before: (verts.len() as u32).saturating_sub(6),
462 clip: None,
463 }];
464 if let Some(renderer) = &mut app.renderer {
465 // 3D pane: right of the side panel, below the header (physical px).
466 // Full-frame NDC scissored to the pane, exactly like the app.
467 let pane = (
468 (56.0 * s) as u32,
469 (36.0 * s) as u32,
470 ((lw - 56.0) * s) as u32,
471 ((lh - 36.0) * s) as u32,
472 );
473 let aspect = (lw - 56.0) / (lh - 36.0);
474 let proj = Mat4::perspective_rh(0.9, aspect, 0.1, 100.0);
475 let view = Mat4::look_at_rh(Vec3::new(2.5, 1.8, 2.5), Vec3::ZERO, Vec3::Y);
476 if rt_mode {
477 let inv_mvp = (proj * view).inverse().to_cols_array_2d();
478 renderer.stage_rt(pane, RtCamera { inv_mvp });
479 } else {
480 let model = Mat4::from_rotation_y(t * 0.8);
481 let mvp = (proj * view * model).to_cols_array_2d();
482 renderer.stage_scene(
483 pane,
484 vec![
485 SceneDraw { mesh: bg_mesh, mvp: Mat4::IDENTITY.to_cols_array_2d(), wireframe: false, wire_tint: [0.0; 4], opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: None, screen_space: true },
486 SceneDraw { mesh: grid_mesh, mvp, wireframe: false, wire_tint: [0.0; 4], opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: None, screen_space: false },
487 SceneDraw { mesh: cube_mesh, mvp, wireframe: false, wire_tint: [0.0; 4], opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: None, screen_space: false },
488 ],
489 );
490 }
491 renderer.prepare_text(&mut font_system, &mut swash_cache, &spans);
492 // FIFO present paces this loop to the display's refresh rate.
493 renderer.draw_frame_2d(Frame2D {
494 verts: &verts,
495 batches: &[],
496 overlay_verts: &[],
497 images: &image_quads,
498 plate_features: &[],
499 clear_color: [0.0; 4],
500 damage: None,
501 });
502 }
503 }
504
505 // Tear down the renderer (and its swapchain) before the surface dies.
506 app.renderer.take();
507 }