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, 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, 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, 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, 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    pub(crate) scissor: Option<GlScissor>,
255    /// The viewport rectangle.
256    pub(crate) viewport: GlViewport,
257}
258
259/// The complete WebGL 2 backend: a context plus the three shadows that
260/// make a frame cheap.
261///
262/// # Performance
263///
264/// Three things are shadowed rather than re-queried. The fixed-function
265/// state is shadowed by a [`GlRenderState`] copy, so applying a state
266/// costs only the calls for fields that actually changed. The current
267/// texture unit is shadowed by a `u32`, so binding a texture to the unit
268/// that is already active costs no `activeTexture` at all. The current
269/// program is shadowed by a `JsValue` compared with `Object.is`, so
270/// rebinding the same program costs no `useProgram`. None of the three
271/// can be answered by asking the driver without a synchronous
272/// round trip, which is exactly why they are tracked here instead.
273#[derive(Clone, Data, Debug)]
274pub struct WebGl2Backend {
275    /// The canvas element this backend draws into.
276    ///
277    /// Stored at construction instead of re-derived from
278    /// [`WebGl2RenderingContext::canvas`], so
279    /// [`WebGl2Backend::get_canvas`] is a plain field read and cannot
280    /// fail (the old `context.canvas().expect(..)` could panic if the
281    /// context was ever detached from its element).
282    pub(crate) canvas: HtmlCanvasElement,
283    /// The WebGL 2 rendering context every call goes through.
284    pub(crate) context: WebGl2RenderingContext,
285    /// The last state applied, diffed against to skip redundant calls.
286    pub(crate) shadow: GlRenderState,
287    /// The texture unit that is currently active, or
288    /// [`GL_TEXTURE_UNIT_NONE`] when the active unit is not yet known.
289    #[get(type(copy))]
290    pub(crate) active_unit: u32,
291    /// The program currently bound, compared with `Object.is`.
292    pub(crate) bound_program: JsValue,
293    /// The color [`WebGl2Backend::begin_frame`] clears to.
294    #[get(type(copy))]
295    pub(crate) clear_color_field: Color,
296    /// The reused destination for
297    /// [`WebGl2Backend::read_pixels`](super::WebGl2Backend::read_pixels),
298    /// grown on demand so a steady-state readback allocates nothing.
299    #[get_mut(pub(crate))]
300    pub(crate) readback: Vec<u8>,
301}