Skip to main content

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}