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 }