git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

src/vk_smoke.rs (18.4K)

  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: z=9.99 triggers the shader's
205 /// far-plane special case, filling the scissor with the (linear) bg color.
206 fn viewport_bg_verts() -> Vec<Vertex3D> {
207     let color = cce_ui::colors::to_linear_rgb([0.10, 0.10, 0.13]);
208     let mut v = Vec::new();
209     quad3(&mut v, [-1.0, -1.0, 9.99], [1.0, -1.0, 9.99], [1.0, 1.0, 9.99], [-1.0, 1.0, 9.99], color);
210     v
211 }
212 
213 /// The RT-mode demo scene (`CCE_VK_SMOKE_RT=1`): the reference cube on a gray
214 /// floor beneath an emissive panel — enough for visible indirect light, color
215 /// bleed, and soft shadows once the accumulation converges.
216 fn rt_demo_scene() -> (Vec<RtTriangle>, Vec<RtMaterial>) {
217     let mut tris: Vec<RtTriangle> = Vec::new();
218     let mut mats: Vec<RtMaterial> = Vec::new();
219     let mat = |mats: &mut Vec<RtMaterial>, albedo: [f32; 3], emission: [f32; 3]| -> u32 {
220         let m = RtMaterial { albedo, emission };
221         if let Some(i) = mats.iter().position(|x| *x == m) {
222             i as u32
223         } else {
224             mats.push(m);
225             (mats.len() - 1) as u32
226         }
227     };
228 
229     // The cube, from the same triangle list the raster pass draws.
230     for tri in cube_verts().chunks_exact(3) {
231         let material = mat(&mut mats, tri[0].color, [0.0; 3]);
232         tris.push(RtTriangle { p0: tri[0].position, p1: tri[1].position, p2: tri[2].position, material });
233     }
234 
235     let quad = |tris: &mut Vec<RtTriangle>, c: [[f32; 3]; 4], material: u32| {
236         tris.push(RtTriangle { p0: c[0], p1: c[1], p2: c[2], material });
237         tris.push(RtTriangle { p0: c[0], p1: c[2], p2: c[3], material });
238     };
239     let floor = mat(&mut mats, [0.55, 0.55, 0.58], [0.0; 3]);
240     quad(
241         &mut tris,
242         [[-6.0, -0.5, -6.0], [6.0, -0.5, -6.0], [6.0, -0.5, 6.0], [-6.0, -0.5, 6.0]],
243         floor,
244     );
245     let panel = mat(&mut mats, [0.0; 3], [6.0, 5.6, 5.0]);
246     quad(
247         &mut tris,
248         [[-1.0, 2.2, -1.0], [1.0, 2.2, -1.0], [1.0, 2.2, 1.0], [-1.0, 2.2, 1.0]],
249         panel,
250     );
251     (tris, mats)
252 }
253 
254 /// Designer-style 2D chrome in logical coordinates. No full-window background —
255 /// the 3D viewport (copied in as the backdrop) shows through everywhere the UI
256 /// doesn't paint, exactly like the app.
257 fn build_scene(lw: f32, lh: f32, scale: f32, t: f32) -> Vec<Vertex> {
258     let mut verts: Vec<Vertex> = Vec::new();
259 
260     // Header bar + side panel chrome.
261     verts.extend(quad_vertices(0.0, 0.0, lw, 36.0, lw, lh, [0.13, 0.13, 0.17, 1.0]));
262     verts.extend(quad_vertices(0.0, 36.0, 56.0, lh - 36.0, lw, lh, [0.11, 0.11, 0.145, 1.0]));
263 
264     // Floating alpha-blended quads over the 3D scene.
265     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]];
266     for (i, color) in palette.iter().enumerate() {
267         verts.extend(quad_vertices(70.0 + i as f32 * 50.0, 46.0, 40.0, 40.0, lw, lh, *color));
268     }
269 
270     // Animated quad: hue-cycled color proves per-frame uploads.
271     let pulse = |phase: f32| 0.5 + 0.5 * (t * 2.0 + phase).sin();
272     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]));
273 
274     // Circle-clipped quad (clip center/radius in physical pixels).
275     let (ccx, ccy, ccr) = (320.0f32, 90.0f32, 26.0f32);
276     let clip = [ccx * scale, ccy * scale, ccr * scale];
277     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]) {
278         verts.push(Vertex { clip_circle: clip, ..v });
279     }
280 
281     // Blur-behind plate (negative alpha): blurs the real 3D backdrop beneath it.
282     verts.extend(quad_vertices(110.0, 140.0, 200.0, 70.0, lw, lh, [0.45, 0.55, 0.95, -0.55]));
283 
284     verts
285 }
286 
287 fn main() {
288     env_logger::Builder::from_default_env()
289         .filter_level(log::LevelFilter::Info)
290         .init();
291 
292     let conn = Connection::connect_to_env().expect("No Wayland display");
293     let (globals, event_queue) = registry_queue_init::<SmokeApp>(&conn).unwrap();
294     let qh = event_queue.handle();
295 
296     let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
297     let xdg_shell_state = XdgShell::bind(&globals, &qh).unwrap();
298     let output_state = OutputState::new(&globals, &qh);
299 
300     let mut app = SmokeApp {
301         registry_state: RegistryState::new(&globals),
302         output_state,
303         window: None,
304         renderer: None,
305         exit: false,
306         configured: false,
307         logical_size: (720, 460),
308         scale: 1.0,
309     };
310 
311     let mut event_loop = calloop::EventLoop::<SmokeApp>::try_new().unwrap();
312     WaylandSource::new(conn.clone(), event_queue)
313         .insert(event_loop.handle())
314         .unwrap();
315 
316     // Roundtrip so outputs (and their scales) are known.
317     event_loop.dispatch(std::time::Duration::ZERO, &mut app).unwrap();
318     app.scale = cce_ui::wayland::detect_scale_factor(&app.output_state);
319 
320     let wl_surface = compositor_state.create_surface(&qh);
321     wl_surface.set_buffer_scale(app.scale as i32);
322     let window = xdg_shell_state.create_window(wl_surface.clone(), WindowDecorations::None, &qh);
323     window.set_title("vk-smoke");
324     window.set_app_id("cce-designer-vk-smoke");
325     window.set_min_size(Some((360, 240)));
326     window.commit();
327     app.window = Some(window);
328 
329     // Wait for the first configure before creating the swapchain (a Vulkan
330     // present attaches a buffer, which is illegal pre-configure).
331     while !app.configured && !app.exit {
332         event_loop
333             .dispatch(std::time::Duration::from_millis(16), &mut app)
334             .unwrap();
335     }
336 
337     let display_ptr = conn.backend().display_id().as_ptr() as *mut std::ffi::c_void;
338     let surface_ptr = wl_surface.id().as_ptr() as *mut std::ffi::c_void;
339     let pw = (app.logical_size.0 as f64 * app.scale) as u32;
340     let ph = (app.logical_size.1 as f64 * app.scale) as u32;
341     let radius = cce_ui::color::root_plate_corner_radius() * app.scale as f32;
342     app.renderer =
343         Some(unsafe { VkRenderer::new(display_ptr, surface_ptr, pw, ph, radius) });
344     log::info!("vk-smoke: renderer up at {pw}x{ph} (scale {})", app.scale);
345 
346     // 3D meshes for the viewport scene.
347     let (bg_mesh, grid_mesh, cube_mesh) = {
348         let r = app.renderer.as_mut().unwrap();
349         (
350             r.create_mesh(&viewport_bg_verts()),
351             r.create_mesh(&grid_verts()),
352             r.create_mesh(&cube_verts()),
353         )
354     };
355 
356     // Text stack: same bundled fonts as the app, shaped once up front.
357     let mut font_system = cce_ui::create_font_system();
358     let mut swash_cache = SwashCache::new();
359     let title_buf = make_buffer(&mut font_system, "vk-smoke — ash text stage", 14.0);
360     let body_buf = make_buffer(
361         &mut font_system,
362         "cosmic-text shaping → swash raster → vulkan glyph atlas",
363         13.0,
364     );
365     let clipped_buf = make_buffer(
366         &mut font_system,
367         "this line is clipped mid-glyph by span bounds ###########",
368         13.0,
369     );
370 
371     // User image: a checkerboard drawn between the animated quad (under) and
372     // the blur plate (over) via z_before.
373     let mut px = vec![0u8; 64 * 64 * 4];
374     for y in 0..64usize {
375         for x in 0..64usize {
376             let on = ((x / 8) + (y / 8)) % 2 == 0;
377             let i = (y * 64 + x) * 4;
378             px[i..i + 4].copy_from_slice(if on { &[240, 90, 60, 255] } else { &[40, 200, 220, 255] });
379         }
380     }
381     let checker = vk::upload_rgba(px, 64, 64);
382 
383     // RT mode: the pane runs the phase-2 compute path tracer instead of the
384     // raster 3D pass. Static camera, so progressive accumulation visibly
385     // converges from noise to a clean render.
386     let rt_mode = std::env::var_os("CCE_VK_SMOKE_RT").is_some();
387     if rt_mode {
388         let (tris, mats) = rt_demo_scene();
389         log::info!("vk-smoke: RT mode — {} triangles, {} materials", tris.len(), mats.len());
390         app.renderer.as_mut().unwrap().set_rt_scene(&tris, &mats);
391     }
392 
393     let start = std::time::Instant::now();
394     while !app.exit {
395         event_loop
396             .dispatch(std::time::Duration::from_millis(16), &mut app)
397             .unwrap();
398         if app.exit {
399             break;
400         }
401         let (lw, lh) = (app.logical_size.0 as f32, app.logical_size.1 as f32);
402         let t = start.elapsed().as_secs_f32();
403         let s = app.scale as f32;
404         let verts = build_scene(lw, lh, s, t);
405         let pulse = 0.75 + 0.25 * (t * 3.0).sin();
406         let spans = [
407             TextSpan {
408                 buffer: &title_buf,
409                 left: 12.0 * s,
410                 top: 9.0 * s,
411                 scale: s,
412                 bounds: None,
413                 default_color: [0.92, 0.92, 0.95, 1.0],
414                 rotation: None,
415                 clip_circle: [0.0; 3],
416                 clip_extents: [0.0; 2],
417             },
418             TextSpan {
419                 buffer: &body_buf,
420                 left: 120.0 * s,
421                 top: 158.0 * s,
422                 scale: s,
423                 bounds: None,
424                 // Animated color: proves per-frame vertex rebuilds.
425                 default_color: [pulse, 0.80, 0.55, 1.0],
426                 rotation: None,
427                 clip_circle: [0.0; 3],
428                 clip_extents: [0.0; 2],
429             },
430             TextSpan {
431                 buffer: &clipped_buf,
432                 left: 66.0 * s,
433                 top: 215.0 * s,
434                 scale: s,
435                 bounds: Some([
436                     (66.0 * s) as i32,
437                     (215.0 * s) as i32,
438                     (260.0 * s) as i32,
439                     (233.0 * s) as i32,
440                 ]),
441                 default_color: [0.70, 0.85, 1.00, 1.0],
442                 rotation: None,
443                 clip_circle: [0.0; 3],
444                 clip_extents: [0.0; 2],
445             },
446         ];
447         // build_scene vertex layout: blur plate is the last 6 verts; the image
448         // sorts just before it (above everything else, beneath the plate).
449         let image_quads = [ImageQuad {
450             image: checker,
451             rect: (250.0 * s, 115.0 * s, 90.0 * s, 90.0 * s),
452             alpha: 1.0,
453             z_before: (verts.len() as u32).saturating_sub(6),
454             clip: None,
455         }];
456         if let Some(renderer) = &mut app.renderer {
457             // 3D pane: right of the side panel, below the header (physical px).
458             // Full-frame NDC scissored to the pane, exactly like the app.
459             let pane = (
460                 (56.0 * s) as u32,
461                 (36.0 * s) as u32,
462                 ((lw - 56.0) * s) as u32,
463                 ((lh - 36.0) * s) as u32,
464             );
465             let aspect = (lw - 56.0) / (lh - 36.0);
466             let proj = Mat4::perspective_rh(0.9, aspect, 0.1, 100.0);
467             let view = Mat4::look_at_rh(Vec3::new(2.5, 1.8, 2.5), Vec3::ZERO, Vec3::Y);
468             if rt_mode {
469                 let inv_mvp = (proj * view).inverse().to_cols_array_2d();
470                 renderer.stage_rt(pane, RtCamera { inv_mvp });
471             } else {
472                 let model = Mat4::from_rotation_y(t * 0.8);
473                 let mvp = (proj * view * model).to_cols_array_2d();
474                 renderer.stage_scene(
475                     pane,
476                     vec![
477                         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 },
478                         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 },
479                         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 },
480                     ],
481                 );
482             }
483             renderer.prepare_text(&mut font_system, &mut swash_cache, &spans);
484             // FIFO present paces this loop to the display's refresh rate.
485             renderer.draw_frame_2d(Frame2D {
486                 verts: &verts,
487                 batches: &[],
488                 overlay_verts: &[],
489                 images: &image_quads,
490                 plate_features: &[],
491                 clear_color: [0.0; 4],
492             });
493         }
494     }
495 
496     // Tear down the renderer (and its swapchain) before the surface dies.
497     app.renderer.take();
498 }