Skip to main content

siglus_scene_vm/
soft_render.rs

1use crate::assets::RgbaImage;
2use crate::image_manager::ImageManager;
3use crate::layer::{ClipRect, RenderSprite, SpriteBlend, SpriteFit, SpriteSizeMode};
4use crate::render_math::sprite_quad_points;
5
6fn uses_depth_pipeline(sprite: &crate::layer::Sprite) -> bool {
7    sprite.camera_enabled
8        || sprite.billboard
9        || sprite.z.abs() > f32::EPSILON
10        || sprite.pivot_z.abs() > f32::EPSILON
11        || (sprite.scale_z - 1.0).abs() > 1e-6
12        || sprite.rotate_x.abs() > f32::EPSILON
13        || sprite.rotate_y.abs() > f32::EPSILON
14}
15
16pub fn render_to_image(
17    images: &ImageManager,
18    sprites: &[RenderSprite],
19    width: u32,
20    height: u32,
21) -> RgbaImage {
22    let mut out = vec![
23        0u8;
24        (width as usize)
25            .saturating_mul(height as usize)
26            .saturating_mul(4)
27    ];
28    let win_w = width as i32;
29    let win_h = height as i32;
30    let mut depth_buf = vec![f32::INFINITY; (width as usize).saturating_mul(height as usize)];
31
32    for s in sprites {
33        let sprite = &s.sprite;
34        let Some(img_id) = sprite.image_id else {
35            continue;
36        };
37        let Some(img) = images.get(img_id) else {
38            continue;
39        };
40        let tone_img = sprite.tonecurve_image_id.and_then(|id| images.get(id));
41        let mask_img = sprite.mask_image_id.and_then(|id| images.get(id));
42        let wipe_src_img = sprite.wipe_src_image_id.and_then(|id| images.get(id));
43
44        let (src_left, src_top, src_right, src_bottom) =
45            match src_clip_rect(sprite.src_clip, img.width, img.height) {
46                Ok(v) => v,
47                Err(_) => continue,
48            };
49        let src_w = (src_right - src_left).max(1.0);
50        let src_h = (src_bottom - src_top).max(1.0);
51
52        let (dst_x, dst_y, dst_w, dst_h) = match sprite.fit {
53            SpriteFit::FullScreen => (0.0f32, 0.0f32, win_w as f32, win_h as f32),
54            SpriteFit::PixelRect => {
55                let (w, h) = match sprite.size_mode {
56                    SpriteSizeMode::Intrinsic => (src_w, src_h),
57                    SpriteSizeMode::Explicit { width, height } => (width as f32, height as f32),
58                };
59                (sprite.x as f32, sprite.y as f32, w, h)
60            }
61        };
62
63        if dst_w <= 0.0 || dst_h <= 0.0 {
64            continue;
65        }
66
67        let Some([p0, p1, p2, p3]) =
68            sprite_quad_points(sprite, dst_x, dst_y, dst_w, dst_h, width as f32, height as f32)
69        else {
70            continue;
71        };
72
73        let mut min_x = p0.x.min(p1.x).min(p2.x).min(p3.x).floor() as i32;
74        let mut max_x = p0.x.max(p1.x).max(p2.x).max(p3.x).ceil() as i32;
75        let mut min_y = p0.y.min(p1.y).min(p2.y).min(p3.y).floor() as i32;
76        let mut max_y = p0.y.max(p1.y).max(p2.y).max(p3.y).ceil() as i32;
77
78        min_x = min_x.clamp(0, win_w);
79        max_x = max_x.clamp(0, win_w);
80        min_y = min_y.clamp(0, win_h);
81        max_y = max_y.clamp(0, win_h);
82
83        if let Some(sci) = dst_scissor_rect(sprite.dst_clip, width, height) {
84            let sci_left = sci.x as i32;
85            let sci_top = sci.y as i32;
86            let sci_right = (sci.x + sci.w) as i32;
87            let sci_bottom = (sci.y + sci.h) as i32;
88            min_x = min_x.max(sci_left);
89            max_x = max_x.min(sci_right);
90            min_y = min_y.max(sci_top);
91            max_y = max_y.min(sci_bottom);
92        }
93
94        if min_x >= max_x || min_y >= max_y {
95            continue;
96        }
97
98        let alpha = (sprite.alpha as f32) / 255.0;
99        let tr = (sprite.tr as f32) / 255.0;
100        let mono = (sprite.mono as f32) / 255.0;
101        let reverse = (sprite.reverse as f32) / 255.0;
102        let bright = (sprite.bright as f32) / 255.0;
103        let dark = (sprite.dark as f32) / 255.0;
104        let color_rate = (sprite.color_rate as f32) / 255.0;
105        let color_add_r = (sprite.color_add_r as f32) / 255.0;
106        let color_add_g = (sprite.color_add_g as f32) / 255.0;
107        let color_add_b = (sprite.color_add_b as f32) / 255.0;
108        let color_r = (sprite.color_r as f32) / 255.0;
109        let color_g = (sprite.color_g as f32) / 255.0;
110        let color_b = (sprite.color_b as f32) / 255.0;
111
112        let use_depth = uses_depth_pipeline(sprite);
113        let tris = [
114            (
115                [
116                    (p0.x, p0.y, p0.depth),
117                    (p1.x, p1.y, p1.depth),
118                    (p2.x, p2.y, p2.depth),
119                ],
120                [
121                    (src_left, src_top),
122                    (src_right, src_top),
123                    (src_right, src_bottom),
124                ],
125            ),
126            (
127                [
128                    (p0.x, p0.y, p0.depth),
129                    (p2.x, p2.y, p2.depth),
130                    (p3.x, p3.y, p3.depth),
131                ],
132                [
133                    (src_left, src_top),
134                    (src_right, src_bottom),
135                    (src_left, src_bottom),
136                ],
137            ),
138        ];
139
140        for (tri, uv_tri) in tris {
141            let area = edge(
142                (tri[0].0, tri[0].1),
143                (tri[1].0, tri[1].1),
144                (tri[2].0, tri[2].1),
145            );
146            if area.abs() <= f32::EPSILON {
147                continue;
148            }
149            for y in min_y..max_y {
150                for x in min_x..max_x {
151                    let p = (x as f32 + 0.5, y as f32 + 0.5);
152                    let w0 = edge((tri[1].0, tri[1].1), (tri[2].0, tri[2].1), p) / area;
153                    let w1 = edge((tri[2].0, tri[2].1), (tri[0].0, tri[0].1), p) / area;
154                    let w2 = edge((tri[0].0, tri[0].1), (tri[1].0, tri[1].1), p) / area;
155                    if w0 < -1e-5 || w1 < -1e-5 || w2 < -1e-5 {
156                        continue;
157                    }
158
159                    let depth = w0 * tri[0].2 + w1 * tri[1].2 + w2 * tri[2].2;
160                    let u = w0 * uv_tri[0].0 + w1 * uv_tri[1].0 + w2 * uv_tri[2].0;
161                    let v = w0 * uv_tri[0].1 + w1 * uv_tri[1].1 + w2 * uv_tri[2].1;
162                    if u < src_left || v < src_top || u >= src_right || v >= src_bottom {
163                        continue;
164                    }
165                    let sx = u.floor() as i32;
166                    let sy = v.floor() as i32;
167                    if sx < 0 || sy < 0 || sx >= img.width as i32 || sy >= img.height as i32 {
168                        continue;
169                    }
170
171                    let z_idx = (y as u32 * width + x as u32) as usize;
172                    if use_depth {
173                        if depth > depth_buf[z_idx] + 1e-6 {
174                            continue;
175                        }
176                        depth_buf[z_idx] = depth;
177                    }
178
179                    let src = sample_base_with_wipe(
180                        sprite,
181                        img,
182                        wipe_src_img.as_deref().map(|v| &**v),
183                        u,
184                        v,
185                    );
186                    let sr = src[0];
187                    let sg = src[1];
188                    let sb = src[2];
189                    let sa = src[3];
190
191                    let mut r = sr;
192                    let mut g = sg;
193                    let mut b = sb;
194
195                    if let Some(tone_img) = tone_img.as_ref() {
196                        let sat = sprite.tonecurve_sat.clamp(0.0, 1.0);
197                        if sat > 0.0 {
198                            let gray = r * 0.299 + g * 0.587 + b * 0.114;
199                            r = r + sat * (gray - r);
200                            g = g + sat * (gray - g);
201                            b = b + sat * (gray - b);
202                        }
203                        let row = (sprite.tonecurve_row.clamp(0.0, 1.0) * tone_img.height as f32)
204                            .floor()
205                            .clamp(0.0, (tone_img.height.saturating_sub(1)) as f32)
206                            as u32;
207                        let ri = (r.clamp(0.0, 1.0) * 255.0).round().clamp(0.0, 255.0) as u32;
208                        let gi = (g.clamp(0.0, 1.0) * 255.0).round().clamp(0.0, 255.0) as u32;
209                        let bi = (b.clamp(0.0, 1.0) * 255.0).round().clamp(0.0, 255.0) as u32;
210                        r = tone_img.rgba[((row * tone_img.width
211                            + ri.min(tone_img.width.saturating_sub(1)))
212                            * 4) as usize] as f32
213                            / 255.0;
214                        g = tone_img.rgba[((row * tone_img.width
215                            + gi.min(tone_img.width.saturating_sub(1)))
216                            * 4
217                            + 1) as usize] as f32
218                            / 255.0;
219                        b = tone_img.rgba[((row * tone_img.width
220                            + bi.min(tone_img.width.saturating_sub(1)))
221                            * 4
222                            + 2) as usize] as f32
223                            / 255.0;
224                    }
225
226                    if reverse > 0.0 {
227                        r = r + reverse * ((1.0 - r) - r);
228                        g = g + reverse * ((1.0 - g) - g);
229                        b = b + reverse * ((1.0 - b) - b);
230                    }
231                    if mono > 0.0 {
232                        let gray = r * 0.299 + g * 0.587 + b * 0.114;
233                        r = r + mono * (gray - r);
234                        g = g + mono * (gray - g);
235                        b = b + mono * (gray - b);
236                    }
237
238                    r = (r + bright).clamp(0.0, 1.0);
239                    g = (g + bright).clamp(0.0, 1.0);
240                    b = (b + bright).clamp(0.0, 1.0);
241                    r = (r - dark).clamp(0.0, 1.0);
242                    g = (g - dark).clamp(0.0, 1.0);
243                    b = (b - dark).clamp(0.0, 1.0);
244
245                    if color_rate > 0.0 {
246                        r = r + color_rate * (color_r - r);
247                        g = g + color_rate * (color_g - g);
248                        b = b + color_rate * (color_b - b);
249                    }
250                    r = (r + color_add_r).clamp(0.0, 1.0);
251                    g = (g + color_add_g).clamp(0.0, 1.0);
252                    b = (b + color_add_b).clamp(0.0, 1.0);
253
254                    let mut mask_alpha = sa;
255                    if let Some(mask_img) = mask_img.as_ref() {
256                        let mx = sx + sprite.mask_offset_x;
257                        let my = sy + sprite.mask_offset_y;
258                        if mx >= 0
259                            && my >= 0
260                            && mx < mask_img.width as i32
261                            && my < mask_img.height as i32
262                        {
263                            let mi = ((my as u32 * mask_img.width + mx as u32) * 4) as usize;
264                            let mr = mask_img.rgba[mi] as f32 / 255.0;
265                            let mg = mask_img.rgba[mi + 1] as f32 / 255.0;
266                            let mb = mask_img.rgba[mi + 2] as f32 / 255.0;
267                            let ma = mask_img.rgba[mi + 3] as f32 / 255.0;
268                            mask_alpha *=
269                                ((mr * 0.299 + mg * 0.587 + mb * 0.114) * ma).clamp(0.0, 1.0);
270                        } else {
271                            mask_alpha = 0.0;
272                        }
273                    }
274                    if sprite.mask_mode == 1 {
275                        mask_alpha = sr * 0.299 + sg * 0.587 + sb * 0.114;
276                    }
277                    if sprite.alpha_test && mask_alpha <= 0.0 {
278                        continue;
279                    }
280                    let a = (mask_alpha * alpha * tr).clamp(0.0, 1.0);
281                    if a <= 0.0 {
282                        continue;
283                    }
284
285                    let src_premul = [r * a, g * a, b * a, a];
286                    let dst_idx = ((y as u32 * width + x as u32) * 4) as usize;
287                    let dr = out[dst_idx] as f32 / 255.0;
288                    let dg = out[dst_idx + 1] as f32 / 255.0;
289                    let db = out[dst_idx + 2] as f32 / 255.0;
290                    let da = out[dst_idx + 3] as f32 / 255.0;
291
292                    let dst = [dr, dg, db, da];
293                    let blended = if sprite.alpha_blend {
294                        blend_pixel(dst, src_premul, sprite.blend)
295                    } else {
296                        src_premul
297                    };
298
299                    out[dst_idx] = (blended[0].clamp(0.0, 1.0) * 255.0).round() as u8;
300                    out[dst_idx + 1] = (blended[1].clamp(0.0, 1.0) * 255.0).round() as u8;
301                    out[dst_idx + 2] = (blended[2].clamp(0.0, 1.0) * 255.0).round() as u8;
302                    out[dst_idx + 3] = (blended[3].clamp(0.0, 1.0) * 255.0).round() as u8;
303                }
304            }
305        }
306    }
307
308    RgbaImage {
309        width,
310        height,
311        center_x: 0,
312        center_y: 0,
313        rgba: out,
314    }
315}
316
317fn sample_rgba_norm(img: &RgbaImage, u: f32, v: f32) -> [f32; 4] {
318    if !(0.0..=1.0).contains(&u) || !(0.0..=1.0).contains(&v) {
319        return [0.0, 0.0, 0.0, 0.0];
320    }
321    let x = (u * img.width as f32)
322        .floor()
323        .clamp(0.0, (img.width.saturating_sub(1)) as f32) as u32;
324    let y = (v * img.height as f32)
325        .floor()
326        .clamp(0.0, (img.height.saturating_sub(1)) as f32) as u32;
327    let i = ((y * img.width + x) * 4) as usize;
328    [
329        img.rgba[i] as f32 / 255.0,
330        img.rgba[i + 1] as f32 / 255.0,
331        img.rgba[i + 2] as f32 / 255.0,
332        img.rgba[i + 3] as f32 / 255.0,
333    ]
334}
335
336fn raster_amp(progress: f32) -> f32 {
337    let rp = (1.0 - progress).clamp(1e-4, 1.0);
338    1.0 - (((1.0 - rp) * 100.0).log10() + 1.0) / 3.0
339}
340
341fn sample_base_with_wipe(
342    sprite: &crate::layer::Sprite,
343    img: &RgbaImage,
344    wipe_src_img: Option<&RgbaImage>,
345    u_px: f32,
346    v_px: f32,
347) -> [f32; 4] {
348    let mut u = (u_px / img.width as f32).clamp(0.0, 1.0);
349    let mut v = (v_px / img.height as f32).clamp(0.0, 1.0);
350    match sprite.wipe_fx_mode {
351        1 => {
352            let cu = sprite.wipe_fx_params[0].max(1e-5);
353            let cv = (cu * sprite.wipe_fx_params[1].max(1e-5)).max(1e-5);
354            u = (u / cu).floor() * cu;
355            v = (v / cv).floor() * cv;
356            sample_rgba_norm(img, u, v)
357        }
358        2 | 3 => {
359            let fraction_num = sprite.wipe_fx_params[0].max(1.0);
360            let wave_num = sprite.wipe_fx_params[1];
361            let power = sprite.wipe_fx_params[2];
362            let progress = sprite.wipe_fx_params[3];
363            let mut tex_coord_for_sin = if sprite.wipe_fx_mode == 2 {
364                v * fraction_num
365            } else {
366                u * fraction_num
367            };
368            tex_coord_for_sin = tex_coord_for_sin.fract();
369            tex_coord_for_sin = (tex_coord_for_sin - fraction_num * 0.1) / fraction_num;
370            let delta = (std::f32::consts::PI * progress * power
371                + tex_coord_for_sin * std::f32::consts::PI * wave_num)
372                .sin()
373                * raster_amp(progress);
374            if sprite.wipe_fx_mode == 2 {
375                u += delta;
376            } else {
377                v += delta;
378            }
379            sample_rgba_norm(img, u, v)
380        }
381        4 => {
382            let center = (sprite.wipe_fx_params[0], sprite.wipe_fx_params[1]);
383            let blur_power = sprite.wipe_fx_params[2];
384            let blur_coeff = sprite.wipe_fx_params[3].max(0.0);
385            let mut dir = (center.0 - u, center.1 - v);
386            let len = (dir.0 * dir.0 + dir.1 * dir.1).sqrt();
387            if len <= 1e-5 || blur_power <= 1e-5 {
388                return sample_rgba_norm(img, u, v);
389            }
390            let texel = 1.0 / (img.width.max(img.height).max(1) as f32);
391            dir.0 = dir.0 / len * texel * blur_power * len * blur_coeff;
392            dir.1 = dir.1 / len * texel * blur_power * len * blur_coeff;
393            let taps = [
394                (0.0, 0.19f32),
395                (1.0, 0.17),
396                (2.0, 0.15),
397                (3.0, 0.13),
398                (4.0, 0.11),
399                (5.0, 0.09),
400                (6.0, 0.07),
401                (7.0, 0.05),
402                (8.0, 0.03),
403                (9.0, 0.01),
404            ];
405            let mut out = [0.0f32; 4];
406            for (k, w) in taps {
407                let s = sample_rgba_norm(img, u + dir.0 * k, v + dir.1 * k);
408                for i in 0..4 {
409                    out[i] += s[i] * w;
410                }
411            }
412            out
413        }
414        5 => {
415            let mut c = sample_rgba_norm(img, u, v);
416            let fade = sprite.wipe_fx_params[0].clamp(0.0, 1.0);
417            let progress = sprite.wipe_fx_params[1].clamp(0.0, 1.0);
418            let brightness = 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2];
419            if brightness > progress {
420                c[3] *= (fade * (1.0 - progress)).clamp(0.0, 1.0);
421            }
422            c
423        }
424        6 => {
425            let mut c = sample_rgba_norm(img, u, v);
426            let fade = sprite.wipe_fx_params[0].clamp(0.0, 1.0);
427            let progress = sprite.wipe_fx_params[1].clamp(0.0, 1.0);
428            let brightness = 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2];
429            if brightness < 1.0 - progress {
430                c[3] *= (fade * (1.0 - progress)).clamp(0.0, 1.0);
431            }
432            c
433        }
434        10 => {
435            let Some(src) = wipe_src_img else {
436                return sample_rgba_norm(img, u, v);
437            };
438            let cut = sprite.wipe_fx_params[0].max(1e-5);
439            let aspect = sprite.wipe_fx_params[1].max(1e-5);
440            let progress = sprite.wipe_fx_params[2].clamp(0.0, 1.0);
441            let variant = sprite.wipe_fx_params[3] as i32;
442            let cu = cut;
443            let cv = (cut * aspect).max(1e-5);
444            let uu = (u / cu).floor() * cu;
445            let vv = (v / cv).floor() * cv;
446            let oldc = sample_rgba_norm(src, uu, vv);
447            let newc = sample_rgba_norm(img, uu, vv);
448            if variant == 230 {
449                if progress < 0.5 {
450                    oldc
451                } else {
452                    newc
453                }
454            } else if sprite.tonecurve_sat < 0.5 {
455                mix_rgba(newc, oldc, 1.0 - progress)
456            } else {
457                mix_rgba(oldc, newc, progress)
458            }
459        }
460        11 | 12 => {
461            let Some(src) = wipe_src_img else {
462                return sample_rgba_norm(img, u, v);
463            };
464            let fraction_num = sprite.wipe_fx_params[0].max(1.0);
465            let wave_num = sprite.wipe_fx_params[1];
466            let power = sprite.wipe_fx_params[2];
467            let progress = sprite.wipe_fx_params[3].clamp(0.0, 1.0);
468            let mut tex_coord_for_sin = if sprite.wipe_fx_mode == 11 {
469                v * fraction_num
470            } else {
471                u * fraction_num
472            };
473            tex_coord_for_sin = tex_coord_for_sin.fract();
474            tex_coord_for_sin = (tex_coord_for_sin - fraction_num * 0.1) / fraction_num;
475            let delta = (std::f32::consts::PI * progress * power
476                + tex_coord_for_sin * std::f32::consts::PI * wave_num)
477                .sin()
478                * raster_amp(progress);
479            let (nu, nv) = if sprite.wipe_fx_mode == 11 {
480                (u + delta, v)
481            } else {
482                (u, v + delta)
483            };
484            let oldc = sample_rgba_norm(src, nu, nv);
485            let newc = sample_rgba_norm(img, nu, nv);
486            mix_rgba(oldc, newc, progress)
487        }
488        13 => {
489            let Some(src) = wipe_src_img else {
490                return sample_rgba_norm(img, u, v);
491            };
492            let center = (sprite.wipe_fx_params[0], sprite.wipe_fx_params[1]);
493            let blur_power = sprite.wipe_fx_params[2];
494            let blur_coeff = sprite.wipe_fx_params[3].max(0.0);
495            let mixv = sprite.tonecurve_row.clamp(0.0, 1.0);
496            let oldc = sample_explosion_src(src, u, v, center, blur_power, blur_coeff);
497            let newc = sample_explosion_src(img, u, v, center, blur_power, blur_coeff);
498            mix_rgba(oldc, newc, mixv)
499        }
500        _ => sample_rgba_norm(img, u, v),
501    }
502}
503
504fn mix_rgba(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] {
505    let tt = t.clamp(0.0, 1.0);
506    [
507        a[0] * (1.0 - tt) + b[0] * tt,
508        a[1] * (1.0 - tt) + b[1] * tt,
509        a[2] * (1.0 - tt) + b[2] * tt,
510        a[3] * (1.0 - tt) + b[3] * tt,
511    ]
512}
513
514fn sample_explosion_src(
515    img: &RgbaImage,
516    u: f32,
517    v: f32,
518    center: (f32, f32),
519    blur_power: f32,
520    blur_coeff: f32,
521) -> [f32; 4] {
522    let mut dir = (center.0 - u, center.1 - v);
523    let len = (dir.0 * dir.0 + dir.1 * dir.1).sqrt();
524    if len <= 1e-5 || blur_power <= 1e-5 {
525        return sample_rgba_norm(img, u, v);
526    }
527    let texel = 1.0 / (img.width.max(img.height).max(1) as f32);
528    dir.0 = dir.0 / len * texel * blur_power * len * blur_coeff;
529    dir.1 = dir.1 / len * texel * blur_power * len * blur_coeff;
530    let taps = [
531        (0.0, 0.19f32),
532        (1.0, 0.17),
533        (2.0, 0.15),
534        (3.0, 0.13),
535        (4.0, 0.11),
536        (5.0, 0.09),
537        (6.0, 0.07),
538        (7.0, 0.05),
539        (8.0, 0.03),
540        (9.0, 0.01),
541    ];
542    let mut out = [0.0f32; 4];
543    for (k, w) in taps {
544        let s = sample_rgba_norm(img, u + dir.0 * k, v + dir.1 * k);
545        for i in 0..4 {
546            out[i] += s[i] * w;
547        }
548    }
549    out
550}
551
552fn edge(a: (f32, f32), b: (f32, f32), p: (f32, f32)) -> f32 {
553    (p.0 - a.0) * (b.1 - a.1) - (p.1 - a.1) * (b.0 - a.0)
554}
555
556fn src_clip_rect(
557    clip: Option<ClipRect>,
558    img_w: u32,
559    img_h: u32,
560) -> Result<(f32, f32, f32, f32), ()> {
561    if let Some(c) = clip {
562        let mut left = c.left.max(0) as f32;
563        let mut top = c.top.max(0) as f32;
564        let mut right = c.right.max(0) as f32;
565        let mut bottom = c.bottom.max(0) as f32;
566        let max_w = img_w as f32;
567        let max_h = img_h as f32;
568        left = left.min(max_w);
569        right = right.min(max_w);
570        top = top.min(max_h);
571        bottom = bottom.min(max_h);
572        if right <= left || bottom <= top {
573            return Ok((0.0, 0.0, max_w, max_h));
574        }
575        Ok((left, top, right, bottom))
576    } else {
577        Ok((0.0, 0.0, img_w as f32, img_h as f32))
578    }
579}
580
581fn dst_scissor_rect(clip: Option<ClipRect>, win_w: u32, win_h: u32) -> Option<ScissorRect> {
582    let c = clip?;
583    let mut left = c.left.max(0) as i64;
584    let mut top = c.top.max(0) as i64;
585    let mut right = c.right.max(0) as i64;
586    let mut bottom = c.bottom.max(0) as i64;
587    let max_w = win_w as i64;
588    let max_h = win_h as i64;
589    left = left.min(max_w);
590    right = right.min(max_w);
591    top = top.min(max_h);
592    bottom = bottom.min(max_h);
593    if right <= left || bottom <= top {
594        return Some(ScissorRect {
595            x: 0,
596            y: 0,
597            w: 0,
598            h: 0,
599        });
600    }
601    Some(ScissorRect {
602        x: left as u32,
603        y: top as u32,
604        w: (right - left) as u32,
605        h: (bottom - top) as u32,
606    })
607}
608
609#[derive(Debug, Copy, Clone)]
610struct ScissorRect {
611    x: u32,
612    y: u32,
613    w: u32,
614    h: u32,
615}
616
617fn blend_pixel(dst: [f32; 4], src: [f32; 4], blend: SpriteBlend) -> [f32; 4] {
618    let sa = src[3].clamp(0.0, 1.0);
619    let da = dst[3].clamp(0.0, 1.0);
620    let out_a = sa + da * (1.0 - sa);
621    match blend {
622        SpriteBlend::Normal => [
623            src[0] + dst[0] * (1.0 - sa),
624            src[1] + dst[1] * (1.0 - sa),
625            src[2] + dst[2] * (1.0 - sa),
626            out_a,
627        ],
628        SpriteBlend::Add => [
629            (src[0] + dst[0]).min(1.0),
630            (src[1] + dst[1]).min(1.0),
631            (src[2] + dst[2]).min(1.0),
632            out_a,
633        ],
634        SpriteBlend::Sub => [
635            (dst[0] - src[0]).max(0.0),
636            (dst[1] - src[1]).max(0.0),
637            (dst[2] - src[2]).max(0.0),
638            out_a,
639        ],
640        SpriteBlend::Mul => [
641            dst[0] * ((1.0 - sa) + src[0]),
642            dst[1] * ((1.0 - sa) + src[1]),
643            dst[2] * ((1.0 - sa) + src[2]),
644            out_a,
645        ],
646        SpriteBlend::Screen => [
647            src[0] * (1.0 - dst[0]) + dst[0],
648            src[1] * (1.0 - dst[1]) + dst[1],
649            src[2] * (1.0 - dst[2]) + dst[2],
650            out_a,
651        ],
652        SpriteBlend::Overlay => {
653            let overlay = |d: f32, s: f32| {
654                if d <= 0.5 {
655                    2.0 * d * s
656                } else {
657                    1.0 - 2.0 * (1.0 - d) * (1.0 - s)
658                }
659            };
660            [
661                overlay(dst[0], src[0]) * sa + dst[0] * (1.0 - sa),
662                overlay(dst[1], src[1]) * sa + dst[1] * (1.0 - sa),
663                overlay(dst[2], src[2]) * sa + dst[2] * (1.0 - sa),
664                out_a,
665            ]
666        }
667    }
668}