ui/gpu_view.rs
1// gpu_view -- GPU content in a window: a 3D view between widgets (G5e1; docs/GPU.md "3D-Ansicht")
2//
3// The framebuffer stays the truth: a view is drawn into it like any canvas, its command's Draw copies the view's
4// last frame there. What is new is where that frame comes from: the app's content (GpuContent; a render3d Renderer
5// and Scene, or anything else on lib/gpu: lib/ui knows nothing of render3d) draws it on the UI's own device into a
6// texture of the view's size, and the frame is read back from there.
7//
8// ui.gpu_view(id, &content, ver) -> &UiInput the view in the layout's next rectangle (next_size), its input
9// as ui.canvas's; a new ver (or size, or device) draws a frame
10// GpuContent::attach(dev) / detach() a device for the view: the first one, a new one after a loss /
11// it goes; build and drop what the frames need there
12// GpuContent::draw(e, target, size) a frame into target (GPU_VIEW_FORMAT, the view's size)
13// GpuContent::submitted(id) the frame went to the queue
14// GpuContent::paint(g, r) what the view shows without a device
15// ui.gpu_view_info(win, id) what a view does now (tests, measurements)
16// AF_GPU_3D the app shows views: the UI's device at level Voll on the
17// fastest adapter (without it, as ever: Basis, the frugal one)
18//
19// Where a frame shows (gpu_out.rs "views"):
20// - the window on its swapchain and the view on top in its cells: the output's render pass draws the texture
21// there (a quad, as an image's), the framebuffer lacks those pixels (Split::owed) until somebody reads it:
22// gp_settle runs the Draw, which reads the texture back. At rest (REST after the last frame) gp_tick does
23// that once, so the framebuffer holds the picture again without anybody asking. A layer over the view gets
24// the frame below it the same way; cells with something of the base over the view are drawn by the CPU over
25// the frame read back.
26// - anywhere else (the CPU path: before the switch, a new size, minimised, the inspector, right to left,
27// jfgui_GPU_OUTPUT=0, jfgui_GPU_VIEWS=0, a script without `gpu on`): the frame is read back at once and the
28// Draw copies it into the framebuffer, in the cells the CPU draws.
29// - no device (jfgui_GPU=0, no Vulkan or no usable adapter, a static Linux program, a lost device not back
30// yet): the last frame read back, else GpuContent::paint.
31// The device is the UI output's (one for all windows); without an output in this run (not wanted, never
32// possible) the views make one of their own on first use. A new device (generation) detaches the content from
33// the old one and attaches it to the new one; its next frame follows.
34//
35// The browser cannot wait: a read-back there is answered later, the Draw shows the last frame that came back. When
36// the answer comes (gp_views_tick looks), the next render takes it to the framebuffer as it would a new frame, and at
37// rest it is settled again (G5e2: before, a view at rest or under a layer kept an older frame in the framebuffer).
38//
39// A view's frame is bound to the display (G5e3): while its window is on its swapchain and the window's last frame has
40// not been shown yet (gpu_out.rs Win::pending: no free image, the display has not answered, less than 4 ms since the
41// last present), a new ver does not ask the content to draw; renders in between take the view's last frame. Once the
42// window's frame is shown gp_views_wake asks for a render, which draws the newest ver; a frame that is never shown
43// holds the view at most VIEW_HOLD_US, then it draws one anyway. GpuViewInfo::held counts the frames that waited.
44
45use alloc::rc::{Rc, Weak};
46use alloc::string::String;
47use alloc::vec::Vec;
48use core::cell::RefCell;
49
50use gfx::Surface;
51
52use crate::canvas::ViewRec;
53use crate::cmd::{Cmd, Draw, DrawRef, Gfx};
54use crate::{Ui, UiInput};
55
56#[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
57use gpu::{Access, Buffer, DataLayout, Device, GpuError, Layout, Texture, TextureRegion};
58
59/// the format of a view's target: what the output samples and what is read back byte for byte into the
60/// framebuffer (B, G, R, x). The content's colours go in as it writes them, as sRGB codes like the framebuffer's:
61/// no hardware encodes them (G5e2: render3d's fragment shaders do, RendererDesc::encode_srgb)
62pub const GPU_VIEW_FORMAT: gpu::Format = gpu::Format::Bgra8Unorm;
63
64/// what a view (ui.gpu_view) shows: content drawn on the UI's GPU device. The app keeps it in an
65/// `Rc<RefCell<..>>` and hands the same one every frame; lib/ui keeps a Weak and calls these at render time
66pub trait GpuContent {
67 /// a device for the view: the first, or a new one after the last was lost (what was built on an earlier one is
68 /// dead). Build what the frames need on it. Err: the view paints until the next device
69 fn attach(&mut self, dev: &gpu::Device) -> gpu::Result<()>;
70 /// the device goes (lost, the output ended, another content took the view's id): drop what was built on it
71 fn detach(&mut self);
72 /// one frame: record into e a render pass into target (GPU_VIEW_FORMAT, size w x h px). Keep its view, before
73 /// and after (the pass may set its own load and clear: `ColorAttachment { clear: .., ..target }`); passes that
74 /// split it (render3d's occlusion test) begin with its before and end with its after. lib/ui submits e
75 fn draw<'a>(&'a mut self, e: &mut gpu::CommandEncoder<'a>, target: gpu::ColorAttachment<'a>, size: (u32, u32)) -> gpu::Result<()>;
76 /// the frame went to the queue as id (render3d: Scene::submitted)
77 fn submitted(&mut self, id: gpu::SubmitId) {
78 let _ = id;
79 }
80 /// what the view shows without a device (no GPU, jfgui_GPU=0, before the first frame): painted on the CPU into
81 /// g, clipped to r (x, y, w, h); a dark grey unless the content says otherwise
82 fn paint(&self, g: &mut Gfx, r: [i32; 4]) {
83 paint_default(g, r);
84 }
85}
86
87/// what a view does now (Ui::gpu_view_info; tests and measurements)
88#[derive(Clone, Debug, Default)]
89#[non_exhaustive]
90pub struct GpuViewInfo {
91 /// its content is attached to a device: its frames come from the GPU
92 pub device: bool,
93 /// that device is the UI output's (else the views' own: no output in this run)
94 pub output: bool,
95 /// the device's level
96 pub level: Option<gpu::Level>,
97 /// the device's generation (a new one after a lost device; 0: none)
98 pub generation: u32,
99 /// the output's render pass drew it in the window's last frame (else the CPU copies the frame read back)
100 pub apart: bool,
101 /// the content attached and detached so far
102 pub attaches: u32,
103 pub detaches: u32,
104 /// frames drawn, the CPU time recording and submitting them took (us)
105 pub frames: u64,
106 pub draw_us: u64,
107 /// frames read back into the CPU's copy, the time that took (us: submit, wait, copy)
108 pub reads: u64,
109 pub read_us: u64,
110 /// (G5e2) the newest frame read back into the CPU's copy (1, 2, ..; 0: none): the framebuffer can hold the view's
111 /// newest frame once it equals `frames` (in the browser a read-back comes later)
112 pub read_frame: u64,
113 /// (G5e3) frames that had to wait: a new ver (size, device) came while the view's window had not shown its last
114 /// frame yet, so the content was not asked to draw then (each ver counted once; the newest is drawn when the
115 /// window's frame has arrived)
116 pub held: u64,
117 /// the size of its target (px)
118 pub size: (u32, u32),
119 /// what the last failure said
120 pub error: Option<String>,
121}
122
123/// the dark grey of a view without a device
124pub(crate) fn paint_default(g: &mut Gfx, r: [i32; 4]) {
125 g.dc.set_color(0xFF30_3438);
126 g.dc.fill(g.sf, r[0], r[1], r[2], r[3]);
127}
128
129/// a view's target on its device (gpu_out.rs makes it; the Draw reads it back)
130#[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
131pub(crate) struct Target {
132 pub(crate) dev: Device,
133 pub(crate) t: Texture,
134 /// the read-back's buffer, rows of `stride` bytes (WebGPU wants multiples of 256; Vulkan takes them too). The
135 /// browser: a buffer per read-back (a mapped one may not go into a submit again), on its way with its frame
136 #[cfg(not(target_arch = "wasm32"))]
137 pub(crate) rb: Buffer,
138 pub(crate) stride: u32,
139 #[cfg(target_arch = "wasm32")]
140 pub(crate) wait: Option<(Buffer, u64)>,
141}
142
143/// what the Draw of a view holds: the last frame on the CPU, the way to the newest
144pub(crate) struct Shared {
145 pub(crate) c: RefCell<Cpu>,
146}
147
148pub(crate) struct Cpu {
149 /// the last frame read back (none: 0 x 0) and its number
150 pub(crate) image: Surface,
151 pub(crate) image_frame: u64,
152 /// the newest frame the GPU drew into the target (0: none yet)
153 pub(crate) frame: u64,
154 #[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
155 pub(crate) gpu: Option<Target>,
156 pub(crate) content: Weak<RefCell<dyn GpuContent>>,
157 pub(crate) reads: u64,
158 pub(crate) read_us: u64,
159 #[cfg_attr(not(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32")), allow(dead_code))]
160 pub(crate) error: Option<String>,
161 #[cfg_attr(not(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32")), allow(dead_code))]
162 raw: Vec<u8>,
163}
164
165impl Cpu {
166 fn new(content: Weak<RefCell<dyn GpuContent>>) -> Cpu {
167 Cpu {
168 image: Surface::default(),
169 image_frame: 0,
170 frame: 0,
171 #[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
172 gpu: None,
173 content,
174 reads: 0,
175 read_us: 0,
176 error: None,
177 raw: Vec::new(),
178 }
179 }
180
181 /// the newest frame into the image (waits for the GPU; the browser cannot: there it comes with a later call)
182 /// -> the image holds it now
183 #[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
184 pub(crate) fn read(&mut self) -> bool {
185 if self.image_frame == self.frame {
186 return true;
187 }
188 let t0 = plat::time_us();
189 let frame = self.frame;
190 let Some(g) = self.gpu.as_mut() else { return false };
191 let (w, h, stride) = (g.t.width(), g.t.height(), g.stride);
192 let got = match read_back(g, frame, &mut self.raw) {
193 Ok(got) => got,
194 Err(e) => {
195 self.error = Some(alloc::format!("{e}"));
196 return false;
197 }
198 };
199 let Some(got) = got else { return false };
200 if (self.image.w, self.image.h) != (w as i32, h as i32) {
201 self.image = Surface::new(w as i32, h as i32, gfx::FMT_XRGB);
202 }
203 let (row, to) = (w as usize * 4, self.image.stride as usize);
204 for y in 0..h as usize {
205 self.image.px[y * to..y * to + row].copy_from_slice(&self.raw[y * stride as usize..][..row]);
206 }
207 self.image_frame = got;
208 self.reads += 1;
209 self.read_us += plat::time_us().saturating_sub(t0);
210 self.image_frame == self.frame
211 }
212}
213
214/// the target's frame into raw (rows of g.stride bytes) -> the number of the frame it holds, None: not yet (the
215/// browser). The target rests in Layout::Sampled between frames (the content's pass ends there): to CopySrc for
216/// the copy (after the shaders that sampled it: the output's pass), back to Sampled (before the next shader reads
217/// it); the copy's writes before the host reads them, which waits for the submission's value
218#[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple"))]
219fn read_back(g: &mut Target, frame: u64, raw: &mut Vec<u8>) -> gpu::Result<Option<u64>> {
220 let rb = &g.rb;
221 let mut e = g.dev.encoder()?;
222 e.texture_barrier(&g.t, Layout::Sampled, Layout::CopySrc);
223 e.copy_texture_to_buffer(TextureRegion::mip(&g.t, 0), rb, DataLayout { offset: 0, bytes_per_row: g.stride, rows_per_image: 0 });
224 e.texture_barrier(&g.t, Layout::CopySrc, Layout::Sampled);
225 e.barrier(Access::COPY_WRITE, Access::HOST_READ);
226 let id = g.dev.queue().submit([e.finish()?])?;
227 if !g.dev.wait(id, 5_000_000_000)? {
228 return Err(GpuError::Timeout);
229 }
230 raw.resize(rb.size() as usize, 0);
231 rb.read(0, &mut raw[..])?;
232 Ok(Some(frame))
233}
234
235/// (the browser) a read-back on its way: done -> its frame; else one is sent off for this frame and comes later
236#[cfg(target_arch = "wasm32")]
237fn read_back(g: &mut Target, frame: u64, raw: &mut Vec<u8>) -> gpu::Result<Option<u64>> {
238 use gpu::{BufferDesc, BufferUsage, MemoryKind};
239 if let Some((rb, at)) = &g.wait {
240 raw.resize(rb.size() as usize, 0);
241 return match rb.read(0, &mut raw[..]) {
242 Ok(()) => {
243 let at = *at;
244 g.wait = None;
245 Ok(Some(at))
246 }
247 Err(GpuError::NotReady) => Ok(None),
248 Err(e) => {
249 g.wait = None;
250 Err(e)
251 }
252 };
253 }
254 let rb = g.dev.create_buffer(&BufferDesc { size: g.stride as u64 * g.t.height() as u64, usage: BufferUsage::NONE, memory: MemoryKind::Readback })?;
255 let mut e = g.dev.encoder()?;
256 e.texture_barrier(&g.t, Layout::Sampled, Layout::CopySrc);
257 e.copy_texture_to_buffer(TextureRegion::mip(&g.t, 0), &rb, DataLayout { offset: 0, bytes_per_row: g.stride, rows_per_image: 0 });
258 e.texture_barrier(&g.t, Layout::CopySrc, Layout::Sampled);
259 e.barrier(Access::COPY_WRITE, Access::HOST_READ);
260 g.dev.queue().submit([e.finish()?])?;
261 g.wait = Some((rb, frame));
262 Ok(None)
263}
264
265/// a view's Draw: its last frame (the newest read back first), else the content's painting
266pub(crate) struct ViewDraw(pub(crate) Rc<Shared>);
267
268impl Draw for ViewDraw {
269 fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
270 #[cfg_attr(not(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32")), allow(unused_mut))]
271 let mut c = self.0.c.borrow_mut();
272 #[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
273 c.read();
274 if c.image_frame != 0 && (c.image.w, c.image.h) == (r[2], r[3]) {
275 g.dc.blit(g.sf, &c.image, r[0], r[1]);
276 return;
277 }
278 let content = c.content.upgrade();
279 drop(c);
280 // (borrowed elsewhere: an app that reads the framebuffer while it holds its content)
281 match content.as_ref().and_then(|x| x.try_borrow().ok()) {
282 Some(x) => x.paint(g, r),
283 None => paint_default(g, r),
284 }
285 }
286}
287
288/// a view's target on the device it is drawn with (gpu_out.rs): the texture, the output's bind group for it
289#[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
290pub(crate) struct ViewTex {
291 pub(crate) t: Texture,
292 pub(crate) group: Option<gpu::BindGroup>,
293 pub(crate) w: u32,
294 pub(crate) h: u32,
295 /// no frame drawn into it yet (Layout::Undefined)
296 pub(crate) fresh: core::cell::Cell<bool>,
297}
298
299/// a view the UI has seen: (window, id), its content, its Draw; the device side (gpu_out.rs)
300#[cfg_attr(not(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32")), allow(dead_code))]
301pub(crate) struct Entry {
302 pub(crate) win: usize,
303 pub(crate) id: u32,
304 pub(crate) content: Weak<RefCell<dyn GpuContent>>,
305 pub(crate) sh: Rc<Shared>,
306 pub(crate) draw: DrawRef,
307 /// the device generation the content is attached to (0: none), whether it is the output's, its level
308 pub(crate) devgen: u32,
309 pub(crate) output: bool,
310 pub(crate) level: Option<gpu::Level>,
311 /// a generation whose attach failed: not again on it
312 pub(crate) refused: u32,
313 #[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
314 pub(crate) tex: Option<Rc<ViewTex>>,
315 /// ver, size and generation of the last frame tried (another one draws a frame)
316 pub(crate) tried: Option<(u32, (u32, u32), u32)>,
317 /// a frame was drawn in this render; when its pixels go to the framebuffer at rest (0: done)
318 pub(crate) moved: bool,
319 /// (G5e2, the browser) a read-back came after the Draw that asked for it: the next render takes it to the framebuffer
320 /// as a new frame (gpu_out.rs gp_views_tick)
321 pub(crate) arrived: bool,
322 /// (G5e3) a frame waits for the window's last frame to be shown since then (0: none waits); the ver, size and
323 /// generation counted last in info.held; the render gp_views_wake asked for has not come yet
324 pub(crate) held_at: u64,
325 pub(crate) held_key: Option<(u32, (u32, u32), u32)>,
326 pub(crate) woke: bool,
327 pub(crate) rest_at: u64,
328 pub(crate) apart: bool,
329 pub(crate) info: GpuViewInfo,
330}
331
332/// the views' own device: none, made (on first use when there is no output), to be tried again, never
333#[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
334#[derive(Default)]
335pub(crate) enum Own {
336 #[default]
337 None,
338 Ready {
339 _inst: gpu::Instance,
340 dev: Device,
341 devgen: u32,
342 },
343 Retry(u64),
344 Never(String),
345}
346
347/// the views of all windows
348#[derive(Default)]
349pub(crate) struct S {
350 pub(crate) views: Vec<Entry>,
351 #[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
352 pub(crate) own: Own,
353}
354
355impl Ui {
356 /// a view of GPU content (gpu_view.rs): the layout's next rectangle (next_size) shows what `content` draws on
357 /// the UI's device, between the widgets, under popups and tooltips; its input is ui.canvas's. `ver` stands for
358 /// everything the view shows (scene, camera, an animation's time): another value draws a frame, the same one
359 /// none (an animation asks for passes itself, ui.invalidate). Hand the same content every frame
360 #[cfg_attr(jfgui_dev, track_caller)]
361 pub fn gpu_view<C: GpuContent + 'static>(&mut self, id: u32, content: &Rc<RefCell<C>>, ver: u32) -> &UiInput {
362 let rc: Rc<RefCell<dyn GpuContent>> = content.clone();
363 let win = self.uiw_cur as usize;
364 let draw = self.gv_entry(win, id, &rc);
365 let _ = self.canvas(id, Some(draw.clone()), 0, ver);
366 let l = self.wp.cmd.layer as usize;
367 let list = &self.wp.cmd.layers[l];
368 // (a full layer drops the command)
369 if let Some(Cmd::Custom { f: Some(f), .. }) = list.last() {
370 if Rc::as_ptr(f) as *const () == Rc::as_ptr(&draw) as *const () {
371 let index = list.len() as u32 - 1;
372 self.wp.canv.views.push(ViewRec { layer: l as u8, index, id, ver, apart: false });
373 }
374 }
375 &self.uiinput
376 }
377
378 /// what view `id` of window `win` (0 = main) does now; None: no such view
379 pub fn gpu_view_info(&self, win: u32, id: u32) -> Option<GpuViewInfo> {
380 let e = self.m.gview.views.iter().find(|e| e.win == win as usize && e.id == id)?;
381 let c = e.sh.c.borrow();
382 let mut i = e.info.clone();
383 i.device = e.devgen != 0;
384 i.output = e.devgen != 0 && e.output;
385 i.level = if e.devgen != 0 { e.level } else { None };
386 i.generation = e.devgen;
387 i.apart = e.apart;
388 i.frames = c.frame;
389 i.reads = c.reads;
390 i.read_us = c.read_us;
391 i.read_frame = c.image_frame;
392 if i.error.is_none() {
393 i.error = c.error.clone();
394 }
395 // (why there is no device: the views' own could not be made)
396 #[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
397 if let (None, 0, Own::Never(why)) = (&i.error, e.devgen, &self.m.gview.own) {
398 i.error = Some(why.clone());
399 }
400 Some(i)
401 }
402
403 /// the record of view id in window win, made the first time -> its Draw. Another content under the id: the old
404 /// one leaves its device, the new one starts afresh
405 fn gv_entry(&mut self, win: usize, id: u32, rc: &Rc<RefCell<dyn GpuContent>>) -> DrawRef {
406 let views = &mut self.m.gview.views;
407 let at = Rc::as_ptr(rc) as *const ();
408 if let Some(e) = views.iter_mut().find(|e| e.win == win && e.id == id) {
409 if e.content.as_ptr() as *const () != at {
410 if e.devgen != 0 {
411 if let Some(old) = e.content.upgrade() {
412 if let Ok(mut o) = old.try_borrow_mut() {
413 o.detach();
414 }
415 }
416 e.info.detaches += 1;
417 }
418 e.content = Rc::downgrade(rc);
419 e.devgen = 0;
420 e.refused = 0;
421 e.tried = None;
422 e.apart = false;
423 e.rest_at = 0;
424 e.held_at = 0;
425 e.held_key = None;
426 #[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
427 {
428 e.tex = None;
429 }
430 let mut c = e.sh.c.borrow_mut();
431 let (frame, reads, read_us) = (c.frame, c.reads, c.read_us);
432 *c = Cpu::new(Rc::downgrade(rc));
433 // (the numbers go on: a new frame must not look like an old one)
434 (c.frame, c.image_frame, c.reads, c.read_us) = (frame, frame, reads, read_us);
435 }
436 return e.draw.clone();
437 }
438 let sh = Rc::new(Shared { c: RefCell::new(Cpu::new(Rc::downgrade(rc))) });
439 let draw: DrawRef = Rc::new(ViewDraw(sh.clone()));
440 views.push(Entry {
441 win,
442 id,
443 content: Rc::downgrade(rc),
444 sh,
445 draw: draw.clone(),
446 devgen: 0,
447 output: false,
448 level: None,
449 refused: 0,
450 #[cfg(any(windows, jfgui_glibc, target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
451 tex: None,
452 tried: None,
453 moved: false,
454 arrived: false,
455 held_at: 0,
456 held_key: None,
457 woke: false,
458 rest_at: 0,
459 apart: false,
460 info: GpuViewInfo::default(),
461 });
462 draw
463 }
464}