1use std::cell::RefCell;
2use std::rc::Rc;
3
4use glam::Vec2;
5use loam_runtime::{DomainId, Eye, PublishedView, Rigid, SegmentRecord, Stamp, ViewTarget};
6use wgpu::{Color, CommandEncoder, Device, Queue, RenderPass, TextureFormat, TextureView};
7
8use crate::depth::DepthBuffer;
9use crate::device::GpuContext;
10use crate::pass::{
11 FrameFormat, FramePass, FrameTarget, PassError, PassExecutionError, PassSchedule, PassStage,
12 Section,
13};
14use crate::triangle_pass::TriangleFeed;
15use crate::view::DEPTH_FORMAT;
16use crate::{DepthConvention, DepthMode, FragmentShading, LineRasterNode};
17
18struct ViewLines {
19 opaque: LineRasterNode,
20 translucent: LineRasterNode,
21 scratch: Vec<SegmentRecord>,
22 uploaded: Option<(DomainId, ViewTarget, Stamp)>,
23 camera: Option<(glam::Mat4, Vec2)>,
24}
25
26impl ViewLines {
27 fn new(device: &Device, format: TextureFormat) -> Self {
28 Self {
29 opaque: LineRasterNode::new(
30 device,
31 format,
32 DepthMode::ReadWrite {
33 format: DEPTH_FORMAT,
34 },
35 DepthConvention::ReversedZ,
36 ),
37 translucent: LineRasterNode::new(
38 device,
39 format,
40 DepthMode::ReadOnly {
41 format: DEPTH_FORMAT,
42 },
43 DepthConvention::ReversedZ,
44 ),
45 scratch: Vec::new(),
46 uploaded: None,
47 camera: None,
48 }
49 }
50
51 fn set_camera(&mut self, queue: &Queue, camera: glam::Mat4, viewport: Vec2) {
52 if self.camera == Some((camera, viewport)) {
53 return;
54 }
55 self.camera = Some((camera, viewport));
56 self.opaque.set_camera(queue, camera, viewport);
57 self.translucent.set_camera(queue, camera, viewport);
58 }
59
60 fn upload_segments(&mut self, device: &Device, queue: &Queue, segments: &[SegmentRecord]) {
61 self.scratch.clear();
62 self.scratch
63 .extend(segments.iter().copied().filter(segment_is_opaque));
64 self.opaque.upload_segments(device, queue, &self.scratch);
65 self.scratch.clear();
66 self.scratch.extend(
67 segments
68 .iter()
69 .copied()
70 .filter(|segment| !segment_is_opaque(segment)),
71 );
72 self.translucent
73 .upload_segments(device, queue, &self.scratch);
74 }
75}
76
77struct PublishedLines {
78 views: Rc<RefCell<Vec<ViewLines>>>,
79}
80
81impl FramePass for PublishedLines {
82 fn name(&self) -> &'static str {
83 "present-draw"
84 }
85
86 fn stage(&self) -> PassStage {
87 PassStage::Scene
88 }
89
90 fn depth_convention(&self) -> Option<DepthConvention> {
91 Some(DepthConvention::ReversedZ)
92 }
93
94 fn depth_read(&self) -> Option<DepthConvention> {
95 Some(DepthConvention::ReversedZ)
96 }
97
98 fn shares_pass(&self) -> bool {
99 true
100 }
101
102 fn draw(&mut self, pass: &mut RenderPass<'_>, _target: &FrameTarget<'_>) -> anyhow::Result<()> {
103 let views = self.views.borrow();
104 for slot in views.iter() {
105 slot.opaque.draw(pass, None);
106 }
107 for slot in views.iter() {
108 slot.translucent.draw(pass, None);
109 }
110 Ok(())
111 }
112
113 fn record(
114 &mut self,
115 encoder: &mut CommandEncoder,
116 target: &FrameTarget<'_>,
117 ) -> anyhow::Result<()> {
118 let Some(depth) = target.depth else {
119 return Ok(());
120 };
121 let views = self.views.borrow();
122 for slot in views.iter() {
123 slot.opaque.record(encoder, target.color, Some(depth), None);
124 }
125 for slot in views.iter() {
126 slot.translucent
127 .record(encoder, target.color, Some(depth), None);
128 }
129 Ok(())
130 }
131
132 fn attach(&mut self, _gpu: &GpuContext, _frame: FrameFormat) -> anyhow::Result<()> {
133 Ok(())
134 }
135}
136
137pub struct Presenter {
138 uploads: u64,
139 format: TextureFormat,
140 depth: Option<DepthBuffer>,
141 views: Rc<RefCell<Vec<ViewLines>>>,
142 fills: TriangleFeed,
143 filled: Vec<(DomainId, ViewTarget)>,
144 schedule: PassSchedule,
145}
146
147impl Presenter {
148 pub fn new(format: TextureFormat) -> Result<Self, PassError> {
149 let fills = TriangleFeed::default();
150 let views = Rc::new(RefCell::new(Vec::new()));
151 let mut schedule = PassSchedule::new(DepthConvention::ReversedZ);
152 schedule.register(fills.pass(FragmentShading::FaceNormalLambert))?;
153 schedule.register(Box::new(PublishedLines {
154 views: views.clone(),
155 }))?;
156 Ok(Self {
157 format,
158 uploads: 0,
159 depth: None,
160 views,
161 fills,
162 filled: Vec::new(),
163 schedule,
164 })
165 }
166
167 pub fn attach(&mut self, gpu: &GpuContext) -> Result<(), PassExecutionError> {
168 self.depth = None;
169 self.views.borrow_mut().clear();
170 self.schedule.attach(
171 gpu,
172 FrameFormat {
173 color: self.format,
174 depth: DEPTH_FORMAT,
175 },
176 )
177 }
178
179 pub fn register_pass(&mut self, pass: Box<dyn FramePass>) -> Result<(), PassError> {
180 self.schedule.register(pass)
181 }
182
183 pub fn uploads(&self) -> u64 {
184 self.uploads
185 }
186
187 pub fn sections(&self) -> &[Section] {
188 self.schedule.sections()
189 }
190
191 pub fn after_submit(&mut self) {
192 self.schedule.after_submit();
193 }
194
195 pub fn upload(
196 &mut self,
197 device: &Device,
198 queue: &Queue,
199 eye: &Eye,
200 viewport: Vec2,
201 views: &[PublishedView],
202 ) {
203 let _scope = loam_time::frame_trace::scope("present-upload");
204 let mut line_views = self.views.borrow_mut();
205 while line_views.len() < views.len() {
206 line_views.push(ViewLines::new(device, self.format));
207 }
208 line_views.truncate(views.len());
209 let mut rebuilt = false;
210 for (slot, view) in line_views.iter_mut().zip(views) {
211 let camera = crate::view::placed_view_projection(eye, view.placement);
212 slot.set_camera(queue, camera, viewport);
213 let published = (view.domain, view.target, view.records.built());
214 if slot.uploaded == Some(published) {
215 continue;
216 }
217 slot.uploaded = Some(published);
218 rebuilt = true;
219 self.uploads += 1;
220 slot.upload_segments(device, queue, view.records.segments());
221 }
222 drop(line_views);
223 self.fills.set_view(eye, Rigid::IDENTITY);
224 let listed = self.filled.len() == views.len()
225 && (self.filled.iter().zip(views))
226 .all(|(held, view)| *held == (view.domain, view.target));
227 if rebuilt || !listed {
228 self.filled.clear();
229 self.filled
230 .extend(views.iter().map(|view| (view.domain, view.target)));
231 self.fills.edit(|mesh| {
232 mesh.vertices.clear();
233 mesh.colors.clear();
234 mesh.indices.clear();
235 for view in views {
236 for triangle in view.records.triangles() {
237 let base = mesh.vertices.len() as u32;
238 for corner in triangle.vertices {
239 mesh.vertices.push(view.placement.apply(corner));
240 mesh.colors.push(triangle.color);
241 }
242 mesh.indices.push([base, base + 1, base + 2]);
243 }
244 }
245 });
246 }
247 }
248
249 pub fn record_scene(
250 &mut self,
251 device: &Device,
252 encoder: &mut CommandEncoder,
253 target: &TextureView,
254 size: (u32, u32),
255 background: Color,
256 ) -> Result<(), PassExecutionError> {
257 self.schedule.begin_frame();
258 DepthBuffer::ensure(&mut self.depth, device, DEPTH_FORMAT, size);
259 let Some(depth) = self.depth.as_ref() else {
260 return Ok(());
261 };
262 let frame = FrameTarget {
263 color: target,
264 depth: Some(&depth.view),
265 size,
266 };
267 self.schedule.record_scene(encoder, &frame, background)
268 }
269
270 pub fn record_overlays(
272 &mut self,
273 encoder: &mut CommandEncoder,
274 target: &TextureView,
275 size: (u32, u32),
276 ) -> Result<(), PassExecutionError> {
277 let Some(depth) = self.depth.as_ref() else {
278 return Ok(());
279 };
280 let frame = FrameTarget {
281 color: target,
282 depth: Some(&depth.view),
283 size,
284 };
285 self.schedule.record(PassStage::Overlay, encoder, &frame)?;
286 self.schedule.end_frame(encoder);
287 Ok(())
288 }
289}
290
291fn segment_is_opaque(segment: &SegmentRecord) -> bool {
292 segment.start_color[3] >= 1.0 && segment.end_color[3] >= 1.0
293}
294
295#[cfg(test)]
296mod tests {
297 use crate::pass::ColorLoad;
298 use loam_math::EuclideanR4;
299 use loam_runtime::{
300 DomainBuilder, Instance, LogCapacity, Material, Pose, PreparedGeometry, Projection4,
301 Records, Session, SimConfig, SpawnBundle, ViewSpec,
302 };
303 use wgpu::{
304 BackendOptions, Backends, InstanceDescriptor, LoadOp, NoopBackendOptions, Operations,
305 RenderPassColorAttachment, RenderPassDescriptor, StoreOp, TextureDescriptor,
306 TextureDimension, TextureFormat, TextureUsages,
307 };
308
309 use super::*;
310
311 #[global_allocator]
312 static COUNTING_ALLOCATOR: loam_time::alloc::CountingAllocator<std::alloc::System> =
313 loam_time::alloc::CountingAllocator::new(std::alloc::System);
314
315 loam_runtime::stores! {
316 #[derive(Default)]
317 pub struct Spun {
318 spin: Store<f32>,
319 }
320 }
321
322 fn noop_device() -> (Device, Queue) {
323 let instance = wgpu::Instance::new(&InstanceDescriptor {
324 backends: Backends::NOOP,
325 backend_options: BackendOptions {
326 noop: NoopBackendOptions { enable: true },
327 ..Default::default()
328 },
329 ..Default::default()
330 });
331 let adapter = pollster::block_on(instance.request_adapter(&Default::default()))
332 .expect("noop backend always yields an adapter");
333 pollster::block_on(adapter.request_device(&Default::default()))
334 .expect("noop adapter always yields a device")
335 }
336
337 #[test]
338 fn warmed_publish_allocates_or_the_presenter_reuploads_unchanged_records() {
339 let mut session = Session::new(Spun::default(), SimConfig::default());
340 let r4 = session
341 .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
342 let edges = session.prepare(PreparedGeometry::Lines4 {
343 segments: (0..64)
344 .map(|i| {
345 let t = i as f32 * 0.01;
346 [[t, 0.0, 0.0, 0.0], [t, 1.0, 0.0, 0.5]]
347 })
348 .collect(),
349 });
350 let white = session.add_material(Material::lines([1.0; 4], 2.0));
351 let root = session.views().root();
352 let spun = session.dispatch(|d| {
353 let eye = d
354 .spawn(SpawnBundle::new().at(r4, Pose::at(glam::Vec4::ZERO)))
355 .unwrap();
356 d.domains
357 .typed(r4)
358 .unwrap()
359 .add_view(ViewSpec::new(root, eye, Projection4 { focal: 2.0 }))
360 .unwrap();
361 d.spawn(
362 SpawnBundle::new()
363 .at(r4, Pose::at(glam::Vec4::NEG_Z * 4.0))
364 .instance(Instance::new(edges, white))
365 .row(0.0_f32),
366 )
367 .unwrap()
368 });
369
370 let (device, queue) = noop_device();
371 let target = device
372 .create_texture(&TextureDescriptor {
373 label: None,
374 size: wgpu::Extent3d {
375 width: 64,
376 height: 64,
377 depth_or_array_layers: 1,
378 },
379 mip_level_count: 1,
380 sample_count: 1,
381 dimension: TextureDimension::D2,
382 format: TextureFormat::Rgba8Unorm,
383 usage: TextureUsages::RENDER_ATTACHMENT,
384 view_formats: &[],
385 })
386 .create_view(&Default::default());
387 let mut presenter = Presenter::new(TextureFormat::Rgba8Unorm).expect("presenter");
388 let mut records = Records::default();
389 let eye = Eye::default();
390 let spin = |session: &mut Session<Spun>| {
391 if let Ok(domain) = session.domains_mut().typed(r4) {
392 if let Some(pose) = domain.poses().get(spun) {
393 let mut point = pose.point;
394 point.x += 0.01;
395 domain.set_point(spun, point).unwrap();
396 }
397 }
398 };
399 let present = |session: &mut Session<Spun>,
400 records: &mut Records,
401 presenter: &mut Presenter,
402 encoder: &mut CommandEncoder| {
403 records.publish(session).unwrap();
404 let published = records.lend().unwrap();
405 presenter.upload(&device, &queue, &eye, Vec2::splat(64.0), &published.views);
406 records.release(published);
407 presenter
408 .record_scene(&device, encoder, &target, (64, 64), Color::BLACK)
409 .expect("recorded the scene");
410 presenter
411 .record_overlays(encoder, &target, (64, 64))
412 .expect("recorded the overlays");
413 };
414
415 let mut encoder = device.create_command_encoder(&Default::default());
416 for _ in 0..8 {
417 spin(&mut session);
418 present(&mut session, &mut records, &mut presenter, &mut encoder);
419 }
420
421 let published = loam_time::alloc::bytes_allocated_by(|| {
422 for _ in 0..16 {
423 spin(&mut session);
424 records.publish(&mut session).unwrap();
425 }
426 })
427 .expect("the counting allocator is installed");
428 assert_eq!(
429 published, 0,
430 "16 warmed publications asked the allocator for {published} bytes"
431 );
432
433 present(&mut session, &mut records, &mut presenter, &mut encoder);
434 let before = presenter.uploads();
435 let idle = loam_time::alloc::bytes_allocated_by(|| {
436 for _ in 0..16 {
437 present(&mut session, &mut records, &mut presenter, &mut encoder);
438 }
439 assert_eq!(
440 presenter.uploads(),
441 before,
442 "an idle presenter uploaded a view"
443 );
444 })
445 .expect("the counting allocator is installed");
446 let busy = loam_time::alloc::bytes_allocated_by(|| {
447 for _ in 0..16 {
448 spin(&mut session);
449 present(&mut session, &mut records, &mut presenter, &mut encoder);
450 }
451 assert_eq!(
452 presenter.uploads(),
453 before + 16,
454 "sixteen changed publications uploaded the view {} times",
455 presenter.uploads() - before
456 );
457 })
458 .expect("the counting allocator is installed");
459 assert!(
460 idle < busy,
461 "the presenter uploads records it already holds: {idle} bytes idle against {busy} busy"
462 );
463 }
464
465 #[test]
466 fn a_view_dropped_from_the_publication_takes_its_section_fills_with_it() {
467 use loam_runtime::{Section4, ViewId};
468
469 let mut session = Session::new(Spun::default(), SimConfig::default());
470 let r4 = session
471 .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
472 let geometry = session.prepare(PreparedGeometry::Polytope4 {
473 polytope: loam_shape::polytope::Polytope4::Tesseract,
474 scale: 0.7,
475 });
476 let body = session.add_material(Material::lines([1.0; 4], 1.0));
477 let cut = session.add_material(Material::lines([1.0, 0.85, 0.35, 1.0], 2.0));
478 let root = session.views().root();
479 session.dispatch(|d| {
480 let eye = d
481 .spawn(SpawnBundle::new().at(r4, Pose::at(glam::Vec4::ZERO)))
482 .expect("the eye spawned");
483 d.spawn(
484 SpawnBundle::new()
485 .at(r4, Pose::at(glam::Vec4::NEG_Z * 4.0))
486 .instance(Instance::new(geometry, body).sectioned(cut))
487 .row(0.0_f32),
488 )
489 .expect("the body spawned");
490 let domain = d.domains.typed(r4).expect("the r4 domain");
491 let _: ViewId = domain
492 .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))
493 .expect("the first view");
494 domain
495 .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))
496 .expect("the second view")
497 });
498
499 let (device, queue) = noop_device();
500 let mut presenter = Presenter::new(TextureFormat::Rgba8Unorm).expect("presenter");
501 let mut records = Records::default();
502 let eye = Eye::default();
503 records.publish(&mut session).expect("published");
504 let published = records.lend().expect("the buffer is free");
505
506 presenter.upload(&device, &queue, &eye, Vec2::splat(64.0), &published.views);
507 let both = presenter.fills.triangles();
508 assert_eq!(
509 both,
510 24 * 2,
511 "each of the two cut layers fans the tesseract's six straddling cells into four triangles, not {both}"
512 );
513
514 presenter.upload(
515 &device,
516 &queue,
517 &eye,
518 Vec2::splat(64.0),
519 &published.views[..1],
520 );
521 assert_eq!(
522 presenter.fills.triangles(),
523 24,
524 "the dropped view left its section fills on screen"
525 );
526 records.release(published);
527 }
528
529 #[test]
530 fn dropping_the_first_of_two_equally_stamped_views_skips_the_survivors_lines() {
531 let mut session = Session::new(Spun::default(), SimConfig::default());
532 let white = session.add_material(Material::lines([1.0; 4], 1.0));
533 let root = session.views().root();
534 for (name, polytope) in [
535 ("wide", loam_shape::polytope::Polytope4::Tesseract),
536 ("narrow", loam_shape::polytope::Polytope4::Pentatope),
537 ] {
538 let domain = session.register_domain(
539 DomainBuilder::new(name, EuclideanR4).tracked(LogCapacity::default()),
540 );
541 let geometry = session.prepare(PreparedGeometry::Polytope4 {
542 polytope,
543 scale: 0.7,
544 });
545 session.dispatch(|d| {
546 let eye = d
547 .spawn(SpawnBundle::new().at(domain, Pose::at(glam::Vec4::ZERO)))
548 .expect("the eye spawned");
549 d.spawn(
550 SpawnBundle::new()
551 .at(domain, Pose::at(glam::Vec4::NEG_Z * 4.0))
552 .instance(Instance::new(geometry, white))
553 .row(0.0_f32),
554 )
555 .expect("the body spawned");
556 d.domains
557 .typed(domain)
558 .expect("the domain")
559 .add_view(ViewSpec::new(root, eye, Projection4 { focal: 2.0 }))
560 .expect("the view")
561 });
562 }
563
564 let (device, queue) = noop_device();
565 let mut presenter = Presenter::new(TextureFormat::Rgba8Unorm).expect("presenter");
566 let mut records = Records::default();
567 let eye = Eye::default();
568 records.publish(&mut session).expect("published");
569 let published = records.lend().expect("the buffer is free");
570 let dropped = &published.views[0].records;
571 let kept = &published.views[1].records;
572 assert!(
573 dropped.built() == kept.built() && dropped.segments().len() != kept.segments().len(),
574 "the probe needs two equally stamped views whose lines differ"
575 );
576 let survivor = kept.segments().len() as u32;
577
578 presenter.upload(&device, &queue, &eye, Vec2::splat(64.0), &published.views);
579 presenter.upload(
580 &device,
581 &queue,
582 &eye,
583 Vec2::splat(64.0),
584 &published.views[1..],
585 );
586 let line_views = presenter.views.borrow();
587 assert_eq!(
588 line_views[0].opaque.segment_count() + line_views[0].translucent.segment_count(),
589 survivor,
590 "the first slot still holds the dropped view's lines"
591 );
592 records.release(published);
593 }
594
595 const PROBE_SIZE: u32 = 64;
596 const PAINT: Color = Color {
597 r: 1.0,
598 g: 0.0,
599 b: 0.0,
600 a: 1.0,
601 };
602
603 struct Paint;
604
605 impl FramePass for Paint {
606 fn name(&self) -> &'static str {
607 "paint"
608 }
609
610 fn stage(&self) -> PassStage {
611 PassStage::Background
612 }
613
614 fn color_load(&self) -> ColorLoad {
615 ColorLoad::Clear
616 }
617
618 fn record(
619 &mut self,
620 encoder: &mut CommandEncoder,
621 target: &FrameTarget<'_>,
622 ) -> anyhow::Result<()> {
623 encoder.begin_render_pass(&RenderPassDescriptor {
624 label: Some("paint"),
625 color_attachments: &[Some(RenderPassColorAttachment {
626 view: target.color,
627 depth_slice: None,
628 resolve_target: None,
629 ops: Operations {
630 load: LoadOp::Clear(PAINT),
631 store: StoreOp::Store,
632 },
633 })],
634 depth_stencil_attachment: None,
635 timestamp_writes: None,
636 occlusion_query_set: None,
637 });
638 Ok(())
639 }
640
641 fn attach(&mut self, _gpu: &GpuContext, _frame: FrameFormat) -> anyhow::Result<()> {
642 Ok(())
643 }
644 }
645
646 fn pixel_at(gpu: &GpuContext, texture: &wgpu::Texture, at: [u32; 2]) -> [u8; 4] {
647 let row = PROBE_SIZE * 4;
648 let staging = gpu.device.create_buffer(&wgpu::BufferDescriptor {
649 label: None,
650 size: u64::from(row) * u64::from(PROBE_SIZE),
651 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
652 mapped_at_creation: false,
653 });
654 let mut encoder = gpu.device.create_command_encoder(&Default::default());
655 encoder.copy_texture_to_buffer(
656 wgpu::TexelCopyTextureInfo {
657 texture,
658 mip_level: 0,
659 origin: wgpu::Origin3d::ZERO,
660 aspect: wgpu::TextureAspect::All,
661 },
662 wgpu::TexelCopyBufferInfo {
663 buffer: &staging,
664 layout: wgpu::TexelCopyBufferLayout {
665 offset: 0,
666 bytes_per_row: Some(row),
667 rows_per_image: None,
668 },
669 },
670 wgpu::Extent3d {
671 width: PROBE_SIZE,
672 height: PROBE_SIZE,
673 depth_or_array_layers: 1,
674 },
675 );
676 gpu.queue.submit(Some(encoder.finish()));
677 let slice = staging.slice(..);
678 let (sender, receiver) = std::sync::mpsc::channel();
679 slice.map_async(wgpu::MapMode::Read, move |result| {
680 let _ = sender.send(result);
681 });
682 gpu.device
683 .poll(wgpu::PollType::Wait {
684 submission_index: None,
685 timeout: None,
686 })
687 .expect("the readback polled");
688 receiver
689 .recv()
690 .expect("the map callback ran")
691 .expect("the staging buffer mapped");
692 let data = slice.get_mapped_range();
693 let offset = ((at[1] * PROBE_SIZE + at[0]) * 4) as usize;
694 let pixel = [
695 data[offset],
696 data[offset + 1],
697 data[offset + 2],
698 data[offset + 3],
699 ];
700 drop(data);
701 staging.unmap();
702 pixel
703 }
704
705 #[test]
706 #[ignore = "requires a working wgpu adapter; run with --include-ignored"]
707 fn a_pass_before_the_scene_reaches_the_presented_frame_gpu_probe() {
708 let gpu = pollster::block_on(GpuContext::new(
709 wgpu::Instance::default(),
710 crate::device::FeatureRequest::default(),
711 None,
712 ))
713 .expect("a wgpu adapter");
714 let texture = gpu.device.create_texture(&TextureDescriptor {
715 label: None,
716 size: wgpu::Extent3d {
717 width: PROBE_SIZE,
718 height: PROBE_SIZE,
719 depth_or_array_layers: 1,
720 },
721 mip_level_count: 1,
722 sample_count: 1,
723 dimension: TextureDimension::D2,
724 format: TextureFormat::Rgba8Unorm,
725 usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
726 view_formats: &[],
727 });
728 let view = texture.create_view(&Default::default());
729 let mut presenter = Presenter::new(TextureFormat::Rgba8Unorm).expect("presenter");
730 presenter
731 .register_pass(Box::new(Paint))
732 .expect("registered");
733 presenter.attach(&gpu).expect("attached");
734 let mut encoder = gpu.device.create_command_encoder(&Default::default());
735 presenter
736 .record_scene(
737 &gpu.device,
738 &mut encoder,
739 &view,
740 (PROBE_SIZE, PROBE_SIZE),
741 Color::BLACK,
742 )
743 .expect("recorded the scene");
744 presenter
745 .record_overlays(&mut encoder, &view, (PROBE_SIZE, PROBE_SIZE))
746 .expect("recorded the overlays");
747 gpu.queue.submit(Some(encoder.finish()));
748
749 assert_eq!(
750 pixel_at(&gpu, &texture, [0, 0]),
751 [255, 0, 0, 255],
752 "the presenter cleared the frame after the pass that runs before the scene"
753 );
754 }
755
756 #[test]
757 #[ignore = "requires a working wgpu adapter; run with --include-ignored"]
758 fn published_translucency_follows_all_opaque_geometry_gpu_probe() {
759 let gpu = pollster::block_on(GpuContext::new(
760 wgpu::Instance::default(),
761 crate::device::FeatureRequest::default(),
762 None,
763 ))
764 .expect("a wgpu adapter");
765 let texture = gpu.device.create_texture(&TextureDescriptor {
766 label: None,
767 size: wgpu::Extent3d {
768 width: PROBE_SIZE,
769 height: PROBE_SIZE,
770 depth_or_array_layers: 1,
771 },
772 mip_level_count: 1,
773 sample_count: 1,
774 dimension: TextureDimension::D2,
775 format: TextureFormat::Rgba8Unorm,
776 usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
777 view_formats: &[],
778 });
779 let target = texture.create_view(&Default::default());
780 let mut presenter = Presenter::new(TextureFormat::Rgba8Unorm).expect("presenter");
781 presenter.attach(&gpu).expect("attached");
782 let eye = Eye::default();
783 let camera = crate::view::placed_view_projection(&eye, Rigid::IDENTITY);
784 presenter.fills.set_view(&eye, Rigid::IDENTITY);
785 presenter.fills.edit(|mesh| {
786 mesh.vertices
787 .extend([[-2.0, -1.5, -4.0], [2.0, -1.5, -4.0], [0.0, 2.0, -4.0]]);
788 mesh.colors.extend([[0.0, 1.0, 0.0, 1.0]; 3]);
789 mesh.indices.push([0, 1, 2]);
790 });
791
792 let mut translucent = ViewLines::new(&gpu.device, TextureFormat::Rgba8Unorm);
793 translucent.set_camera(&gpu.queue, camera, Vec2::splat(PROBE_SIZE as f32));
794 translucent.upload_segments(
795 &gpu.device,
796 &gpu.queue,
797 &[SegmentRecord {
798 start: [-1.0, 0.0, -3.0],
799 end: [1.0, 0.0, -3.0],
800 start_color: [1.0, 0.0, 0.0, 0.01],
801 end_color: [1.0, 0.0, 0.0, 0.01],
802 width_px: 12.0,
803 ..Default::default()
804 }],
805 );
806 let mut opaque = ViewLines::new(&gpu.device, TextureFormat::Rgba8Unorm);
807 opaque.set_camera(&gpu.queue, camera, Vec2::splat(PROBE_SIZE as f32));
808 opaque.upload_segments(
809 &gpu.device,
810 &gpu.queue,
811 &[SegmentRecord {
812 start: [0.0, -0.75, -3.5],
813 end: [0.0, 0.75, -3.5],
814 start_color: [0.0, 0.0, 1.0, 1.0],
815 end_color: [0.0, 0.0, 1.0, 1.0],
816 width_px: 8.0,
817 ..Default::default()
818 }],
819 );
820 presenter.views.borrow_mut().extend([translucent, opaque]);
821
822 let mut encoder = gpu.device.create_command_encoder(&Default::default());
823 presenter
824 .record_scene(
825 &gpu.device,
826 &mut encoder,
827 &target,
828 (PROBE_SIZE, PROBE_SIZE),
829 Color::BLACK,
830 )
831 .expect("recorded the scene");
832 presenter
833 .record_overlays(&mut encoder, &target, (PROBE_SIZE, PROBE_SIZE))
834 .expect("recorded the overlays");
835 gpu.queue.submit(Some(encoder.finish()));
836
837 let surface = pixel_at(&gpu, &texture, [42, 32]);
838 assert!(
839 surface[0] > 0 && surface[1] > 80,
840 "the translucent line exposed the background through the fill: {surface:?}"
841 );
842 let crossing = pixel_at(&gpu, &texture, [32, 32]);
843 assert!(
844 crossing[0] > 0 && crossing[2] > 200,
845 "a later view drew its opaque line after the translucent batch: {crossing:?}"
846 );
847 }
848}