Skip to main content

euv_engine/renderer/webgl/
struct.rs

1use super::*;
2
3/// A GPU buffer holding vertex, index, or uniform data.
4///
5/// Unlike the WebGPU backend, a WebGL buffer carries its own allocation
6/// size, so this wrapper remembers how many bytes were last written and
7/// can answer [`GlBuffer::fits`](super::GlBuffer::fits) without asking the
8/// driver. That is what lets
9/// [`GlBuffer::upload`](super::GlBuffer::upload) re-issue `bufferData` on
10/// a resize instead of creating a new `WebGlBuffer` object and leaving
11/// the old one to the garbage collector.
12#[derive(Clone, Data, Debug)]
13pub struct GlBuffer {
14    /// The GL buffer object.
15    pub(crate) buffer: WebGlBuffer,
16    /// The byte capacity the driver last allocated for this buffer.
17    #[get(type(copy))]
18    pub(crate) capacity: u32,
19    /// The GL usage hint passed at allocation: `STATIC_DRAW` for data
20    /// uploaded once, `DYNAMIC_DRAW` for data rewritten every frame.
21    #[get(type(copy))]
22    pub(crate) usage: u32,
23}
24
25/// A GPU texture holding sampled image data or render-target contents.
26///
27/// Carries the dimensions and mip count the driver was told about so a
28/// resize re-allocates instead of stretching stale texels, and records
29/// whether a mip chain exists so a caller cannot accidentally select a
30/// minification filter that samples across levels the texture does not
31/// have, which samples as solid black and is the single most common "my
32/// texture is invisible" WebGL bug.
33#[derive(Clone, Data, Debug)]
34pub struct GlTexture {
35    /// The GL texture object.
36    pub(crate) texture: WebGlTexture,
37    /// The width in texels the texture was allocated with.
38    #[get(type(copy))]
39    pub(crate) width: u32,
40    /// The height in texels the texture was allocated with.
41    #[get(type(copy))]
42    pub(crate) height: u32,
43    /// The number of mip levels allocated.
44    #[get(type(copy))]
45    pub(crate) levels: u32,
46    /// Whether `generateMipmap` was called, so a minification filter that
47    /// samples across levels is known to be legal.
48    #[get(type(copy))]
49    pub(crate) mipmapped: bool,
50}
51
52/// A vertex array object: the recorded bundle of buffer bindings and
53/// vertex attribute formats.
54///
55/// Binding a VAO replaces up to a dozen `bindBuffer` /
56/// `enableVertexAttribArray` / `vertexAttribPointer` calls with one,
57/// which is what makes per-mesh draw state cheap enough to change on
58/// every draw call.
59#[derive(Clone, Data, Debug)]
60pub struct GlVertexArray {
61    /// The GL vertex array object.
62    pub(crate) vao: WebGlVertexArrayObject,
63}
64
65/// A framebuffer: a color attachment texture plus an optional depth (and
66/// stencil) renderbuffer.
67///
68/// The color attachment is always a [`GlTexture`] rather than a
69/// renderbuffer, so the result can be sampled by a later pass or read
70/// back without a resolve blit.
71#[derive(Clone, Data, Debug)]
72pub struct GlFramebuffer {
73    /// The GL framebuffer object.
74    pub(crate) framebuffer: WebGlFramebuffer,
75    /// The depth renderbuffer, optionally packed with an 8-bit stencil,
76    /// or `None` for a color-only target such as a post-process pass
77    /// that reads depth from a texture instead of writing its own.
78    #[get(type(clone))]
79    pub(crate) depth: Option<WebGlRenderbuffer>,
80    /// The color attachment, retained so a later pass can sample the
81    /// rendered result without a separate resolve.
82    #[get(type(clone))]
83    pub(crate) color: GlTexture,
84    /// The width every attachment was allocated with, re-checked on
85    /// rebind so a window resize re-allocates rather than leaving a
86    /// frame whose attachments disagree on size.
87    #[get(type(copy))]
88    pub(crate) width: u32,
89    /// The height every attachment was allocated with.
90    #[get(type(copy))]
91    pub(crate) height: u32,
92}
93
94/// A linked GLSL program together with its cached uniform locations.
95///
96/// Uniform locations are stable for the lifetime of a linked program, but
97/// resolving one costs a round trip into the GL frontend that walks the
98/// program's uniform table. Caching them in a name-keyed map means a
99/// per-frame `set_uniform_1f` is a hash lookup plus the upload with no
100/// re-resolution, and a name the GLSL compiler optimized out caches the
101/// `None` so the miss is paid exactly once rather than every frame.
102#[derive(Clone, Data, Debug)]
103pub struct GlProgram {
104    /// The GL program object.
105    pub(crate) program: WebGlProgram,
106    /// Uniform locations resolved so far, keyed by uniform name. A
107    /// `None` entry records a uniform the compiler removed, so the
108    /// negative result is not recomputed every frame either.
109    #[get(type(clone))]
110    pub(crate) uniforms: HashMap<String, Option<WebGlUniformLocation>>,
111    /// The reusable 16-float scratch slice every `mat4` upload goes
112    /// through. A fixed-size array rather than a `Vec` so uploading a
113    /// [`Matrix4x4`] cannot allocate at all, not even on the first call.
114    pub(crate) matrix_scratch: [f32; GL_MAT4_FLOATS],
115    /// The uniform block indices resolved so far, keyed by block name.
116    #[get(type(clone))]
117    pub(crate) blocks: HashMap<String, u32>,
118}
119
120/// A uniform buffer bound to a numbered binding point.
121///
122/// WebGL 2 has no bind groups: the equivalent of a `var<uniform>` block
123/// is a `WebGlBuffer` bound to `UNIFORM_BUFFER` at a binding index the
124/// shader agrees on. This holds both halves of that agreement, the buffer
125/// and the binding point, so swapping programs cannot leave the two out
126/// of sync.
127#[derive(Clone, Data, Debug)]
128pub struct GlUniformBlock {
129    /// The GL buffer holding the block's records.
130    pub(crate) buffer: WebGlBuffer,
131    /// The binding point index the shader's `layout(binding = N)`
132    /// qualifier agrees with.
133    #[get(type(copy))]
134    pub(crate) binding: u32,
135    /// The byte capacity allocated for the block.
136    #[get(type(copy))]
137    pub(crate) capacity: u32,
138}
139
140/// The blend and color-write state of one draw.
141///
142/// Mirrors the alpha/color pair of [`BlendState`] exactly, so the WebGL
143/// path can consume the same blend the WebGPU path builds, but carries
144/// the extra `enabled` flag WebGL needs: WebGPU expresses "no blend" as
145/// `blend: None` on the target, whereas WebGL expresses it as leaving
146/// `BLEND` disabled, which is a different call with a different cost.
147#[derive(Clone, Copy, Data, Debug, Default, PartialEq)]
148pub struct GlBlendState {
149    /// Whether `BLEND` is enabled for this draw.
150    pub(crate) enabled: bool,
151    /// The blend applied to the RGB channels.
152    pub(crate) color: BlendComponent,
153    /// The blend applied to the alpha channel, which is frequently not
154    /// the same blend as the color channels.
155    pub(crate) alpha: BlendComponent,
156}
157
158/// The depth-test state of one draw.
159///
160/// WebGL splits "test" from "write" into two independent calls
161/// (`DEPTH_TEST` plus `depthFunc`, and `depthMask`), whereas WebGPU's
162/// [`DepthStencilState`] derives both from `depth_write_enabled` plus the
163/// presence of an attachment. Keeping the two axes separate here is what
164/// lets a transparent overlay bind a depth buffer it must read but must
165/// not write.
166#[derive(Clone, Copy, Data, Debug, New, PartialEq)]
167pub struct GlDepthState {
168    /// Whether `DEPTH_TEST` is enabled.
169    pub(crate) enabled: bool,
170    /// The comparison between the fragment's depth and the stored depth.
171    pub(crate) compare: CompareFunction,
172    /// Whether a fragment that passes the test updates the depth buffer.
173    pub(crate) write_enabled: bool,
174}
175
176/// The culling and winding state of one draw.
177#[derive(Clone, Copy, Data, Debug, Default, PartialEq)]
178pub struct GlCullState {
179    /// Which faces are discarded before rasterization.
180    pub(crate) mode: CullMode,
181    /// Which winding order counts as front facing. Flipping it swaps what
182    /// [`CullMode::Front`] and [`CullMode::Back`] discard.
183    pub(crate) front_face: FrontFace,
184}
185
186/// Which channels the fragment stage may write, as a raw four-bit mask.
187///
188/// Red is `0x1`, green `0x2`, blue `0x4`, alpha `0x8`, and all four is
189/// `0xf`. WebGL's `colorMask` is the only place a per-channel write mask
190/// can be expressed, and keeping it a bitmask rather than four booleans
191/// matches the [`ColorTargetState`] write-mask convention the WebGPU path
192/// already uses.
193#[derive(Clone, Copy, Data, Debug, New, PartialEq)]
194pub struct GlColorMask {
195    /// The four channel bits.
196    pub(crate) bits: u32,
197}
198
199/// A scissor rectangle.
200///
201/// A zero-sized box is legal and clips everything away, which is
202/// meaningfully different from scissoring being off; the off case is
203/// therefore expressed by the `Option` on [`GlRenderState::scissor`]
204/// rather than by a sentinel rectangle.
205#[derive(Clone, Copy, Data, Debug, New, PartialEq)]
206pub struct GlScissor {
207    /// The left edge in framebuffer pixels.
208    pub(crate) x: i32,
209    /// The bottom edge in framebuffer pixels.
210    pub(crate) y: i32,
211    /// The width in framebuffer pixels.
212    pub(crate) width: i32,
213    /// The height in framebuffer pixels.
214    pub(crate) height: i32,
215}
216
217/// The viewport rectangle, in framebuffer pixels.
218///
219/// Stored as signed values because GL accepts a negative origin for
220/// flipped-coordinate passes even though the default is the origin.
221#[derive(Clone, Copy, Data, Debug, New, PartialEq)]
222pub struct GlViewport {
223    /// The left edge in framebuffer pixels.
224    pub(crate) x: i32,
225    /// The bottom edge in framebuffer pixels.
226    pub(crate) y: i32,
227    /// The width in framebuffer pixels.
228    pub(crate) width: i32,
229    /// The height in framebuffer pixels.
230    pub(crate) height: i32,
231}
232
233/// The whole fixed-function state of one draw, as a single `Copy` value.
234///
235/// The point of bundling these six fields into one struct is that it can
236/// be diffed field by field against the state currently bound, so
237/// [`WebGl2Backend::apply_state`](super::WebGl2Backend::apply_state)
238/// issues a GL call only for what actually changed. A renderer that
239/// calls `enable`, `depthFunc`, `depthMask`, `blendFunc`,
240/// `blendEquation`, `cullFace`, `frontFace`, `colorMask`, `scissor`, and
241/// `viewport` unconditionally pays for a dozen redundant calls per draw,
242/// each one a validation check inside the driver.
243#[derive(Clone, Copy, Data, Debug, PartialEq)]
244pub struct GlRenderState {
245    /// The depth-test state.
246    pub(crate) depth: GlDepthState,
247    /// The blend state.
248    pub(crate) blend: GlBlendState,
249    /// The culling and winding state.
250    pub(crate) cull: GlCullState,
251    /// Which channels the fragment stage may write.
252    pub(crate) color_mask: GlColorMask,
253    /// The scissor rectangle, or `None` when scissoring is off.
254    #[get(type(copy))]
255    pub(crate) scissor: Option<GlScissor>,
256    /// The viewport rectangle.
257    pub(crate) viewport: GlViewport,
258}
259
260/// The complete WebGL 2 backend: a context plus the three shadows that
261/// make a frame cheap.
262///
263/// # Performance
264///
265/// Three things are shadowed rather than re-queried. The fixed-function
266/// state is shadowed by a [`GlRenderState`] copy, so applying a state
267/// costs only the calls for fields that actually changed. The current
268/// texture unit is shadowed by a `u32`, so binding a texture to the unit
269/// that is already active costs no `activeTexture` at all. The current
270/// program is shadowed by a `JsValue` compared with `Object.is`, so
271/// rebinding the same program costs no `useProgram`. None of the three
272/// can be answered by asking the driver without a synchronous
273/// round trip, which is exactly why they are tracked here instead.
274#[derive(Clone, Data, Debug)]
275pub struct WebGl2Backend {
276    /// The canvas element this backend draws into.
277    ///
278    /// Stored at construction instead of re-derived from
279    /// [`WebGl2RenderingContext::canvas`], so
280    /// [`WebGl2Backend::get_canvas`] is a plain field read and cannot
281    /// fail (the old `context.canvas().expect(..)` could panic if the
282    /// context was ever detached from its element).
283    pub(crate) canvas: HtmlCanvasElement,
284    /// The WebGL 2 rendering context every call goes through.
285    pub(crate) context: WebGl2RenderingContext,
286    /// The last state applied, diffed against to skip redundant calls.
287    pub(crate) shadow: GlRenderState,
288    /// The texture unit that is currently active, or
289    /// [`GL_TEXTURE_UNIT_NONE`] when the active unit is not yet known.
290    #[get(type(copy))]
291    pub(crate) active_unit: u32,
292    /// The program currently bound, compared with `Object.is`.
293    pub(crate) bound_program: JsValue,
294    /// The color [`WebGl2Backend::begin_frame`] clears to.
295    #[get(type(copy))]
296    pub(crate) clear_color_field: Color,
297    /// The reused destination for
298    /// [`WebGl2Backend::read_pixels`](super::WebGl2Backend::read_pixels),
299    /// grown on demand so a steady-state readback allocates nothing.
300    #[get_mut(pub(crate))]
301    pub(crate) readback: Vec<u8>,
302}
303
304/// Constructor inputs for [`WebGl2Backend`].
305///
306/// `WebGl2Backend::init` is the only constructor, and it needs a live
307/// document: it queries the canvas by selector and acquires a `webgl2`
308/// context. Every field on the backend is `pub(crate)`, so without this
309/// struct there is no way to build one from outside the crate and the
310/// state-diffing, draw-call and resize surface is untestable. Naming the
311/// inputs lets a caller that already holds a context construct a backend
312/// without going through DOM lookup.
313#[derive(Clone, Data, New)]
314pub struct WebGl2BackendInit {
315    /// The canvas the backend draws into.
316    pub canvas: HtmlCanvasElement,
317    /// The `webgl2` context every call goes through.
318    pub context: WebGl2RenderingContext,
319    /// The physical pixel width of the drawing buffer.
320    pub width: u32,
321    /// The physical pixel height of the drawing buffer.
322    pub height: u32,
323    /// The colour `begin_frame` clears to.
324    pub clear_color: Color,
325}
326
327/// Constructor inputs for [`GlRenderState`].
328///
329/// `GlRenderState::context_defaults` is the only way to build one today, and
330/// it hard-codes the state a *freshly created* GL context happens to be in.
331/// Every other state — a depth-tested draw, a scissored viewport, a masked
332/// render target — has to be assembled field by field to reach
333/// `WebGl2Backend::apply_state`, which means it cannot be built from outside
334/// the crate. Naming the inputs closes that gap.
335#[derive(Clone, Copy, Data, New)]
336pub struct GlRenderStateInit {
337    /// The depth-test state.
338    pub depth: GlDepthState,
339    /// The blend state.
340    pub blend: GlBlendState,
341    /// The culling and winding state.
342    pub cull: GlCullState,
343    /// Which channels the fragment stage may write.
344    pub color_mask: GlColorMask,
345    /// The scissor rectangle, or `None` when scissoring is off.
346    #[get(type(copy))]
347    pub scissor: Option<GlScissor>,
348    /// The viewport rectangle.
349    pub viewport: GlViewport,
350}