git.lucas.co / cce-designer
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 }