Skip to main content

euv_chart/backend/webgpu/
struct.rs

1use super::*;
2
3/// The WebGPU rendering backend for the chart.
4///
5/// This is the backend the chart prefers: vector geometry is tessellated
6/// into triangles once, uploaded to the GPU once, and replayed with a
7/// handful of draw calls. The display list is retained between frames, so a
8/// chart that has not changed does not re-tessellate and does not re-upload
9/// anything — the backend keeps the GPU buffers alive and reuses them as
10/// long as the list is unchanged.
11///
12/// The backend is created asynchronously, because acquiring a GPU adapter
13/// and a GPU device returns JavaScript promises. When the host cannot
14/// provide WebGPU, [`WebGpuChartBackend::create`] returns `None` and the
15/// factory falls back to WebGL2 and then to Canvas2D.
16#[derive(Data)]
17pub struct WebGpuChartBackend {
18    /// The canvas the chart draws into.
19    #[get(pub(crate))]
20    #[get_mut(pub(crate))]
21    #[set(pub(crate))]
22    pub(crate) canvas: HtmlCanvasElement,
23    /// The GPU device the resources are created on.
24    #[get(pub(crate))]
25    #[get_mut(pub(crate))]
26    #[set(pub(crate))]
27    pub(crate) device: JsValue,
28    /// The GPU queue the command buffers are submitted to.
29    #[get(pub(crate))]
30    #[get_mut(pub(crate))]
31    #[set(pub(crate))]
32    pub(crate) queue: JsValue,
33    /// The canvas's WebGPU context.
34    #[get(pub(crate))]
35    #[get_mut(pub(crate))]
36    #[set(pub(crate))]
37    pub(crate) context: JsValue,
38    /// The swap-chain texture format, as reported by
39    /// `getPreferredCanvasFormat`.
40    #[get(pub(crate))]
41    #[get_mut(pub(crate))]
42    #[set(pub(crate))]
43    pub(crate) format: String,
44    /// The JavaScript methods the backend calls every frame.
45    #[get(pub(crate))]
46    #[get_mut(pub(crate))]
47    #[set(pub(crate))]
48    pub(crate) api: WebGpuApi,
49    /// The methods of the command encoder, fetched on the first frame.
50    #[get(pub(crate))]
51    #[get_mut(pub(crate))]
52    #[set(pub(crate))]
53    pub(crate) encoder_api: Option<WebGpuEncoderApi>,
54    /// The methods of the render pass, fetched on the first frame.
55    #[get(pub(crate))]
56    #[get_mut(pub(crate))]
57    #[set(pub(crate))]
58    pub(crate) pass_api: Option<WebGpuPassApi>,
59    /// The pipeline that draws flat-coloured triangles.
60    #[get(pub(crate))]
61    #[get_mut(pub(crate))]
62    #[set(pub(crate))]
63    pub(crate) solid_pipeline: JsValue,
64    /// The pipeline that draws textured triangles.
65    #[get(pub(crate))]
66    #[get_mut(pub(crate))]
67    #[set(pub(crate))]
68    pub(crate) textured_pipeline: JsValue,
69    /// The bind group layout of the texture and the sampler.
70    #[get(pub(crate))]
71    #[get_mut(pub(crate))]
72    #[set(pub(crate))]
73    pub(crate) texture_layout: JsValue,
74    /// The sampler bound to the texture bind group.
75    #[get(pub(crate))]
76    #[get_mut(pub(crate))]
77    #[set(pub(crate))]
78    pub(crate) sampler: JsValue,
79    /// The buffer holding the CSS-pixel to clip-space transform.
80    #[get(pub(crate))]
81    #[get_mut(pub(crate))]
82    #[set(pub(crate))]
83    pub(crate) uniform_buffer: JsValue,
84    /// The bind group that exposes the transform to the pipelines.
85    #[get(pub(crate))]
86    #[get_mut(pub(crate))]
87    #[set(pub(crate))]
88    pub(crate) uniform_bind_group: JsValue,
89    /// The render pass descriptor, reused across frames.
90    #[get(pub(crate))]
91    #[get_mut(pub(crate))]
92    #[set(pub(crate))]
93    pub(crate) pass_descriptor: JsValue,
94    /// The colour attachment inside the render pass descriptor.
95    #[get(pub(crate))]
96    #[get_mut(pub(crate))]
97    #[set(pub(crate))]
98    pub(crate) pass_attachment: JsValue,
99    /// The glyph atlas texture and its bind group.
100    #[get(pub(crate))]
101    #[get_mut(pub(crate))]
102    #[set(pub(crate))]
103    pub(crate) atlas: WebGpuAtlas,
104    /// The buffer holding the flat-coloured vertices.
105    #[get(pub(crate))]
106    #[get_mut(pub(crate))]
107    #[set(pub(crate))]
108    pub(crate) solid_buffer: WebGpuBuffer,
109    /// The buffer holding the indices of the flat-coloured vertices.
110    #[get(pub(crate))]
111    #[get_mut(pub(crate))]
112    #[set(pub(crate))]
113    pub(crate) solid_index_buffer: WebGpuBuffer,
114    /// The buffer holding the textured vertices.
115    #[get(pub(crate))]
116    #[get_mut(pub(crate))]
117    #[set(pub(crate))]
118    pub(crate) textured_buffer: WebGpuBuffer,
119    /// The buffer holding the indices of the textured vertices.
120    #[get(pub(crate))]
121    #[get_mut(pub(crate))]
122    #[set(pub(crate))]
123    pub(crate) textured_index_buffer: WebGpuBuffer,
124    /// The batches to draw, in paint order.
125    #[get(pub(crate))]
126    #[get_mut(pub(crate))]
127    #[set(pub(crate))]
128    pub(crate) batches: Vec<WebGpuBatch>,
129    /// The images the display list refers to.
130    #[get(pub(crate))]
131    #[get_mut(pub(crate))]
132    #[set(pub(crate))]
133    pub(crate) images: Vec<WebGpuImage>,
134    /// The identifier handed to the next image that shows up.
135    #[get(pub(crate), type(copy))]
136    #[get_mut(pub(crate))]
137    #[set(pub(crate))]
138    pub(crate) next_image_id: u32,
139    /// The number of glyphs the atlas held at the last render.
140    ///
141    /// The atlas starts over when it runs out of room, which moves the
142    /// glyphs around. A shrinking count is the signal that the text
143    /// geometry has to be built again.
144    #[get(pub(crate), type(copy))]
145    #[get_mut(pub(crate))]
146    #[set(pub(crate))]
147    pub(crate) atlas_glyphs: usize,
148    /// The fingerprint of the display list the buffers currently hold.
149    #[get(pub(crate))]
150    #[get_mut(pub(crate))]
151    #[set(pub(crate))]
152    pub(crate) fingerprint: Option<u64>,
153    /// The tessellator, reused across frames so its scratch buffers are
154    /// allocated once.
155    #[get(pub(crate))]
156    #[get_mut(pub(crate))]
157    #[set(pub(crate))]
158    pub(crate) tessellator: Tessellator,
159    /// The logical width of the canvas, in CSS pixels.
160    #[get(pub(crate), type(copy))]
161    #[get_mut(pub(crate))]
162    #[set(pub(crate))]
163    pub(crate) width: f64,
164    /// The logical height of the canvas, in CSS pixels.
165    #[get(pub(crate), type(copy))]
166    #[get_mut(pub(crate))]
167    #[set(pub(crate))]
168    pub(crate) height: f64,
169    /// The device pixel ratio the canvas renders at.
170    #[get(pub(crate), type(copy))]
171    #[get_mut(pub(crate))]
172    #[set(pub(crate))]
173    pub(crate) dpr: f64,
174    /// The scratch storage for the flat-coloured vertices.
175    #[get(pub(crate))]
176    #[get_mut(pub(crate))]
177    #[set(pub(crate))]
178    pub(crate) solid_vertices: Vec<u8>,
179    /// The scratch storage for the indices of the flat-coloured vertices.
180    #[get(pub(crate))]
181    #[get_mut(pub(crate))]
182    #[set(pub(crate))]
183    pub(crate) solid_indices: Vec<u32>,
184    /// The scratch storage for the textured vertices.
185    #[get(pub(crate))]
186    #[get_mut(pub(crate))]
187    #[set(pub(crate))]
188    pub(crate) textured_vertices: Vec<u8>,
189    /// The scratch storage for the indices of the textured vertices.
190    #[get(pub(crate))]
191    #[get_mut(pub(crate))]
192    #[set(pub(crate))]
193    pub(crate) textured_indices: Vec<u32>,
194    /// The clip rectangles currently in effect.
195    #[get(pub(crate))]
196    #[get_mut(pub(crate))]
197    #[set(pub(crate))]
198    pub(crate) clips: Vec<WebGpuScissor>,
199}
200
201/// A GPU buffer whose capacity the backend manages.
202///
203/// The buffer is only re-created when the data outgrows it, and the new
204/// capacity is rounded up to a power of two so that a chart whose data
205/// grows a little at a time does not re-create the buffer on every change.
206#[derive(Data)]
207pub(crate) struct WebGpuBuffer {
208    /// The GPU buffer, or `None` before the first allocation.
209    #[get(pub(crate))]
210    #[get_mut(pub(crate))]
211    #[set(pub(crate))]
212    pub(crate) buffer: Option<JsValue>,
213    /// The capacity of the buffer, in bytes.
214    #[get(pub(crate), type(copy))]
215    #[get_mut(pub(crate))]
216    #[set(pub(crate))]
217    pub(crate) capacity: u32,
218}
219
220/// The GPU-side state of the glyph atlas.
221///
222/// The atlas is a canvas the text shaper rasterises glyphs into. The
223/// backend uploads it as a texture and binds it to the textured pipeline,
224/// which is how the GPU backends draw text at all — WebGPU has no text
225/// API.
226#[derive(Clone, Debug)]
227pub(crate) struct WebGpuAtlas {
228    /// The atlas texture, or `None` before the first upload.
229    pub(crate) texture: Option<JsValue>,
230    /// The bind group that binds the atlas to group one.
231    pub(crate) bind_group: Option<JsValue>,
232    /// The width of the atlas texture, in pixels.
233    pub(crate) width: u32,
234    /// The height of the atlas texture, in pixels.
235    pub(crate) height: u32,
236}
237
238/// The GPU-side state of one image.
239///
240/// Images are uploaded with `copyExternalImageToTexture` and drawn with the
241/// textured pipeline, using a bind group of their own.
242pub(crate) struct WebGpuImage {
243    /// The image element the texture was made from.
244    pub(crate) element: JsValue,
245    /// A stable identifier, used to notice that the display list changed.
246    pub(crate) id: u32,
247    /// The texture holding the image, or `None` before the first upload.
248    pub(crate) texture: Option<JsValue>,
249    /// The bind group that binds the texture to group one.
250    pub(crate) bind_group: Option<JsValue>,
251    /// The width of the texture, in pixels.
252    pub(crate) width: u32,
253    /// The height of the texture, in pixels.
254    pub(crate) height: u32,
255    /// The source the texture was made from, used to notice a reload.
256    pub(crate) source: String,
257}
258
259/// One contiguous run of triangles that costs a single draw call.
260///
261/// The batches are kept in paint order, and consecutive batches usually
262/// share a pipeline, so the render pass only has to rebind when the kind
263/// changes.
264pub(crate) struct WebGpuBatch {
265    /// The pipeline the batch is drawn with.
266    pub(crate) kind: WebGpuBatchKind,
267    /// The index of the batch's first triangle.
268    pub(crate) first_index: u32,
269    /// The number of indices in the batch.
270    pub(crate) index_count: u32,
271    /// The bind group bound to group one, for textured batches.
272    pub(crate) bind_group: Option<JsValue>,
273    /// The scissor rectangle the batch is clipped to, if any.
274    pub(crate) scissor: Option<WebGpuScissor>,
275}
276
277/// A scissor rectangle, in physical pixels.
278#[derive(Clone, Copy, Debug, Eq, PartialEq)]
279pub(crate) struct WebGpuScissor {
280    /// The left edge of the rectangle.
281    pub(crate) x: u32,
282    /// The top edge of the rectangle.
283    pub(crate) y: u32,
284    /// The width of the rectangle.
285    pub(crate) width: u32,
286    /// The height of the rectangle.
287    pub(crate) height: u32,
288}
289
290/// The JavaScript methods the backend calls on the device, the queue, and
291/// the canvas context.
292///
293/// The methods are fetched once, when the backend is created: JavaScript
294/// class methods live on the prototype, so every call returns the same
295/// `Function` object, and caching them keeps a steady-state frame free of
296/// property lookups.
297#[derive(Clone)]
298pub(crate) struct WebGpuApi {
299    /// `GPUDevice.createCommandEncoder`.
300    pub(crate) create_command_encoder: Function,
301    /// `GPUQueue.submit`.
302    pub(crate) submit: Function,
303    /// `GPUQueue.writeBuffer`.
304    pub(crate) write_buffer: Function,
305    /// `GPUQueue.copyExternalImageToTexture`.
306    pub(crate) copy_external_image: Function,
307    /// `GPUCanvasContext.getCurrentTexture`.
308    pub(crate) get_current_texture: Function,
309}
310
311/// The methods of the command encoder.
312///
313/// The encoder is created per frame, but its methods live on the
314/// prototype, so they are fetched from the first encoder and then reused.
315#[derive(Clone)]
316pub(crate) struct WebGpuEncoderApi {
317    /// `GPUCommandEncoder.beginRenderPass`.
318    pub(crate) begin_render_pass: Function,
319    /// `GPUCommandEncoder.finish`.
320    pub(crate) finish_encoder: Function,
321}
322
323/// The methods of the render pass.
324///
325/// The pass is created per frame, but its methods live on the prototype,
326/// so they are fetched from the first pass and then reused.
327#[derive(Clone)]
328pub(crate) struct WebGpuPassApi {
329    /// `GPURenderPassEncoder.setPipeline`.
330    pub(crate) set_pipeline: Function,
331    /// `GPURenderPassEncoder.setVertexBuffer`.
332    pub(crate) set_vertex_buffer: Function,
333    /// `GPURenderPassEncoder.setIndexBuffer`.
334    pub(crate) set_index_buffer: Function,
335    /// `GPURenderPassEncoder.setBindGroup`.
336    pub(crate) set_bind_group: Function,
337    /// `GPURenderPassEncoder.setScissorRect`.
338    pub(crate) set_scissor_rect: Function,
339    /// `GPURenderPassEncoder.drawIndexed`.
340    pub(crate) draw_indexed: Function,
341    /// `GPURenderPassEncoder.end`.
342    pub(crate) end_pass: Function,
343}
344
345/// A small FNV-1a hasher, used to fingerprint the display list.
346///
347/// The fingerprint tells the backend whether the geometry it holds on the
348/// GPU is still the geometry the display list asks for. Hashing the actual
349/// coordinates — rather than just counting the shapes — means a chart that
350/// moves its data but keeps the same number of points is still noticed.
351#[derive(Data)]
352pub(crate) struct GpuHasher {
353    /// The running hash state.
354    #[get(pub(crate), type(copy))]
355    #[get_mut(pub(crate))]
356    #[set(pub(crate))]
357    pub(crate) state: u64,
358}